这是Leetcode上的第5题,题目如下:
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
给定一个字符串,找到该字符串的最长连续回文子串。例如输入“babad”,输出结果为“bab”.
这道题有一个时间复杂度为 O(n) 的算法,被称为Manacher’s Algorithm,该算法的思路十分巧妙。
算法来源: http://articles.leetcode.com/longest-palindromic-substring-part-ii/
还参考了这篇博客:http://blog.csdn.net/hopeztm/article/details/7932245
原文的解释不太容易理解,所以我在这里解释这个算法,也帮助自己理解吸收。
算法思路其实就是遍历字符串扩展出以每个字符为中心的最长回文串。但是如果用一般的扩展方法是会达到 O(n2) 的时间复杂度的,而这个算法的核心就是利用回文串的性质来减少扩展用到的步数,从而优化到 O(n) 的时间复杂度。
首先给定一个字符串S = “abaaba”,我们在S的每个字符周围都加个’#’,构造出一个T = “# a # b # a # a # b # a #”
例如我们当前考虑以Ti为回文串中间的元素,如果要找到最长回文字串,我们要从当前的Ti扩展使得 Ti-d … Ti+d 组成最长回文字串. 这里 d 其实和 以Ti为中心的回文串长度是一样的
所以用一个数组P来存放以当前字符为中心的回文串长度(也就是上面的d),例如对于上面的字符串T,它对应的数组P如下:
T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0
我们发现由于加入了 ‘#’符号,当回文串长度为奇数时,回文中心为字符;当回文串长度为偶数时,回文中心为 ‘#’,这就免去了判断奇偶的麻烦。
除此之外还定义了以下参数:
C:当前扩展到的最 “远”回文串中心(这里的 “远”指的是当前遇到的所有回文串中右边界index最大)
L:以C为中心的最长回文串左边界
R:以C为中心的最长回文串右边界
i:当前处理的字符位置
i’:i关于C的对称点
Q[i]:以i为中心的最长回文串(该参数不会出现在代码中,只是为了便于解释算法)
P的构造过程是怎样的呢?这是算法的重点,我们先看一个例子,根据回文串的特性来理解这个过程。
上面的例子已经计算到第13个字符,i’是i的对称点,以C为对称轴。
由于L到R是以C为中心的最长回文串,所以C两侧的字符应该是对称的,而P[i’] = 1,也就是说,以T[i’]为中心的回文串Q[i’]的扩展度为1。那么,必然有 P[i] = P[i’] = 1,这很好理解,因为L到R的范围内的字符是关于C对称的,而Q[i’]的边界在这个范围内,所以与它对称的回文串Q[i]也必定在这个范围之内。
上面这种情况即 i’ - P[i’] > L(也就是i + P[i’] < R,一个意思,所以R不需要更新),这时候 P[i] 就等于 P[i’],然后进行下一步循环。
另外一种情况就是i’ - P[i’] < L,先上例子:
P[i’] = 7,因为Q[i’]超出了LR的范围,这时候P[i]就不一定等于P[i’]了,这时候我们只能保证Q[i]在LR范围内的部分是与Q[i’]对称的,即P[i] = R - i = 5,但它可能更长,所以就需要对P[i]进行扩展。
扩展的方法就是以i为中心,右边界为R,左边界为 i-(R-i),向两边扩展,比较两边界的字符是否相等,如果相等,则P[i]++。直到不相等时停止扩展。扩展结束后,令C = i,R = 扩展到的右边界,L = 扩展到的左边界。
这样一来就完成了P的一次更新,循环P的构造过程,最后再从P中找到最大的值,那个值所在的位置对应的就是最长回文串的中心。
值得注意的是,在循环之中还内嵌了一个循环,即上面第二种情况时的扩展,而这种扩展看起来时间复杂度是 O(n) ,其实并不是,因为扩展是以R作为边界开始的,而且每次扩展都会更新R的位置,所以全部的扩展的过程加在一起可以看成是R从0到n的遍历,复杂度是 O(n) ,所以分解到单独的一次扩展复杂度就是 O(1) 。
算法的整体时间复杂度就是i从0到n的遍历加上R从0到n的遍历, O(n)+O(n)=O(n)
以下是我根据该算法写出的Java代码:
public String longestPalindrome(String s) {
String T = "#";
for(int i = 0; i < s.length(); ++i){
T += s.charAt(i) + "#";
}
T = "^" + T + "$";
int n = T.length();
int[] P = new int[n];
int C = 0, L = 0, R = 0;
int maxCenter = 0;
int maxLen = 0;
for(int i = 1; i < n - 1; ++i){
int i_mirror = 2 * C - i;
//以i'为中心的回文串 达到/超出 了LR边界
if(i_mirror < 0 || i_mirror - P[i_mirror] <= L){
P[i] = (R > i) ? (R-i) : 0;
//对P扩展
while(T.charAt(i - 1 - P[i]) == T.charAt(i + 1 + P[i])){
++P[i];
}
R = i + P[i];
L = i - P[i];
C = i;
}
//以i'为中心的回文串未达到LR边界
else{
P[i] = P[i_mirror];
}
//保存最长回文串的信息,
if(P[i] > maxLen){
maxLen = P[i];
maxCenter = i;
}
}
int start = (maxCenter - 1 - maxLen) / 2;
return s.substring(start, start + maxLen);
}