求已知字符串的最大回文字符串长度,并输出该字符串(不考虑数字)
目前这篇文章并不完善,不足之处颇多,博主目前也在学习中,会持续更新~
输入示例:
Madam,I'm Adam.
输出示例:
Madam,I'm Adam.
题目分析:
求回文字符串一类的问题
输入分析:
C语言的常用输入字符串函数scanf(),gets()都不适合本题,scanf()会有输入不了空格的麻烦,gets会出现缓存区溢出漏洞,已经被废弃(vs2012代替的是get_s(),但不具有可移植性),我们选择c标准库中的一个函数fgets()来解决输入问题。
fgets(buff, sizeof(buff), stdin);
字符串的预处理:
题目要求输出的字符串包括标点和大写,这使得问题变得麻烦。我们先给字符串处理一下,提取出我们要判断的字符,并保存标点和空格的位置。(这里我们只保存提取出的字符的位置,可以简化位置,方便输出)
回文问题:
接下来问题就变成处理回文的问题了。
在一串字符中判断回文最直接的方法就是从左往右遍历字符的每一个子串,保存回文字符串的起始和结束位置,再输出。时间复杂度是O(n^3)
然而这种方法的时间复杂度太大,我们可以利用回文的特性,不要从左往右遍历,从中间数向左右横扫,这样只用两个for循环就可以扫完,时间复杂度为O(n^2),然而,这种方法要考虑的是字符个数是奇数还是偶数的问题。
还有一种著名的专门针对回文的算法叫Manacher算法。这种算法完美的解决了时间复杂度和奇偶问题,通过插入无关字符统一奇偶,并且每个字符只用遍历一遍,因此复杂度是O(n)。
下面是c/c++的代码,每一种算法都写在一个函数里面了。
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define MAX_NUM 5000+10//注意如果出现MAX_NUM*x是5000+10*x
#define min(x, y) (x)<(y)?(x):(y)
char buf[MAX_NUM], s[MAX_NUM];
char p[MAX_NUM];
int traverse1(char*, int*, int*, int);
int traverse2(char*, int*, int*, int);
int manacher (char*, int, int *, int *);
int main()
{
int n, m=0, max=0;
int i, j, k;
int x, y;
fgets(buf, sizeof(s), stdin);
n = strlen(buf);
//提取字母并全部变为大写
for (i=0; i<n; i++){
if (isalpha( buf[i] ))
{
p[m] = i;//保存字母的位置
s[m++] = toupper(buf[i]);
}
}
// printf("max = %d\n", traverse1(s, &x, &y, m));
//printf("max = %d\n", traverse2(s, &x, &y, m));
printf("max = %d\n", manacher (s, m+1, &x, &y));
for (i=x; i<=y; i++)
{
printf("%c", buf[i]);
}
printf("\n");
return 0;
}
//暴力遍历算法
int traverse1(char* s, int *x, int *y, int m)
{
int max=0;
int i, j, k;
for (i=0; i<m; i++){
for (j=i; j<m; j++)
{
int ok = 1;
for (k=i; k<=j; k++){
if (s[k] != s[i+j-k]) ok=0;
}
if (ok && j-i+1>max)
{
*x = p[i];
*y = p[j];
max = j-i+1;//更新最大字符值
}
}
}
return max;
}
//从中间遍历
int traverse2(char *s, int *x, int *y, int m)
{
int max=0;
int i, j, k;
for (i=0; i<m; i++)
{
//奇数遍历
for (j=0; i-j>=0 && i+j<m; j++)
{
if (s[i-j] != s[i+j]) break;
if (j*2+1 > max)//更新最大字符值
{
max = j*2+1;
*x = p[i-j];
*y = p[i+j];
}
}
//偶数遍历
for (j=0; i-j>=0 && i+j+1<m; j++)
{
if (s[i-j] != s[i+j+1]) break;
if (j*2+2 > max)//更新最大字符值
{
max = j*2+2;
*x = p[i-j];
*y = p[i+j+1];
}
}
}
return max;
}
//更好的算法
//该算法插入是在原字符串的字符之间插入无关字符'#'来白字符统一成奇数个
//在开头插入'$'就不用特殊处理越界问题,然后从1开始遍历统计以每一个位置为中心的回
//文字符长度,最后比较出最大长度
//该算法的核心就是如何统计以字符位置为中心的字符长度,详细的算法解释请到这篇博客
//http://www.cnblogs.com/biyeymyhjob/archive/2012/10/04/2711527.html
int manacher (char* s, int len, int *x, int *y)
{
int nlen = 2 * len + 3;
char* str = new char[nlen];
int i = 0;
int max = 0;
str[0] = '$';
str[1] = '#';
for (; i < len; i++)
{
str[i * 2 + 2] = s[i];
str[i * 2 + 3] = '#';
}
str[nlen - 1] = 0;
int *p = new int[nlen];//用p数组来保存回文长度
for (i = 1; i < nlen; i++)
{
p[i] = 0;
}
int id = 0;//id最大回文子串中心的位置,max=p[id]+id也就是最大回文子串的边界
for (i = 1; i < nlen; i++)
{
if (max > i)
p[i] = min(p[2*id-i], p[id]+id-i);//2*id-i是i关于id点的对称点,因为已知2*id-i的字符半径,
//因为对称性,我们比较i位置的字符半径和它的大小,如果它的半径小于id的半径,
//说明它在id的回文字符串的里面,我们直接将p[2*id-i]赋给p[i],如果大于id的半径
//就要遍历id+p[id]之外的字符,如果相等还要加到p[id]里面
else
p[i] = 1;
while (str[i+p[i]] == str[i-p[i]])
p[i]++;
if (p[i]+i > max)
{
max = p[i] + i;
id = i;
}
}
//保存最大长度和回文字符中心
int mx = 0;
int pos = 0;
for (i = 1; i < nlen; i++)
{
if (mx < p[i] - 1){
mx = p[i] - 1;
pos = i;
}
}
//转化成原字符串的位置
pos = pos/2-1;
int rad = mx/2;
if (pos == 0)
{
*x = pos;
*y = pos;
}
else
{
if (mx%2 == 0)
*x = pos-rad+1;
else
{
*x = pos-rad;
}
*y = pos+rad;
}
return mx;
}
目前这篇文章并不完善,不足之处颇多,博主目前也在学习中,会持续更新~