给你一个字符串 s,找到 s 中最长的回文子串。
回文子串: 字符串中连续的非空回文子串字符序列。
回文性: 如果字符串向前和向后读都相同,则它满足回文性。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
动态规划
这段代码实现了一个寻找字符串中最长回文子串的算法,使用的是动态规划(Dynamic Programming)的方法。我们一步步解释其原理和实现思路。
1. 问题定义
回文串是指一个字符串从前往后读和从后往前读是一样的。例如,字符串 “aba” 和 “racecar” 都是回文串。给定一个字符串 s,我们要找到它的最长回文子串。
2. 思路分析
动态规划的核心思想是通过构建一个表格(或数组)来记录中间状态,从而避免重复计算。
2.1 表格定义
我们定义一个二维数组 dp,其中 dp[i][j] 表示字符串 s 的第 i 到第 j 个字符组成的子串 s[i…j] 是否为回文串。
如果 s[i…j] 是回文串,那么 dp[i][j] = true。
如果 s[i…j] 不是回文串,那么 dp[i][j] = false。
2.2 状态转移方程
如何判断 s[i…j] 是否为回文串呢?
首先,如果 s[i] == s[j],即首尾字符相同,那么我们需要继续检查子串 s[i+1…j-1] 是否为回文串。
如果 s[i+1…j-1] 是回文串,那么 s[i…j] 也是回文串,即 dp[i][j] = true。
否则,s[i…j] 不是回文串,即 dp[i][j] = false。
总结起来,可以写成:
2.3 边界条件
所有长度为1的子串都是回文串,因此 dp[i][i] = true。
对于长度为2的子串,如果两个字符相等,则它是回文串。
2.4 求解过程
通过动态规划的思想,我们从较短的子串开始,一步步构建更长的子串的回文信息。最终,我们就可以得到最长的回文子串。
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) {
return s;
}
int n = s.length();
// dp[i][j] 表示 s[i...j] 是否为回文
boolean[][] dp = new boolean[n][n];
// 记录最长回文子串的起始位置
int start = 0;
// 记录最长回文子串的长度
int maxLength = 1;
// 所有长度为1的子串都是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 检查长度为2的子串
for (int i = 0; i < n - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
dp[i][i + 1] = true;
start = i;
maxLength = 2;
}
}
// 检查长度大于2的子串
// length 表示子串的长度
for (int length = 3; length <= n; length++) {
for (int i = 0; i <= n - length; i++) {
// 子串的结束索引
int j = i + length - 1;
// 如果首尾字符相同,并且内部子串也是回文
if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) {
dp[i][j] = true;
start = i;
maxLength = length;
}
}
}
return s.substring(start, start + maxLength);
}
中心扩展法
1. 问题定义
与之前一样,问题是寻找字符串 s 中的最长回文子串。
2. 思路分析
回文串有一个特点,即它是关于中心对称的。因此,我们可以选择一个位置作为回文中心,向两边扩展,直到不能再扩展为止,这样就可以找到以这个位置为中心的最长回文。
需要注意的是,回文的中心可以是一个字符(奇数长度的回文)或者两个字符之间的间隙(偶数长度的回文)。
3. 解法步骤
3.1 中心扩展
对于字符串中的每一个字符(或字符间隙),都可以作为回文中心,然后从这个中心向外扩展来寻找回文。通过对每个可能的中心进行扩展,我们可以找到最长的回文子串。
奇数长度的回文: 以某个字符为中心,从它向左右两边扩展。
偶数长度的回文: 以两个字符之间的间隙为中心,从这个间隙向左右两边扩展。
代码实现:
public String longestPalindrome(String s) {
if (s == null || s.isEmpty()) {
return "";
}
int start = 0, maxLength = 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 > maxLength) {
maxLength = len;
start = i - (len - 1) / 2;
}
}
return s.substring(start, start + maxLength);
}
private 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;
}
详细步骤解析
检查输入有效性:
首先检查字符串是否为空或长度为0,如果是,则直接返回空字符串。
遍历每个可能的中心: 使用 for 循环遍历字符串中的每个字符 i,将它作为中心进行扩展。
调用 expandAroundCenter 函数检查奇数长度的回文(以 i 为中心)。
再调用一次 expandAroundCenter 函数检查偶数长度的回文(以 i 和 i+1 之间的间隙为中心)。
更新最长回文信息: 通过比较当前扩展得到的回文长度,如果发现更长的回文,则更新最长回文的起始位置和长度。
返回结果: 最后返回找到的最长回文子串。
5. 时间复杂度和空间复杂度
时间复杂度: 每次扩展的操作是 O(n),总共进行 n 次中心扩展,因此时间复杂度为 O(n^2)。
空间复杂度: 只使用了常数级别的额外空间,因此空间复杂度为 O(1)。
6. 总结
这种方法利用了回文的对称性,通过从每个可能的中心向外扩展,简单且有效地找到了最长的回文子串。与动态规划方法相比,代码更为简洁,并且由于不需要额外的二维数组,空间复杂度也更低