leetcode——Longest Palindromic Substring
题目:Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.
大意是:从所给的字符串s中找出其最长回文字串
例如:s = “aabcba”, 则最长回文字串为”abcba”
思路1:
我一开始的思路:遍历s,找出两个相同字母之间的最长回文字串,因此要记录后一个字母的位置,将其存到一个map里。但是还是超时。
class Solution { public: string longestPalindrome(string s) { // 用map记录每个字母的位置 // 遍历s,找出后面同一个字母的位置,看其之间是否是回文 unordered_map<char, vector<int> > index; for(int i = s.size()-1; i >= 0; i--){ char c = s[i]; vector<int> tmp; if(index.find(c) != index.end()) tmp = index[c]; tmp.push_back(i); index[c] = tmp; } int maxRet(1); string maxSub(""+s[0]); for(int i = 0; i != s.size(); i ++){ if(s.size() - i <= maxRet) break; char c = s[i]; auto tmp = index[c]; for(int j: tmp){ // i后面的同一个字符的索引 if(j > i){ // 如果字符串本身长度都没maxRet大,则不用判断其是否是回文 if(j-i+1 <= maxRet) break; int count(0); int k = i; while(c == s[j] && k < j){ count+=2; k++; j--; } if(k >= j){ // 是回文字串 // 说明是回文字符串且中间有单个字符 if(k == j) count--; if(maxRet < count){ maxRet = count; maxSub = s.substr(i, count); } } } } } return maxSub; } };
思路2:
后来看了网上的答案,用到Manaer’s Algorithm,时间复杂度是O(n), 参考了博文之后自己写了一遍,大致了解了算法的过程。
算法的基本思想是要把s=”babcbabcbaccba”变为T=”#b#a#b#c#b#a#b#c#b#a#c#c#b#a#”,之后在T的基础上进行查找。
如图,把s=”babcbabcbaccba”变为T之后, #的作用是不管s的长度是奇数还是偶数,使得以T[i]为中心的回文字符串都是奇数;
p[i]是以T[i]为中心的回文字符串的长度;
C是最近一次算出的最长回文字符串的中心字符的位置;
R是以C为中心的回文字符串的最右边的字符的位置,同理,L是最左边的位置,则L到R之间是以C为中心的回文字符串。
上图的意思是当i==15时,i属于以T[C]为中心的、左右两边长为9的回文字符串的范围内。
p[i]的这个长度最终由下面算出来的
while(T[i+1+p[i]] == T[i-1-p[i]]){ p[i]++; }
当i==0时,T[0]之前没有回文字串,因此p[0]是0,然后T[0]的左边T[-1]和右边T[1]不是同样的字符,所以p[i]还是0;
当i==1时,T[1]左右有T[0]==T[2],因此p[1]++,因此p[1]为1;
以此类推,T[3]即a的左边有3个和3个右边的字符相等,所以p[3]为3
上述p[i]的算法是扩充,初始的p[i]是下面算出来的,由初始到扩充的这样的好处是优化了时间复杂度是O(n²)的直接遍历s[i]算出以其为中心的回文字符串长度的那种算法:
int i_mirror = 2*C - i; // i` = C – (i-C) p[i] = R>i ? min(p[i_mirror], R-i) : 0;
这个的意思是
1. 若前面算出的以T[C]为中心的回文字符串很长,当前T[i]属于其回文中心内,即R>i,则可以找到其在回文字符串中对应的i`的p[i`],由其算出p[i], 例如,T[11]的回文串长度为9,则R则到了20,那么算T[15]时,则有两种情况
那么其p[i]:
1)可能就是其前面镜像i`的p[i`]的长度,例如T[12]的b则和T[10]的b是回文,则p[12]和p[10]的相等,
2)可能是R-i的长度,但是以T[i`]为中心的回文字符串可能不只在T[C]的回文字符串的范围内,例如
因此p[i]应为1)和2)中最小的情况,之后再以T[i]为中心重新扩充p[i]
2. 若T[i]不在以T[C]为中心的回文字符串内,则其p[i]为0
每次扩充完p[i]之后,以T[i]为中心的回文字符串的R可能就要超出以T[C]为中心的字符串的R了,因此要更新C和R,这样能保证算出i的镜像i`的位置是当前回文字符串中算出来的
if(i+p[i] > R){ C = i; R = i + p[i]; }
之后找出T中p[i]最大值maxVal以及最大值的位置index,这样能用index算出s中回文字符串的开始位置,即(index-maxVal)/2,例如上图是(11-9)/2 = 1;
因此s=”babcbabcbaccba”,即从s[1]的a开始,长度为9:“abcbabcba”
下面是完整代码,注意头加入’^’尾加入’$’作为开始和结束的标志,因此遍历T是是[1, n-1),而最后算回文字符串开始位置时要多-1,减去开始’^’的占位
class Solution { // 将s="aba"变为T="^#a#b#a#&",其中"^$"为字符串开始和结束符, // #的作用是不管s的长度是奇数还是偶数,使得以T[i]为中心的回文字符串都是奇数 // 其中T[i]可能是字母也可能是'#' public: string longestPalindrome(string s) { // 将s变为T string T = S2T(s); // 计算以T[i]为中心的最大回文字符串的长度p[i] const int n = T.size(); // C是最近一次算出的最长回文字符串的中心字符的位置 // R是最近一次算出的最长回文字符串的最右边的字符的位置 int p[n], C(0), R(0); // 1. 若前面算出的以T[C]为中心的回文字符串很长,当前T[i]属于其回文中心内,即R>i, // 那么其p[i]:(如上图) // 1)可能就是其前面镜像i`的p[i`]的长度 // 2)可能是R-i的长度,但是以T[i`]为中心的回文字符串可能不只在T[C]的回文字符串的范围内 // 因此p[i]应为1)和2)中最小的情况,之后再以T[i]为中心重新扩充p[i] // 2. 若T[i]不在以T[C]为中心的回文字符串内,则其p[i]为0 for(int i = 1; i < n-1; i ++){ int i_mirror = 2*C - i; // i` = C - (i-C) p[i] = R>i ? min(p[i_mirror], R-i) : 0; // 扩充p[i] while(T[i+1+p[i]] == T[i-1-p[i]]){ p[i]++; } // 若以T[i]为中心的字符串的范围走出了以T[C]为中心的字符串的范围,则更新C,R if(i+p[i] > R){ C = i; R = i + p[i]; } } // 算好p[i]之后找出最大的p[i]所在的i的位置index,则算出回文字符串在原s中的开始字符的位置 // 之后得出其最大回文子串 int maxVal(0), index(0); for(int i = 1; i < n-1; i ++){ if(maxVal < p[i]){ maxVal = p[i]; index = i; } } return s.substr((index-1-maxVal)/2, maxVal); } string S2T(const string &s){ if(s.size() == 0) return "^$"; string T = "^"; for(int i = 0; i != s.size(); i ++){ T += "#" + s.substr(i, 1); } T += "#$"; return T; } };