览
1.问题背景
给出一个字符串,找到这个字符串中,最长的回文子串
例如字符串:S = “aacabdkacaa”
最长的回文子串是:“aca”
其中1<=S.length <=1000
S仅由字母数字组成
2.中间扩散算法
2.1.思路:
把字符串中每一个字符当作一个中心点,然后同时向两边搜索,但是会有一个问题,当这个字符串的个数可能是一个奇数也可以是一个偶数的情况,所以需要做对奇数个数 字符串与偶数个数字符串分别处理
奇数:从自己的位置开始向两边搜索, i 与 i 判断,i - 1与i + 1判断,i - 2 与 i + 2 判断…
偶数:从自己的位置与i+1位置开始向两边搜索, i 与 i+1 判断,i - 1 与 i + 2 判断,i - 2 与 i + 3判断
最后,将每一个字符向两边扩散得到的开始位置与终止位置,终止位置 减 起始位置就是这个字符向两边扩散的长度,每一次向两边扩散会得到这个一个长度,当下一次得到的这个长度比上一次得到的这个长度要长,则更新记录长度的变量
2.2代码解析
2.2.1 两边搜索
//将求得的每一个字符为中心的最长回文串
void expandAroundCenter(char* s, int left, int right, int* l, int* r) {
while(left >= 0 && right < strlen(s) && s[left] == s[right]) { left--; right++; }
*l = left + 1; *r = right - 1; //l指向子回文串的第一个字母位置 r指向子回文串最后一个字母位置
}
这个函数有5个参数,第一个参数S, 传递过来的原字符串,第二个参数left,往左遍历的起始位置,第三个参数right,往右遍历的起始位置,第三第四参数,用来保存传递回去的最终左边和右边停下来的位置。
left是不断往左走,right是不断往右走,所以需要对left和right进行合法的判断,是不是在遍历的范围内,然后左边那个元素与右边那个元素进行判断,是否相同,如果相同则,往左走的继续往左一位,往右走的继续往右走一位,不断的这样循环判断,最终以当前字符为中心,left指向的左边的下标,right指向的右边的下标,通过两个指针传递回去
2.2.2 核心代码
char* longestPalindrome(char* s) {
int left1, right1;
int left2, right2;
int start = 0, end = 0;
for (int i = 0; s[i]; i++) {
//中间扩散法分别求奇数串和偶数串的每一个字符最长子回文串
expandAroundCenter(s, i, i, &left1, &right1); //奇数
expandAroundCenter(s, i, i + 1, &left2, &right2);//偶数
if (end - start < right1 - left1) {
end = right1;
start = left1;
}
if (end - start < right2 - left2) {
end = right2;
start = left2;
}
}
return newstr(s, start, end);
}
其中以下这段代码的意思:如果当前right1与left1的区间长度 大于 end与start的区间长度,则更新新的区间长度
if (end - start < right1 - left1) {
end = right1;
start = left1;
}
3.中间扩散法整体代码
/*
LeetCode 5.
输入:s = "babad"
输出:"bab"
解释:"aba"
输入:s = "cbbd"
输出:"bb"
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//截取指定区间的串返回
char* newstr(char* s, int start, int end) {
char* str = (char*)malloc(sizeof(char) * 1000 + 1);
int k = 0;
for (int i = start; i <= end; i++) {
str[k++] = s[i];
}
str[k] = '\0';
return str;
}
//将求得的每一个字符为中心的最长回文串
void expandAroundCenter(char* s, int left, int right, int* l, int* r) {
while(left >= 0 && right < strlen(s) && s[left] == s[right]) { left--; right++; }
*l = left + 1; *r = right - 1; //l指向子回文串的第一个字母位置 r指向子回文串最后一个字母位置
}
char* longestPalindrome(char* s) {
int left1, right1;
int left2, right2;
int start = 0, end = 0;
for (int i = 0; s[i]; i++) {
//中间扩散法分别求奇数串和偶数串的每一个字符最长子回文串
expandAroundCenter(s, i, i, &left1, &right1); //奇数
expandAroundCenter(s, i, i + 1, &left2, &right2);//偶数
if (end - start < right1 - left1) {
end = right1;
start = left1;
}
if (end - start < right2 - left2) {
end = right2;
start = left2;
}
}
return newstr(s, start, end);
}
int main() {
char s[] = "cbbd";
printf("字符串:%s \n最长回文子串为:%s", s, longestPalindrome(s));
return 0;
}
3.1 运行结果
4.Manacher算法
Manachar算法其实名叫马拉车算法,这个算法是在原本的中间扩散法进行优化的一个算法,而中间扩散算法平均的时间复杂度在O(n²),而马拉车算法优化后的时间复杂度是在O(n),马拉车算法需要理解几个东西。
1.在一段区间内求 i 的对称点怎么计算
2.回文半径是什么
3.d[i]数组是做什么的
5.Manacher算法基本思路
在区间[L,R],要进行分类讨论
1.当 i 在[L,R]区间内,那么 i 的对称点 i1 是R - i + L
1.1 如果d[R - i + L ] < R - i + 1,则d[i] = d[R - i + L]
1.2 如果d[R - i + L ] >= R - i + 1,则d[i] = R - i + 1,然后再继续暴力枚举
2.如果 i 不在[L,R]区间内, 那么直接进行暴力枚举
3.如果 i + d[i] - 1 > R
根据已经求出的d[i]判断是否需要更新新的区间 ,L = i - d[i] + 1, R = i + d[i] - 1
5.1 i 的对称点
例如在这么一个区间内 1 2 3 4 5 6 7 8
L是2 R是8
当i是6,则求 i 的对称点
R - i: 8 - 6得到2,意思就是从i到R的距离就是2,表示偏移量
那么从L + 这个偏移量就是 i 的对称点, i1 = R - i + L = 8 - 6 + 2 = 4
i 的对称点: R - i + L
5.2 回文半径
对于一个回文的字符串:abcba,它的回文半径是3,abc或者cba
回文半径:回文字符串的长度 / 2
也可以算成3,也可以算成2,算成3是加上自己就是3,如果不算自己就是2
5.3 d[i]数组
例如有这么一个字符串: S = “abcdcba”,假设下标从1开始,S[ 1 ] = ‘a’, S[ 1 ] = ‘b’
d[ 1 ] = 1, d[ 2 ] = 1, d[ 3 ] = 1
d[4] = 4
这个d[ i ]数组的意思是:记录以当前 i 位置的字符作为回文串的中点,统计当前回文串的半径
S[ 4 ]是d,那么半径是 abcd 和 dcba是4,而其他字母假设回文中心点,只有自己,所以就是1
d[ i ]用来记录当前 i 位置字符为回文中点,记录当前 i 的回文半径
5.4 理解基本思路
在区间[L,R],要进行分类讨论
1.当 i 在[L,R]区间内,那么 i 的对称点 i1 是R - i + L
1.1 如果d[R - i + L ] < R - i + 1,则d[i] = d[R - i + L]
这句话的意思:当如果 i 的半径长度 小于 i 到 R 的偏移量,则d[ i ]的值直接取 i 的对称点,就是半径长度 d[ i1 ], 当 i + 半径小于R,直接取对称点,因为 i 就是 i1
1.2 如果d[R - i + L ] >= R - i + 1,则d[i] = R - i + 1,然后再继续暴力枚举
如果i + 半径大于R或者等于R, 则取i到R的距离作为d[ i ]的值,然后继续暴力
2.如果 i 不在[L,R]区间内, 那么直接进行暴力枚举
3.如果 i + d[i] - 1 > R
根据已经求出的d[i]判断是否需要更新新的区间 ,L = i - d[i] + 1, R = i + d[i] - 1
6.核心代码
//马拉车算法求解d数组
void Manacher(char* s, int n, int d[]) {
int L, R;
d[1] = 1;
for (int i = 2, L, R = 1; i <= n; i++) {
if (i <= R) d[i] = min(d[R - i + L], R - i + 1);
while(s[i - d[i]] == s[i + d[i]]) d[i]++;
if (i + d[i] - 1 > R) {
L = i - d[i] + 1;
R = i + d[i] - 1;
}
}
for(int i = 1; i<= n; i++) {
printf("%3d", d[i]);
}
}
需要注意的是,需要对原字符串做一个预处理,头和尾都需要插入一个特殊字符,两个字符中间也要插入特殊字符
原字符串 a b c b a
处理后 # a # b # c # b # a
这样处理的好处是,不管原字符串是奇数个数还是偶数个数,最终都会是奇数个数字符串
7 代码
/*
LeetCode 5.
输入:s = "babad"
输出:"bab"
解释:"aba"
输入:s = "cbbd"
输出:"bb"
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//将原字符串插入特殊符号#,保证原字符串的字符个数不管是奇数还是偶数最终字符个数是奇数
char* newString(char* s) {
int index = 1;
char* newstr = (char*)malloc(sizeof(char) * strlen(s) * 2 + 3);
newstr[index++] = '#';
for (int i = 0; s[i]; i++) {
newstr[index++] = s[i];
newstr[index++] = '#';
}
newstr[index] = '\0';
return newstr;
}
int min (int a, int b) {
if (a < b) return a;
return b;
}
//马拉车算法求解d数组
void Manacher(char* s, int n, int d[]) {
int L, R;
d[1] = 1;
for (int i = 2, L, R = 1; i <= n; i++) {
if (i <= R) d[i] = min(d[R - i + L], R - i + 1);
while(s[i - d[i]] == s[i + d[i]]) d[i]++;
if (i + d[i] - 1 > R) {
L = i - d[i] + 1;
R = i + d[i] - 1;
}
}
for(int i = 1; i<= n; i++) {
printf("%3d", d[i]);
}
}
//返回最大值下标
int maxlue_dex(int d[], int n) {
int index_max = 1;
for (int i = 1; i <= n; i++) {
if (d[index_max] <= d[i]) index_max = i;
}
return index_max;
}
//拷贝指定区间上面的字符串,返回一个新的字符串
char* cpy_newstr(char* str, int dex, int d[]) {
int section = strlen(str) * 2 - 1;
int index = 0;
char* newstr = (char*)malloc(sizeof(char) * section); //开辟一个回文子串长度的空间
for (int i = dex - d[dex] + 1; i<= dex + d[dex] - 1; i++) {
if (str[i] != '#') newstr[index++] = str[i];
}
newstr[index] = '\0';
return newstr;
}
char* longestPalindrome(char* s) {
int d[2002] = { 0 };
char* newstr = newString(s);
Manacher(newstr, strlen(s) * 2 + 1, d);
//获取以当前i位置向两边扩散的最长的回文串下标
int dex = maxlue_dex(d, strlen(s) * 2 + 1);
return cpy_newstr(newstr, dex, d);
}
int main() {
char s[] = "aacabdkacaa";
printf("字符串:%s \n最长回文子串为:%s", s, longestPalindrome(s));
return 0;
}
运行结果: