LeetCode—5.最长回文子串[Longest Palindromic Substring]——分析及代码[C++]
一、题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、分析及代码
1. 栈判断回文
(1)思路
由于回文串对称的结构特点,自然想到可以在遍历字符串的过程中,结合栈来判断子串是否为回文子串。
方法时间复杂度为 O(n^2),但每个对应位置在出现第一个非回文子串的长度即可中止,且可跳过一半回文子串长度的位置,因此实际能够得到较好的优化。
实现细节:
1)回文子串对称轴旁可能有无法确定个数的重复字符,栈方法中需特殊处理;
2)出现回文子串后,遍历过程可跳过的位置;
3)提前终止遍历的条件;
4)只需记录子串的起始位置和长度,无需复制子串内容。
(2)代码
class Solution {
public:
string longestPalindrome(string s) {
if (s.size() < 2) return s;//长度小于2直接返回
stack <char> in, tmp1;//in为已遍历的输入字符串,tmp1为当前回文子串
int length = 0, midnum = 1, size = s.size();
int start;
in.push(s[0]);
for (int i = 1; i <= size; i++) {//i = size时处理当时存储的回文子串
if (tmp1.empty() && i != size) {//tmp1栈底即对称轴位置
tmp1.push(s[i]);
while (!in.empty() && in.top() == s[i]) {//在子串对称轴位置,处理当前位置之前连续出现的重复字符
in.pop();
midnum++;//记录重复个数
}
while (i < size - 1 && s[i] == s[i + 1]) {//在子串对称轴位置,处理当前位置之后连续出现的重复字符
midnum++;//记录重复个数
i++;//跳过相应位置
}
}
else if (!in.empty() && s[i] == in.top() && i != size) {//遍历到的新字符符合回文条件,入栈
tmp1.push(s[i]);
in.pop();
}
else {//遍历到的新字符不符合回文,处理之前回文子串
if (i == size && tmp1.empty()) break;
int tlength = (tmp1.size() - 1) * 2 + midnum;//回文子串长度
if (tlength > length) {
start = i - tlength;
length = tlength;
}
i -= tmp1.size();//遍历位置跳回对称轴(从当前子串起始位置到对称轴左边的回文子串一定小于当前回文子串,可跳过)
while (!tmp1.empty()) {
char c = tmp1.top();
in.push(c);
tmp1.pop();
}
for (; midnum > 1; midnum--) {//处理栈底(对称轴)附近重复字符
in.push(in.top());
}
if ((i + length / 2) > size) break;//提前终止条件
}
}
return s.substr(start, length);
}
};
(3)结果
执行用时 : 8 ms, 在所有 C++ 提交中击败了 97.84% 的用户;
内存消耗 : 9.1 MB。
实现过程略不简洁,但优化效果较好。
2. 动态规划
(1)思路
本题的状态转移方程清晰直观,可考虑用动态规划方法求解。
//i, j为字符串对应位置,substr(i, j)表示[i, j]是否为回文子串
substr(i, j) = substr(i + 1, j - 1) && substr[i] == substr[j];
分析 dp[i][j] 计算过程可发现,实际计算时只需用到 dp[i][j - 1],因此可用一维数组存储状态结果。
理论上时间复杂度为 O(n ^ 2),但实际计算过程中几乎遍历了所有子串,导致计算量明显偏大。
(2)代码
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
if (size < 2) return s;
int start, length = 0;
bool dp[1000];
for (int i = size - 1; i >= 0; i--) {
for (int j = size - 1; j >= i; j--) {
if (i == j)
dp[j] = true;
else if (i == j - 1)
dp[j] = s[i] == s[j] ? true : false;
else
dp[j] = dp[j - 1] && s[i] == s[j];
if (dp[j] && j - i + 1> length) {
start = i;
length = j - i + 1;
}
}
}
return s.substr(start, length);
}
};
(3)结果
执行用时 : 120 ms, 在所有 C++ 提交中击败了 49.98% 的用户;
内存消耗 : 8.6 MB。
能够完成题目,代码清晰简洁,但优化效果较弱。
3. 中心扩展法
(1)思路
对长度为 n 的字符串,对称轴分别可能在 n 个字符或 n - 1 个字符间的空隙中。
采用中心扩展法可以简洁直观地对每个中心位置的最长子串进行求解,不符合条件后立刻返回,在 O(n ^ 2) 复杂度的基础上达到一定优化效果。
(2)代码
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
if (size < 2) return s;
int start, length = 0;
for (int i = 1; i < size; i++) {
int l1 = palindromeLength(i, i, size, s);
int l2 = palindromeLength(i - 1, i, size, s);
int l = max(l1, l2);
if (l > length) {
length = l;
start = i - length / 2;
}
}
return s.substr(start, length);
}
private:
int palindromeLength(int l, int r, int size, string& s) {
while (l >= 0 && size > r && s[l] == s[r]) {
l--;
r++;
}
return r - l - 1;
}
};
(3)结果
执行用时 : 32 ms, 在所有 C++ 提交中击败了 80.91% 的用户;
内存消耗 : 8.8 MB。
代码清晰简洁,且具有一定优化效果。
三、其他
1.截取部分字符串的函数
1)C++中substr的参数为起始位置和长度;
//C++
s.substr(pos, n);
//返回值string,包含s中从pos开始n个字符的拷贝
//pos的默认值为0,n默认值为s.size() - pos,即默认拷贝整个字符串
2)Java中substring的参数为起始位置和终止位置。
s.substring(int beginIndex, int endIndex)
//返回值string,包含s中从beginIndex至endIndex的拷贝