题目描述
给定一个字符串 s,找到 s 中最长的回文子串。
回文串从左向右读和从右向左读的结果相同,例如 "aba" 和 "abba"。
最初思路
最开始采用暴力枚举:
- 用两重循环枚举子串的左右端点
i、j。 - 当首尾字符相同时,再调用
isHuiWen检查整个区间。 - 如果该区间是回文串,并且长度超过当前答案,就更新最长回文串。
这个方法逻辑直接,也能够通过,但存在大量重复检查。
问题出在哪里
两重循环需要枚举 O(n^2) 个子串,每次调用 isHuiWen 最多还要扫描 O(n) 个字符,因此最坏时间复杂度为 O(n^3)。
优化的关键不是加快一次回文判断,而是改变枚举方式:不再枚举左右端点,而是枚举回文串的中心。
核心洞察
每个回文串都关于中心对称,但中心有两种形式:
- 奇数长度回文:中心是一个字符,例如
"aba",中心可以表示为(i, i)。 - 偶数长度回文:中心是两个字符之间的空隙,例如
"abba",中心可以表示为(i, i + 1)。
遍历每个位置 i,分别尝试这两种中心。只要左右字符相同,就让左指针向左、右指针向右继续扩展。
扩展过程
以 "abba" 为例,偶数中心位于两个 'b' 之间:
初始:l = 1,r = 2
第一次匹配:s[1] == s[2],继续扩展
第二次匹配:s[0] == s[3],继续扩展
结束:l = -1,r = 4
循环结束时,左右指针已经各多走了一步,所以:
- 实际回文起点:
l + 1 - 实际回文终点:
r - 1 - 实际回文长度:
r - l - 1
辅助方法只返回回文长度后,可以根据中心下标 i 和长度 len,用 i - (len - 1) / 2 统一计算奇数、偶数回文的起点。
伪代码
下面的伪代码和 Java 实现一一对应:
函数 longestPalindrome(s):
如果 s 的长度小于 2:
返回 s
start <- 0
maxLen <- 1
对每个下标 i:
oddLen <- expand(s, i, i)
evenLen <- expand(s, i, i + 1)
currentLen <- oddLen 和 evenLen 中的较大值
如果 currentLen > maxLen:
maxLen <- currentLen
start <- i - (currentLen - 1) / 2
返回 s 从 start 开始、长度为 maxLen 的子串
函数 expand(s, left, right):
当 left、right 没有越界,并且 s[left] == s[right]:
left <- left - 1
right <- right + 1
返回 right - left - 1
Java 代码
class Solution {
public String longestPalindrome(String s) {
if (s.length() < 2) {
return s;
}
int start = 0;
int maxLen = 1;
for (int i = 0; i < s.length(); i++) {
int oddLen = expand(s, i, i);
int evenLen = expand(s, i, i + 1);
int currentLen = Math.max(oddLen, evenLen);
if (currentLen > maxLen) {
maxLen = currentLen;
start = i - (currentLen - 1) / 2;
}
}
return s.substring(start, start + maxLen);
}
private int expand(String s, int left, int right) {
while (left >= 0
&& right < s.length()
&& s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
}
易错点
1. 只处理奇数长度回文
如果只从 (i, i) 开始扩展,会漏掉 "abba" 这类偶数长度回文。每个位置都要检查 (i, i) 和 (i, i + 1)。
2. 扩展结束后的长度多算两个字符
循环退出时,left 和 right 已经指向回文区间之外,因此长度不是 right - left + 1,而是 right - left - 1。
3. 子串结束下标不包含在结果中
Java 的 substring(start, end) 不包含 end,所以应返回 s.substring(start, start + maxLen)。
4. 最短答案的初始化
非空字符串至少存在一个长度为 1 的回文串,因此可以初始化 start = 0、maxLen = 1。
建议测试用例
s = "babad" -> "bab" 或 "aba"
s = "cbbd" -> "bb"
s = "a" -> "a"
s = "ac" -> "a" 或 "c"
s = "abba" -> "abba"
s = "aaaa" -> "aaaa"
复杂度分析
- 时间复杂度:
O(n^2)。共有O(n)个位置,每个位置进行两次中心扩展,每次最多扩展O(n)。 - 空间复杂度:
O(1)。除返回的子串外,只使用常数个变量。
总结
这题的优化关键是利用回文串的中心对称性,将"枚举子串并完整检查"改成"枚举中心并向两边扩展"。
实现时需要同时处理奇数、偶数两种中心,并牢记扩展结束后指针已经多走一步,实际长度为 right - left - 1。