来源:LeetCode
难度:中等
问题详情:
给你一个字符串 s
,找到 s
中最长的回文子串。
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
首先要明确什么叫回文子串。回文子串的定义是一个字符串中正序和反序一样的子串。比如“sabccbad”
中就存在回文子串"abccba"
。
在真正开始介绍各种算法前,先以表格形式展示各自的时间复杂度和空间复杂度, n n n 表示字符串 s s s 的长度。
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
暴力for循环 | O( n 3 n^3 n3) | O( n n n) |
动态规划 | O( n 2 n^2 n2) | O ( n 2 ) O(n^2) O(n2) |
中心扩展 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) |
首先容易想到的就是是双层for循环遍历所有字符子串,然后判断该子串是否为回文子串。
代码较为简单,如下所示:
def longestPalindrome(s: str) -> str:
"""
一个最直接的思路就是双层for循环遍历所有子串可能,然后再对比是否反向和正向是一样的
缺点:面对超长字符串,容易超时
:param s:
:return:
"""
length = len(s)
max_len = 0
result = ''
for i in range(length):
for j in range(i, length + 1):
sub_str = s[i:j]
rever_str = sub_str[::-1]
# 因为这个判断的次数是n次,而两层for循环时间复杂度为O(n²),因此时间复杂度为O(n³)
if sub_str == rever_str:
if len(sub_str) > max_len:
result = sub_str
max_len = len(sub_str)
return result
虽然该思路较为简单易懂,但是时间复杂度也是相当的高,两层for循环的时间复杂度O( n 2 n^2 n2),同时在for循环内部对比是否为回文子串的时间复杂度为O( n n n),因此整个算法的时间复杂度为O( n 3 n^3 n3)。
对于空间复杂度,因为需要存储字符子串,而子串的最大长度与原字符串相等,所以空间复杂度为O( n n n).
看答案不难想到,对于回文子串的处理可以使用动态规划的方法。动态规划法就是将大问题转换为小问题。
还以上边的“sabccbad”
为例,大回文子串“abccba”
去掉两边的“a”
之后的”bccb“
仍是一个回文子串。
因此可以得出这样的公式:
p ( i , j ) = p ( i + 1 , j − 1 ) ∧ s [ i ] = = s [ j ] p\left( i,j\right) =p\left( i+1,j-1\right) \wedge s\left[ i\right] ==s\left[ j\right] p(i,j)=p(i+1,j−1)∧s[i]==s[j]
p ( i , j ) p\left( i,j\right) p(i,j)表示 s [ i : j + 1 ] s[i:j+1] s[i:j+1]子串是否为回文子串,这个公式表示:在满足两边字符相等的前提下,某一子串是否为回文子串取决于去掉两边后的子串是否为回文子串 ,如果条件都满足,则为True
。如此,就将长子串是否回文的问题转换到了短子串是否回文的问题,这就是动态规划的思想。
根据上面的公式,可以发现这个解法需要 s [ i ] s[i] s[i] 、 s [ j ] s[j] s[j] 和一个子串 p ( i + 1 , j − 1 ) p(i+1, j-1) p(i+1,j−1),而子串 p ( i + 1 , j − 1 ) p(i+1, j-1) p(i+1,j−1)的长度至少为1,再加上 s [ i ] s[i] s[i] 、 s [ j ] s[j] s[j] ,我们至少需要当前字符串长度至少为3,才能继续转换问题。因此该解法有两种边界问题:
”a“
"cc"
代码如下:
def longestPalindrome2(s: str) -> str:
"""使用动态规划求解"""
n = len(s)
if n < 2:
return s
max_len = 1
begin = 0
# dp[i][j] 表示 s[i..j] 是否是回文串
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True
# 递推开始
# 先枚举子串长度
for L in range(2, n + 1):
# 枚举左边界,左边界的上限设置可以宽松一些
for i in range(n):
# 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
j = L + i - 1
# 如果右边界越界,就可以退出当前循环
if j >= n:
break
if s[i] != s[j]:
dp[i][j] = False
else:
if L <= 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
# 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if dp[i][j] and L > max_len:
max_len = L
begin = i
return s[begin:begin + max_len]
代码的流程为:
时间复杂度:因为两层for循环,都是 O ( n ) O(n) O(n)级别的,而for循环内部的语句都是 O ( 1 ) O(1) O(1),因此时间复杂度是 O ( n 2 ) O(n^2) O(n2).
空间复杂度:因为需要dp存储各子串是否回文,因此空间复杂度是 O ( n 2 ) O(n^2) O(n2).
在动态规划法中已经体现了该解题方法的思路,如果一个回文子串左右两边加上同样的字符,那么新的子串同样是回文的。
基于此思想,就有了中心扩展方法。
”a“
开始。”c“
,那么就扩展到了另一个回文子串”cac“
。”a“
为回文中心能够扩展的最长回文子串上面步骤提到回文中心,结合2.2动态规划法中的边界问题,可以想到这里的回文中心也有两种:
代码如下所示:
def longestPalindrome3(s: str) -> str:
def expand(s, i, j):
"""向回文中心两边扩展,如果两边的字符相等,就可以扩展,否则终止"""
while i >= 0 and j < len(s) and s[i] == s[j]:
i -= 1
j += 1
# 因为终止的时候是超界限或者不满足相等条件了,所以再通过+1,-1指向最后一个
return i + 1, j - 1
start = end = 0
for i in range(len(s)):
# 将回文中心分为长度为1和长度为2的两种形式;
# ‘acddca’虽然是一个回文字符,但是通过某一个字符为中心,却无法扩展到整个字符串
# 因此,需要以长度为2的'dd'为中心,向两边扩展。这就是下边两行的意思。
start1, end1 = expand(s, i, i)
start2, end2 = expand(s, i, i + 1)
if end1 - start1 > end - start:
start = start1
end = end1
if end2 - start2 > end - start:
start = start2
end = end2
return s[start:end + 1]
对于时间复杂度:for循环的时间复杂度为 O ( n ) O(n) O(n),而expand函数(用于扩展子串的函数)时间复杂度为 O ( n ) O(n) O(n),因此总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
对于空间复杂度,则为 O ( 1 ) O(1) O(1)