回文串这个经典问题在算法面试中出现的频率相当高,尤其是像"最长回文子串"这样的题目。我第一次遇到这个问题是在一次重要的技术面试中,当时就被问得措手不及。后来经过系统学习和反复练习,才发现这类问题其实有章可循。
回文串指的是正读反读都相同的字符串,比如"aba"、"abba"都是典型的回文串。而最长回文子串问题,就是要求我们在给定的字符串中找出最长的那个回文子串。看似简单的问题背后,却蕴含着多种精妙的解法。
最直观的解法当然是暴力枚举:检查所有可能的子串,判断是否为回文,然后记录最长的那个。具体来说:
这样总的时间复杂度就是O(n³),对于较长的字符串来说显然不够高效。
python复制def longestPalindrome_brute(s: str) -> str:
n = len(s)
if n < 2:
return s
max_len = 1
res = s[0]
for i in range(n-1):
for j in range(i+1, n):
if j-i+1 > max_len and s[i:j+1] == s[i:j+1][::-1]:
max_len = j-i+1
res = s[i:j+1]
return res
虽然暴力解法思路简单,但它的性能瓶颈非常明显。当字符串长度达到1000时,计算量将达到10^9级别,这在力扣等在线判题系统中肯定会超时。因此,我们需要寻找更高效的算法。
提示:在实际面试中,即使你能想到暴力解法,面试官通常也会追问更优的解法,所以掌握高效算法是必须的。
动态规划是解决最长回文子串问题的经典方法。其核心思想是利用已经计算过的子问题的解来构建更大问题的解,避免重复计算。
定义dp[i][j]表示字符串s从i到j的子串是否为回文。那么状态转移方程为:
基本情况:
状态转移:
python复制def longestPalindrome_dp(s: str) -> str:
n = len(s)
if n < 2:
return s
dp = [[False]*n for _ in range(n)]
max_len = 1
start = 0
# 所有长度为1的子串都是回文
for i in range(n):
dp[i][i] = True
# 检查长度为2的子串
for i in range(n-1):
if s[i] == s[i+1]:
dp[i][i+1] = True
start = i
max_len = 2
# 检查长度大于2的子串
for length in range(3, n+1):
for i in range(n-length+1):
j = i + length - 1
if s[i] == s[j] and dp[i+1][j-1]:
dp[i][j] = True
if length > max_len:
start = i
max_len = length
return s[start:start+max_len]
动态规划解法的时间复杂度为O(n²),空间复杂度也是O(n²)。相比暴力解法有了显著提升,但对于特别长的字符串(比如n>5000),空间消耗可能会成为问题。
注意:在实现时要注意填表的顺序,必须确保在计算dp[i][j]时,dp[i+1][j-1]已经被计算过。这就是为什么外层循环是子串长度,而不是起始位置。
中心扩散法利用了回文串的对称性质。对于任何一个回文串,我们可以从它的中心开始,向两边扩散寻找更长的回文。
这里需要注意回文串的奇偶性:
python复制def expandAroundCenter(s: str, left: int, right: int) -> int:
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return right - left - 1
def longestPalindrome_center(s: str) -> str:
if not s or len(s) < 1:
return ""
start, end = 0, 0
for i in range(len(s)):
len1 = expandAroundCenter(s, i, i) # 奇数长度
len2 = expandAroundCenter(s, i, i+1) # 偶数长度
max_len = max(len1, len2)
if max_len > end - start:
start = i - (max_len - 1) // 2
end = i + max_len // 2
return s[start:end+1]
中心扩散法的时间复杂度同样是O(n²),但空间复杂度优化到了O(1),因为它不需要存储额外的DP表。在实际运行中,中心扩散法通常比动态规划更快,因为它避免了不必要的计算。
| 方法 | 时间复杂度 | 空间复杂度 | 实际运行速度 |
|---|---|---|---|
| 暴力解法 | O(n³) | O(1) | 非常慢 |
| 动态规划 | O(n²) | O(n²) | 中等 |
| 中心扩散 | O(n²) | O(1) | 较快 |
动态规划适用场景:
中心扩散法适用场景:
在实际编码中,我发现中心扩散法有几个小技巧可以优化:
python复制# 优化后的中心扩散法
def longestPalindrome_optimized(s: str) -> str:
if not s:
return ""
# 预处理字符串,统一奇偶情况
processed = '#' + '#'.join(s) + '#'
n = len(processed)
p = [0] * n # 记录每个中心的回文半径
center = right = 0
max_len = 1
res_center = 0
for i in range(n):
if i < right:
mirror = 2 * center - i
p[i] = min(right - i, p[mirror])
# 尝试扩展
l, r = i - (1 + p[i]), i + (1 + p[i])
while l >= 0 and r < n and processed[l] == processed[r]:
p[i] += 1
l -= 1
r += 1
# 更新中心和右边界
if i + p[i] > right:
center = i
right = i + p[i]
# 更新最大回文
if p[i] > max_len:
max_len = p[i]
res_center = i
start = (res_center - max_len) // 2
return s[start:start+max_len]
在实现这些算法时,有几个常见的边界条件需要特别注意:
当你的代码不能正确处理某些测试用例时,可以:
在力扣上提交代码时要注意:
与子串不同,子序列不要求连续。这个问题也可以用动态规划解决,但状态转移方程有所不同:
python复制def longestPalindromeSubseq(s: str) -> int:
n = len(s)
dp = [[0]*n for _ in range(n)]
for i in range(n-1, -1, -1):
dp[i][i] = 1
for j in range(i+1, n):
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
将字符串分割成若干回文子串,求最小分割次数。这也是一个经典的DP问题:
python复制def minCut(s: str) -> int:
n = len(s)
is_palindrome = [[False]*n for _ in range(n)]
dp = [0]*n
for i in range(n):
min_cut = i
for j in range(i+1):
if s[j] == s[i] and (i-j < 2 or is_palindrome[j+1][i-1]):
is_palindrome[j][i] = True
min_cut = 0 if j == 0 else min(min_cut, dp[j-1]+1)
dp[i] = min_cut
return dp[-1]
Manacher算法可以在O(n)时间内解决最长回文子串问题,它是对中心扩散法的优化:
python复制def longestPalindrome_manacher(s: str) -> str:
# 预处理字符串
if not s:
return ""
T = '#'.join('^{}$'.format(s))
n = len(T)
P = [0] * n
C = R = 0
for i in range(1, n-1):
# 利用对称性快速填充P[i]
if R > i:
P[i] = min(R - i, P[2*C - i])
# 尝试扩展
while T[i + P[i] + 1] == T[i - P[i] - 1]:
P[i] += 1
# 更新中心和右边界
if i + P[i] > R:
C, R = i, i + P[i]
# 找出最大的P[i]
max_len, center = max((val, idx) for idx, val in enumerate(P))
start = (center - max_len) // 2
return s[start:start+max_len]
在实际面试和刷题过程中,我发现掌握最长回文子串问题的解法有几点特别重要:
理解本质:回文串的对称性是所有解法的核心,无论是DP还是中心扩散都基于这一点。
画图辅助:在纸上画出DP表的填充过程或中心扩散的示意图,能帮助理解算法逻辑。
边界处理:特别注意字符串为空、单字符、双字符等边界情况。
代码简洁:中心扩散法的代码通常比DP更简洁,在面试中更容易正确实现。
提前优化:在写出基础解法后,思考是否有优化空间(如提前终止、空间优化等)。
最后,建议在力扣上多次练习这道题的不同解法,直到能够不参考任何资料独立写出正确代码。这不仅有助于掌握回文串问题,也能提升动态规划和双指针等核心算法的应用能力。