文章目录
- 题目
- 1)例子演示
- 2)思路分析
- 3)Manacher 算法
- 4)代码展示
题目
如何求得某字符串 str 的最长回文子串的长度?
要求时间复杂度 O(N)
1)例子演示
什么是回文子串?
回文串
即该字符串从前往后读,从后往前读都是一样的,或者说该字符串是对称的,子串表示该字符串在 str 中是连续的一段字符,不能拼接而成
如上图的字符串 str,"cabbac"是一个回文串,字符串长度为偶数,关于一条虚轴对称,"abbacabba"也是一个回文串,字符串长度为奇数,关于字符 ‘c’ 对称
不难发现该字符串中有很多的回文子串,但是 "abbacabba"便是其中最长的回文子串,其长度 9 就是我们需要返回的结果
2)思路分析
不难想到,求回文子串,我们会让每个字符以其为中心,向两边扩散,如果字符两端的字符相等并且没有超过字符串长度限制就扩,条件不成立了就表示求出了以该字符为中心的回文子串
如上图所示,我们以字符 ‘b’ 为中心扩散,最后走到了字符串的最左侧,停止扩张,得到了回文子串 “cabac”
如果我们能够将所有字符为中心的回文子串找到,保持记录其中的长度最大值,是不是就能够得到最长回文子串了呢?
但是回文子串中还存在着字符数量为偶数的字符串,为偶数时,是以一条虚轴为对称中心,按照上面的做法就会错过偶数长度的回文子串
为解决这个问题,我们可以将特殊字符
插在所有字符间以及字符串两侧,排除奇偶长度字符串的问题,仍然按照上面的思路就能够解决问题
注:特殊字符是什么没有限制
,因为特殊字符相当于是一个虚轴的存在,辅助功能,不管算哪个位置的回文子串,特殊字符只可能和特殊字符进行比对,所以不会对原来的字符比对有影响,自然也不会影响到最后的结果
比如对于字符串 “abba”
得到的最长回文串的长度为 9,返回的结果就是 9/2 = 4,符合预期
在最差情况下,比如 “ccccccc”,这样的字符串每次在算回文子串时都会扩张到边界才停止,扩张代价 O(N),该字符串长度为 N 时,时间复杂度为 O(N*N)
3)Manacher 算法
该算法能够实现时间复杂度为 O(N),主要原理就在于使得每次扩张时掌握充分的已有信息,在此基础上进行有意义的扩张
在这里有四个比较重要的变量
变量 H:回文半径
变量 D:回文直径
变量 R:之前扩张的所有位置中所到达的最远的回文右边界(初始值为-1)
变量 C:取得更远的回文右边界时,回文中心点(初始值为-1)
比如 “abbac”
在下标 0 时,此时的回文串为 “#”,回文半径和直径都为1,最右回文边界 R 更新为 0,中心点 C 更新为 0
在下标 1 时,此时的回文串为 “#a#”,回文半径为2,直径为 3,R 更新为 2,C 随之更新为 1
在下标 2 时,此时的回文串为 “#”,回文半径和直径都为1,R、C 都没有更新
注:可以看得出来,R 和 C 是一起更新的
那么在了解了这些变量信息后,我们就可以根据不同的情况来分类,采取不同的求回文子串的措施
情况一:
当前遍历到的字符的位置在最右回文边界外部
,暴力扩张
就像上面的例子中,index = 0 时,此时 R = -1,字符的位置显而易见在 R 的外部,所以只能进行暴力扩张
情况二:
当前遍历到的字符的位置在最右回文边界及其内部
在第二种情况中又根据当前遍历到的字符的位置 i 关于中心点 C 对称的位置的字符 i’ 的回文区间分成三类
2.1 第一类:
i' 的回文区间在 L..R 内部
向上面图中就属于这一类,此时 i 位置的字符的回文半径一定和 i' 是一样的
为什么呢?
如上图所示,区域 ① 是以 i’ 位置的字符为中心的回文子串区域,X ≠ Y,否则回文子串的区域会包含 X 字符 和 Y 字符
因为 i 和 i’ 关于中心点 C 成对称关系,所以区域 ② 也一定是回文子串,并且和区域 ① 呈逆序关系, X = Q,Y = P
所以得出结论 P ≠ Q,区域 ② 就是以 i 位置的字符为中心的回文子串区域
2.2 第二类:
i' 的回文区域有一部分在 L..R 外面
注:方便起见,去掉了特殊字符
此时以 i 位置的字符为中心的回文子串的回文半径就是 i 到 R 这段距离
这是什么原理呢?
如上图所示,区域 ① 是以 i’ 位置的字符为中心的回文子串区域中的一部分,X = Y
因为 i 和 i’ 关于中心点 C 成对称关系,所以区域 ② 也一定是回文子串,并且和区域 ① 呈逆序关系,Y = P
由于以 C 为中心点的回文子串区域为 L…R,所以 X ≠ Q
综上所述,X = P,P ≠ Q,区域 ② 就是以 i 位置的字符为中心的回文子串区域
2.3 第三类:
i' 的回文区域边界和 L 重合了
此时 i 的回文半径至少是 i 到 R
,至于会不会更长,就需要在已确定的回文串长度基础上向外比对,向外扩
像上面的这种情况,下标 5 ~ 7 的字符串就不需要进行比对了,一定是回文串,然后在比对 4 位置和 8 位置的字符,直到扩张失败
如上图所示,区域 ① 是以 i’ 位置的字符为中心的回文子串区域,左边界刚好和 L 重合
因为 i 和 i’ 关于中心点 C 成对称关系,所以区域 ② 也一定是回文子串,并且和区域 ① 呈逆序关系
但是以 i 为中心的回文子串是否会变得更长,还要继续比对 X 字符 和 Y 字符,相等就外扩,不等回文子串就是区域 ②
4)代码展示
//转换方法 "abba"-->{'#','a','#','b','#','b','#','a','#'}
public char[] Transform(String str) {
char[] chs = str.toCharArray();
char[] newChs = new char[chs.length*2+1];
newChs[0] = '#';
int index = 0;
int i = 1;
while (i < newChs.length) {
newChs[i++] = chs[index++];
newChs[i++] = '#';
}
return newChs;
}
public int Manacher (String str) {
char[] chs = Transform(str);//转换
int[] radius = new int[chs.length];//保存每个字符的回文半径
int R = 0;
//为了方便,这里的 R 为最右回文边界的后一个字符下标
//这样 R-i 就刚好是情况二中的第二类的 i 的回文半径
int C = -1;
int max = 1;
for (int i = 0; i < chs.length; i++) {
//回文半径至少的长度
// R > i 成立了,就是情况一,回文半径至少是1
// R > i 不成立,那就是情况二,radius[2*C-i]就是 i' 的回文半径长度,和 R-i 取较小
radius[i] = R > i ? Math.min(radius[2*C-i],R-i):1;
//虽然只有情况一和情况二的第三类需要往外扩,但是为了方便,不管哪种情况都扩
//反正情况二的第一二类情况外扩也会失败的,不影响结果
//循环进入条件就是左右不可越界
while (i + radius[i] < chs.length && i - radius[i] > -1) {
//配对成功,外扩,即回文半径加一
if (chs[i + radius[i]] == chs[i - radius[i]]) {
radius[i] ++;
}else {
//外扩失败
break;
}
}
//更新 R 和 C
if (R < i + radius[i]) {
R = i + radius[i];
C = i;
}
//保持最大值
max = Math.max(max,radius[i]);
}
//因为有特殊字符的存在,最后的实际最长回文子串的长度是 max-1
//比如 "#a#b#b#a#",max = 5,返回值为 4
return max - 1;
}