主要记录解题过程,反思如何构思代码。
原题:https://leetcode-cn.com/problems/longest-palindromic-substring
题目:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
解题过程
看到这题一开始是完全懵逼的,看着两个例子想了一个错的解法:用两个指针指向字符串的首尾,当两个指针所指的内容相同时,记录下两个索引值,不同时索引值归零,直到两个指针相遇。
这个解法很明显是错的,因为它假定了最长回文子串的中心一定是字符串中心
这个时候我看到了题目的三条提示:
How can we reuse a previously computed palindrome to compute a larger palindrome?
If “aba” is a palindrome, is “xabax” and palindrome? Similarly is “xabay” a palindrome?
Complexity based hint:
If we use brute-force and check whether for every start and end position a substring is a palindrome we have O(n^2) start - end pairs and O(n) palindromic checks. Can we reduce the time for palindromic checks to O(1) by reusing some previous computation.
前两条提示非常有用,我想到回文的特征是:呈中心对称。即,两个指针以同样的步幅从回文的中心出发,所指内容应该会一直保持一致。应该还是使用两个指针,但是是从内到外走,而不是从外到内
总体算法思路为:遍历整个字符串,将每个位置当作回文中心去找以这个字符为中心能形成的最长回文,然后选出整个字符串最长的回文
这里有一个很干扰我思路的问题出现了:就是'cbbd'这种情况。我的算法是无法输出'bb'的,在这个地方卡了很久。后来我决定把这种情况特殊处理,如果第i个字符串与第i+1个字符串相同,则将尾指针往后移。
我觉得有一个经验就是,在时间紧迫的时候,先把自己能想出来的完整算法写出来,再想办法去处理特殊情况,否则在那空想是很浪费时间的。
第一版:
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
longest_palindrome = {'length': 0, 'str': ''}
for i in range(n):
index1 = index2 = i
if i+1 < n and s[i+1] == s[i]:
index1 = i
index2 = i+1
while index1-1 >= 0 and index2+1 < n and s[index1-1] == s[index2+1]:
index1 += -1
index2 += 1
length = index2 - index1 + 1
if length > longest_palindrome['length']:
longest_palindrome['length'] = length
longest_palindrome['str'] = s[index1:index2+1]
return longest_palindrome['str']
结果:不通过
不通过的用例:
输入:
"ccc"
输出:
"cc"
预期:
"ccc"
在第二个c的位置,由于第三个c与第二个c相等,则index1=1,index2=2,将回文的中心变成了这两个字符,所以出错。
解决的思路是:找全所有所有相同的字符组成一个回文中心,即在处理第一个c的时候,就一直往后找,直到回文中心变为"ccc"
第二版:
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
longest_palindrome = {'length': 0, 'str': ''}
for i in range(n):
index1 = index2 = i
while index2+1 < n and s[index1] == s[index2+1]:
index2 += 1
while index1-1 >= 0 and index2+1 < n and s[index1-1] == s[index2+1]:
index1 += -1
index2 += 1
length = index2 - index1 + 1
if length > longest_palindrome['length']:
longest_palindrome['length'] = length
longest_palindrome['str'] = s[index1:index2+1]
return longest_palindrome['str']
可以再优化一下,不要每次找到更长的回文就切出来,只需要记下索引就好了
第三版:
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
l_len = 0
l_index1 = 0
l_index2 = 0
for i in range(n):
index1 = index2 = i
while index2+1 < n and s[index1] == s[index2+1]:
index2 += 1
while index1-1 >= 0 and index2+1 < n and s[index1-1] == s[index2+1]:
index1 += -1
index2 += 1
length = index2 - index1 + 1
if length > l_len:
l_len = length
l_index1 = index1
l_index2 = index2
return s[l_index1: l_index2+1]
二刷
就因为加了“下一次mid的选择越过相同的字符”这一个操作,执行用时从800ms降到90ms
应该是测试用例中有很多连在一起的相同字符
class Solution:
def longestPalindrome(self, s: str) -> str:
# 遍历s,将每个字符当作回文中心扩散
n = len(s)
if n == 0:
return ""
# 记录最长的回文子串,初始为第一个字符
res = s[0]
# 回文中心
mid = 0
while mid < n:
# left和right指向回文子串的前一个字符和后一个字符
left, right = mid - 1, mid + 1
# 找全所有相同的字符组成回文中心
while right < n and s[right] == s[mid]:
right += 1
# 下一次mid的选择应该越过这些相同的字符
mid += 1
while left >= 0 and right < n and s[left] == s[right]:
left -= 1
right += 1
# 当前回文串的长度
if right - left - 1 > len(res):
res = s[left+1:right]
mid += 1
return res
时间复杂度:O(n2)。最多有n个回文中心,每个回文中心最多向外拓展n次
空间复杂度:O(1)