(1)思路
1. 因为字符串长度为奇数和偶数时处理方式不同,所以这里可以使用插入特殊字符的方法,使得每个字符串在处理之后长度都为奇数。处理方法为:在任意两个字符之间和字符串的开始和结尾插入'#'。(因为设字符为n个,那么共有n-1个间隙,n+(n-1)必然为奇数,再加上首尾'#'还是奇数)。因为要防止越界,所以在修改后的字符串首尾再加上边界,开始为'$',结尾为'@'
private String chuli(String str)
{
StringBuffer sb = new StringBuffer();
sb.append('$'); //加入开始标识
for(int i=0; i<str.length(); i++)
{
sb.append('#'); //每个字符之前加入#
sb.append(str.charAt(i));
}
sb.append('#'); //末尾加入#
sb.append('@'); //加入结束符
return sb.toString();
}
测试:
String str = "abba";
str = chuli(str);
for(int i=0; i<str.length(); i++)
System.out.print(str.charAt(i)+" ");
结果为:
$ # a # b # b # a # @
2. 核心算法
1>定义数组p[i]:表示以str[i]为中心的最大回文串的长度(包含字符str[i]),pi表示已经求得的拥有最长回文串的字符的下标,mx表示最长回文串所能达到的最右边的边界
2>
pi是最长回文串的中心(淡蓝色),如果以j为中心的最大回文串如上图所示,那么处于i处的情况和j处相同(因为pi的两侧是对称的)。这样求i的时候就可以利用j的结果了,此处显然j = 2*pi-i
mx是最长回文串的右边界,因为i<mx所以在求p[i],即以i为中心的回文串时,就可以利用关于pi的对称点j的信息。因为图中蓝色标志长度相同,均为mx-i,所以当mx-i>p[j],即图中所示情况时,p[i] = p[j],其余能不能再延长,就只能一个个字符判断了。
3>
这种情况虽然mx>i,但是mx-i<p[j],也就是说最长回文串并没有全部包含以j为中心的回文串,所以此处最长只能选择mx-i为半径,即p[i] = mx-i
4>最后一种情况:mx-i<0,即i在最长回文串的外面,所以此时就只能从头开始算了,即p[i] = 1
(2)代码
@Test
public void huiwen()
{
String str = "abba";
str = chuli(str);
int mx = 0;
int pi = 1;
int[] p = new int[str.length()];
for(int i=1; i<str.length(); i++) //因为已经将字符串第0个字符设置为$,所以直接从第1个开始
{
if(mx>i) //如果i在mx内,即可以利用以前求得的结果
{
if(mx-i>p[pi*2-i]) //整个i的对称点j = pi*2-i,都在最长回文串中
p[i] = p[pi*2-i];
else //一部分在最长回文串外
p[i] = mx-i;
}
else //因为i在mx外,所以不能利用之前求得的结果,那么就直接设为1
p[i] = 1;
while(i-p[i]>0 && i+p[i]<str.length() && str.charAt(i+p[i])==str.charAt(i-p[i])) //设置完p[i]的初始值之后,再一次扩展两边,看看是 //否能够将回文串扩大
p[i]++;
if(mx<i+p[i]) //如果当前求得的回文串长度大于之前最大的,那么更新最大值
{
mx = i+p[i]-1; //i+p[i]-1为右边界
pi = i;
}
}
int maxLen = p[0];
for(int i=1; i<p.length; i++) //选出其中最长的半径
maxLen = Math.max(maxLen, p[i]);
System.out.println(maxLen-1); //maxLen-1就是原字符串的最大回文串长度,下面证明
}
(3)注意
1.因为p[i]记录的是插入分隔符之后的回文串的半径,所以以i为中心的回文串的长度为2*p[i]-1.如bb->$#b#b#@,中间#的半径为3,所以回文串长度为2*3-1 = 5
2.因为#b#b#长度-1后,就正好是原来字符串长度的2倍,即((2*p[i]-1)-1)/2 = p[i]-1,得证。