Medium | LeetCode 5. 最长回文子串 | 动态规划 | 中心拓展法
5. 最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母(大写和/或小写)组成
解题思路
方法一: 动态规划
P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串(下文表示成 s[i:j])是否为回文串, 动态规划方程如下:
\[P(i, j)=P(i+1, j-1) \wedge\left(S_{i}==S_{j}\right)
\]
在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。从边界为0出发, 逐渐向外拓展, 先计算长度为1的子串是否是回文串, 然后计算长度为2的所有子串是否是回文串, 再次基础上再计算长度为3的是否是回文串, 然后逐步计算到长度为N的是否是回文串。
这种动态规划的算法实际上就是填写一张二维表格, 所以时间复杂度是O(N^2), 空间复杂度是O(N^2).
public String longestPalindrome(String s) {
int n = s.length();
// dp[i][j] 表示 从i到j是一个回文串
boolean[][] dp = new boolean[n][n];
String ans = "";
for (int len = 0; len < n; ++len) {
for (int i = 0; i + len < n; ++i) {
int j = i + len;
if (len == 0) {
// 初始化对角线
dp[i][j] = true;
} else if (len == 1) {
// 只有两个元素的情况
dp[i][j] = (s.charAt(i) == s.charAt(j));
} else {
// 三个及以上元素的情况
dp[i][j] = (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]);
}
// 每次找到一个更大的长度的回文子串, 变更新回文子串。
if (dp[i][j] && len + 1 > ans.length()) {
ans = s.substring(i, i + len + 1);
}
}
}
return ans;
}
方法二: 中心拓展法
中心拓展法的思想是, 枚举所有中心。 从中心位置处开始向左右两边延伸, 并且在延伸过程当中分为奇数和偶数两种情况讨论。在延伸直到不可拓展的最大值, 即可得到最大的回文子串。
这种算法只需要枚举中心并且原地向两端延伸, 所以这种算法的时间复杂度是O(N^2), 不需要其他的任何的额外的存储空间, 所以空间复杂度是O(1)
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
// 计算为奇数的回文子串的长度
int len1 = expandAroundCenter(s, i, i);
// 计算长度为偶数的回文子串的长度, 并且此字符和下一个字符是回文子串的中心
int len2 = expandAroundCenter(s, i, i + 1);
// 得到最大的回文子串的长度
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
// 中心拓展法计算回文子串的长度
public int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() // 回文子串的两个指针不能越界。
&& s.charAt(left) == s.charAt(right)) { // 回文子串的两个字符的下标相同
--left;
++right;
}
return right - left - 1;
}