新年之际,给大家带来了一道题目并且解决方法。希望大家喜欢
题目:
给定字符串,找到它的最长回文子串,都有哪些思路呢?例如"adaiziguizhongrenenrgnohziugiziadb",回文字串很多了,但最长的是"daiziguizhongrenenrgnohziugiziad"。
下面简单的介绍5种解决方法:
方法一
暴力法,外面的两层循环找到所有子串,第三层循环判断子串是否是回文。
function makeOdd(str){
var len = str.length;
if(len % 2 === 1){
return str;
}
var newStr = '#';
for(i = 0;i<len;i++){
newStr += str[i]+'#';
}
return newStr;
}
function judge(str){
(str.length%2 === 0) && (str = makeOdd(str));
var len = str.length,
half = Math.floor(len/2),
last = len-half;
var i = 0;
while(i<=last){
if(str[half-i] !== str[half+i]){
return false;
}
i++;
}
return true;
}
function getAllSubs(str){
var len = str.length,
res = [];
for(var i = 0;i<len;i++){
for(var j = i;j<len;j++){
var sub = str.substring(i,j+1);
console.error(sub);
if(sub && judge(sub)){
res[res.length] = sub;
}
}
}
return res;
}
console.log(getAllSubs('aaaabaac'));
方法二:
动态规划的方法。开辟一个P[i][j]用来表示str[i..j]是否为回文,P[i][j]的状态转移方程如下:
当i==j时,P[i][j]=true
当i+1==j时,P[i][j]=str[i]==str[j]
其他,P[i][j]=P[i+1][j-1]&&(str[i]==str[j])
// 获取所有的子串回文情况
function getP2(str){
var len = str.length,
gap = '_';
var p = {};
var i,j,L;
// 只有一个字符的情况是回文
for( i =0;i<len;i++){
p[i+gap+i] = true;
}
// L是i和j之间的间隔数(因为间隔数从小到大渐增,所以大的间隔数总能包含小的间隔数)
for(L=2;L<=len;L++){
// 从0开始
for(i=0;i<=len-L;i++){
j = i+L-1;
if(L === 2){
p[i+gap+j] = (str[i] === str[j]);
}else{
p[i+gap+j] = (str[i]===str[j])&&p[i+1+gap+(j-1)];
}
}
}
re
方法三:
可以从上面那个方法的状态转移方程获得启发,对于每一个回文子串可以先确定一个中心,然后向两边扩展,这样可以在时间复杂度O(n^2),空间复杂度O(1)的情况下完成,需要注意的是,长度为奇数和偶数的中心的情况是不同的。
function getP3(str){
var maxLength = 1,
start = 0,
len = str.length,
low,high;
for(var i=1;i<len;++i){
low = i-1;
high = i;
while(low>=0 && high < len && str[low] === str[high]){
if(high-low+1>maxLength){
start = low;
maxLength = high-low+1;
}
--low;
++high;
}
}
low = i-1;
high = i+1;
while(low>=0 && high < len && str[low] === str[high]){
if(high-low+1>maxLength){
start = low;
maxLength = high-low+1;
}
--low;
++high;
}
return maxLength;
}
方法四:
第四个方法采用后缀数组,将最长回文子串的问题转化为最长公共前缀的问题。具体的做法就是:将整个字符串翻转之后,拼接到原字符串后,注意用特殊字符分开,这样问题就变成了新的字符串的某两个后缀的最长公共前缀的问题了。这个方法比较强大,很多字符串的问题都能够巧妙的解决。不过实现起来也相对比较难,好的实现和差的实现时间复杂度相差很大。
后缀数组是一种对字符串处理的比较有力的实现方案,可以用于求解最大公共子串、最长回文串等问题。但是其实现思路还是比较难以理解的。基本的思想就是获取字符串的后缀字符串,然后获得后缀字符串的最长公共前缀。其中牵扯到一系列的表示(如后缀数组、排序数组等),对于如何求得这些表示又有不同的对应方案。具体的论文可参考:后缀数组——处理字符串的有力工具
方法五:
第五个方法叫做Manacher算法,是一种线性时间的方法,非常巧妙。引入一个技巧,可以使得奇数和偶数的情况统一处理。具体做法如下:abba转换为#a#b#b#a#,也就是在每一个字符两边都加上一个特殊字符,这样,不管是奇数长度的字符串还是偶数长度的字符串就都能转化为奇数长度的字符串了。
然后创建一个新的P[i]表示,以第i个字符为中心的回文字串的半径。例如上面的例子,对应的P如下,设S为原始字符串:
通过观察上面的表,大家可以发现P[i]-1就是实际回文字串的长度。如果知道P,遍历一次就知道最长的回文子串。可以该如何计算P呢?这是这个算法最核心的部分。
具体可见下面的博客: Manacher's ALGORITHM: O(n)时间求字符串的最长回文子串
function getP5(str){
var p = [],
mx = 0,
id = 0;
var i;
// 将字符串转化为奇数长度获取到新的字符串
var newStr = '#';
var len = str.length;
for(i = 0;i<len;i++){
newStr += str[i]+'#';
}
var newLen = newStr.length;
for(i = 0;i<newLen;i++){
p[i] = 0;
}
// 时间复杂度为O(n),空间复杂度为O(1)获取到所有的子回文的长度值组成的数组
for (i = 0;i < newLen; i++) {
// mx表示当前边界值最大的回文子串的边界值
p[i] = mx > i ? Math.min(p[2*id-i], mx-i) : 1;
// 超出其半径的位置再做额外判断
while ((newStr[i + p[i]] == newStr[i - p[i]]) && newStr[i + p[i]]){
p[i]++;
}
// 获取到边界最大的回文子串的中心位置以及边界值,以保证后续迭代可以做以上快捷处理
if (i + p[i] > mx) {
id = i;
mx = id + p[i];
}
}
return p;
}
通过代码实现后,发现上面的博客对于其中的原理的分析并不十分准确。最基本的原则就是2个:
1、如果一个字符串(统一转化为了奇数长度)是回文串,中间字符位置为m。如果其子串S[i,i+x]是回文串,那么其相对于中间字符的对称子串S[2m-i,2m-(i+x)]也一定是回文串;
2、回文串一定是基于中心字符串对称的。也就是:S[i] == S[2m-i]。
然后分析下核心代码:
p[i] = mx > i ? Math.min(p[2*id-i], mx-i) : 1;
// 超出其半径的位置再做额外判断
while ((newStr[i + p[i]] == newStr[i - p[i]]) && newStr[i + p[i]]){
p[i]++;
}
// 获取到边界最大的回文子串的中心位置以及边界值,以保证后续迭代可以做以上快捷处理
if (i + p[i] > mx) {
id = i;
mx = id + p[i];
}
分析后得知,mx并不是像博文中说的表示最大回文串的边界值,mx表示当前边界值最大的回文子串的边界值。由于超出边界值的部分无从判断是否还能与原串组成回文串,所以要额外判断。