马拉车算法,Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,是一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性O(N)。
首先我们在求一个字符串的回文子串时,会有一个问题,长度为奇数的和长度为偶数的字符串求法不一。
假如给你一个字符串,一步一步找最长回文子串,你会怎么找?
青铜级选手:列出所有的子串,然后判断是否是回文子串,如果是,则计算子串长度,并跟当前的最长回文子串进行比较,如果比之长,则更新。
首先我们需要一层循环,确定长度,再需要一层循环,取遍当前确定长度的所有子串,还需要一层循环确定是否是回文字符串。——O(N3)
那么C语言写出的代码是这样的:
char * longestPalindrome(char * s)
{
int len_s = strlen(s);
int max_len = 0;
int left_str = 0;
int right_str = -1;
for(int len = 0; len < len_s; len++)
{
int left = 0;
int right = len;
while(right < len_s)
{
//判断是否是回文子字符串
int begin = left;
int end = right;
int is_true = 1;
while(begin<=end)
{
if(s[begin]!=s[end])
{
is_true = 0;
break;
}
begin++;
end--;
}
if(is_true)
{
left_str = left;
right_str = right;
break;
}
left++;
right++;
}
}
//开辟数组存储子串
char * str = (char*)malloc(sizeof(char)*(len_s+1));
memset(str,0,sizeof(char)*(len_s+1));
for(int left = left_str,i = 0; left <= right_str; left++,i++)
{
str[i] = s[left];
}
return str;
}
黄金级选手:如果是回文子串,那么其中心下标,向两边进行扩展,不还是回文子串吗?这里需要注意的是偶数的中心下标有两种情况,那我们就假设字符串的每一个元素都是中心下标,遍历,然后需要注意第一步确定边界,往两边延伸一次可能是回文,往前面延伸一次也可能是回文。因此我们都要试一次,确定一个边界,再进行两边延伸。总结一下:中心下标一次循环,确定起始边界(两种可能性),然后向两边走,不嵌套的两次循环,因此时间复杂度为——O(N2)。
图解:
因此根据这种思路写出的C代码是这样的:
char * longestPalindrome(char * s)
{
int ce_sut = 0;//center_subscript
int len = strlen(s);
int left_str = 0;
int right_str = 1;
int max_len = 0;
for(ce_sut = 0; ce_sut < len; ce_sut++)
{
//确定起始边界
if(s[ce_sut] == s[ce_sut+1])
{
int begin = ce_sut;
int end = ce_sut + 1;
while(begin>=0&&end<len\
&&s[begin] == s[end])
{
begin--;
end++;
}
//越界或者不是回文字符串
begin++;
end --;
int len_cur = end - begin + 1;
if(len_cur>max_len)
{
max_len = len_cur;
left_str = begin;
right_str = end+1;
}
}
if(ce_sut-1>=0&&ce_sut+1<len&&\
s[ce_sut-1]== s[ce_sut + 1])
{
//确定起始边界
int begin = ce_sut-1;
int end = ce_sut + 1;
while(begin>=0&&end<len\
&&s[begin] == s[end])
{
begin--;
end++;
}
//越界或者不是回文字符串
begin++;
end --;
int len_cur = end - begin + 1;
if(len_cur>max_len)
{
max_len = len_cur;
left_str = begin;
right_str = end+1;
}
}
}
//开辟空间拷贝字符串
char * str = (char*)malloc(sizeof(char)*(len+1));
memset(str,0,sizeof(char)*(len+1));
for(int left = left_str,i = 0; left < right_str; left++,i++)
{
str[i] = s[left];
}
return str;
}
大师级选手:利用manacher——马拉车算法 + 小技巧。
第一步:求当前的中心坐标的长度,并保存此长度
第二步: 计算范围[Left,Right]。
第三步:从中心下标+1开始,到Right-1结束,判断当前坐标的对称坐标回文长度,是否等于当前坐标的回文长度——看对称坐标范围是否到边界,如果等于,就继续走,如果不等于就更新中心下标到当前坐标。
第四步:计算出处理后最大回文子串的长度,由处理前的最大回文子串的长度/2即可。因为处理后的最大回文长度必定是奇数,我们举a,处理之后为#a#,处理后的最大回文子串长度为3,/2等于1,再举一个例子,#a#a#,处理之后最大回文子串的长度为5,/2等于2,就等于原来的回文的字串的长度,要说为什么因为是/2,是相0取整的!
因此根据这种思路写出的C代码是这样的。
char * longestPalindrome(char * s)
{
//处理字符串
//开头和结尾需要加上都不同的两个字符,处理越界,这里我加上$和!
//中间隔开的字符我们用#——字符串的长度+1
//不要忘了还要为\0开辟空间
//带上原来的字符串的长度
//总计:n + n + 1 + 2 + 1 == 2*n + 4
int len_s = strlen(s);
char* str = (char*)malloc(sizeof(char)*(2*len_s+4));
memset(str,0,sizeof(char)*(2*len_s+4));
//开头的下标为:0
//结尾的下标为:2*len_s + 2
str[0] = '$';
str[2*len_s + 2] = '!';
printf("%c",str[0]);
//奇数放'#'
//偶数放s的字符
for(int i = 0;i < len_s; i++)
{
str[2*i + 1] = '#';
str[2*i + 2] = s[i];
printf("%c%c",str[2*i+1],str[2*i+2]);
}
//a这里只会处理成$#a!倒数第二个字符没有处理因此我们需要还要加上
str[2*len_s + 1] = '#';
printf("%c%c\n",str[2*len_s + 1],str[2*len_s + 2]);
//manacher思路
//记录中心下标回文字符串长度的数组
int * infor = (int*)malloc(sizeof(int)*(2*len_s+2));
memset(infor,0,sizeof(int)*(2*len_s+2));
//记录最长回文子符串的长度
int max_len = 0;
//记录最长回文字符串的左右下标
int left_str = 0;
int right_str = 1;
for(int mid = 1; mid < 2*len_s + 2;)
{
//先让中心下标往两边进行扩
int left = mid;
int right = mid;
int count = 0;
while(str[left]==str[right])
{
left--;
right++;
count ++;
}
//这里不是回文字符串,但是我们往前再走一步就是回文字符串
left++;
right--;
//我们需要保存一下最大回文半径的值
infor[mid] = count;
//优化
if(right == 2*len_s + 1)
{
break;
}
//这里我们需要判断一下回文字符串的长度大于不大于当前最大的回文字符串的长度
int len_cur = right - left + 1;//这里是左闭右闭
if(len_cur > max_len)
{
max_len = len_cur;
left_str = left;
right_str = right+1;//左闭右开
}
//如果字符串的长度大于3就可能会有
if(mid+1<right)
{
int k = mid + 1;
while( k < right)
{
//优化
//可能的最大半径
int may_len = 2*len_s + 1 - k + 1;
//当前的最大半径
int cur_r = right - mid + 1;;
if(may_len < cur_r)
{
no_need = 1;
break;
}
//有两种情况
//我们需要计算出k的对称下标的左边界与当前中心下标的左边界进行比较。
int n = 2*mid - k;//求出中心下标对应的对称坐标
int r = infor[n];//最大回文子串的半径
int left_n = n - r + 1;//对称下标的左边界
//如果对称下标的左边界小于等于中心下标的左边界,就不一定会有
if(left_n <= left)
{
mid = k;
break;
}
else
{
infor[k] = infor[n];
}
k++;
}
}
else
{
mid++;
}
}
//储存字符串
char* str1 = (char*)malloc(sizeof(char)*(len_s+1));
memset(str1,0,sizeof(char)*(len_s+1));
int i = 0;
for(int left = left_str; left < right_str; left++)
{
if(str[left]!='#')
{
str1[i++] = str[left];
}
}
return str1;
}