- Posted by 微博@Yangsc_o
- 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
本题是leetcode,地址:5. 最长回文子串
题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:输入: "cbbd"
输出: "bb"
分析
题目还是很好理解的,思路有大致三种,一种暴力解、一种动态规划、一种是中心扩散法;
暴力解决分析:根据回文子串的定义,枚举所有长度大于等于 2的子串,依次判断它们是否是回文,在枚举的过程中需要记录“当前子串的起始位置”和“子串长度”,最后通过substring截取一下对应的字符串就好了。很容发现,这种解法效率很低!时间复杂度:O(N^3),N是字符串的长度,枚举字符串的左边界、右边界,然后继续验证子串是否是回文子串,这三种操作都与 NN 相关;
code-暴力解决
public boolean check(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
int start = 0, maxLenght = 1;
char[] chars = s.toCharArray();
for (int left = 0; left < chars.length; left++) {
for (int right = chars.length - 1; right > left; right--) {
boolean check = check(s, left, right);
if (check && right - left + 1 > maxLenght) {
start = left;
maxLenght = right - left + 1;
break;
}
}
}
return s.substring(start, start + maxLenght);
}
code-动态规划
在设计动态规划时,需要考虑的点如下:
思考状态
思考状态转移方程
思考初始化
思考输出
思考优化空间
回文字符串的特点:
如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;
如果一个字符串的头尾两个字符相等,才有必要继续判断下去。
如果里面的子串是回文,整体就是回文串;
如果里面的子串不是回文串,整体就不是回文串。
如果dp[i] [j] 表示子串 s[i..j]
是否为回文子串,这里子串 s[i..j]
定义为左闭右闭区间,可以取到 s[i]
和 s[j]
- 状态转换公式:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
边界条件:表达式 [i + 1, j - 1]
不构成区间,即长度严格小于 2
,即 j - 1 - (i + 1) + 1 < 2
,即 j - i < 3
。怎么理解?即s[i..j]
的长度等于 2
或者等于 3
的时候,其实只需要判断一下头尾两个字符是否相等就可以直接下结论,而不需要参考之前的结果;
- 初始化:单个字符一定是回文串,因此把对角线先初始化为
true
,即dp[i][i] = true
- 输出:
dp[i][j] = true
,记录子串的长度和起始位置,当然我们是找最长的,需要跟上次记录的做比较;
时间复杂度:O(N^2)
根据以上信息就可以写我们的代码了
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int begin = 0, maxLen = 1;
// 状态记录
boolean[][] dp = new boolean[len][len];
// 初始化:单个字符一定是回文串,因此把对角线先初始化为 `true`,即 `dp[i][i] = true`
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
for (int j = 1; j < len ; j ++) {
for (int i = 0; i < len; i ++) {
// 判断是否相等
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
// 边界条件:即`s[i..j]` 的长度等于 `2` 或者等于 `3`
if (j - i < 3) {
dp[i][j] = true;
} else {
// 状态转换公式:dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
dp[i][j] = dp[i + 1][j - 1];
}
}
// 记录begin和长度
if (dp[i][j] && j - i + 1 > maxLen) {
begin = i;
maxLen = j - i + 1;
}
}
}
return s.substring(begin, begin + maxLen);
}
code-中心扩散法
暴力法采用双指针两边夹,验证是否是回文子串。如果枚举可能出现的回文子串的“中心位置”,从“中心位置”尝试尽可能扩散出去,得到一个回文串。遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。枚举“中心位置”时间复杂度为 O(N),从“中心位置”扩散得到“回文子串”的时间复杂度为 O(N),时间复杂度可以降到 O(N^2)。
- 奇数回文串的“中心”是一个具体的字符,例如:回文串 "aba" 的中心是字符 "b";
- 偶数回文串的“中心”是位于中间的两个字符的“空隙”,例如:回文串串 "abba" 的中心是两个 "b" 中间的那个“空隙”。
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
String res = "";
int maxLength = 0;
for (int i = 0; i < len; i++) {
String one = around(s, i, i);
String two = around(s, i, i + 1);
String maxString = one.length() > two.length() ? one : two;
if (maxString.length() > maxLength) {
res = maxString;
maxLength = res.length();
}
}
return res;
}
public String around(String s, int left, int right) {
while (left >= 0 && right < s.length()) {
if (s.charAt(left) == s.charAt(right)) {
left--;
right++;
} else {
break;
}
}
return s.substring(left + 1, right);
}