小白3.3最长回文子串

例3-4 回文串

  输入一个字符串,求出其中最长的回文子串。子串的含义是:在原串中连续出现的字符串片段。回文的含义是:正着看和倒着看相同,如abba和yyxyy。

  在判断是,应该忽略所有的标点符号和空格,且忽略大小写,但输出保持原样(在回文串中的首部和尾部不要输出多余字符)。输入字符串长度不超过5000,且占据单独的一行。应该输出最长的回文串,如果有多个,输出起始位置最靠左的。

    样例输入:Confuciuss say:Madam,I'm Adam.

    样例输出: Madam,I'm Adam.

分析

  如果输入全部都是大写字母,问题就简单了:直接判断每个子串即可。可其他字符的出现吧问题搞得复杂起来。首先,我们不能用scanf("%s")输入字符串,因为他碰到空格或者TAB就会停下来。可以用下述两种方法结局这个问题:

   第一种方法是使用fgetc(fin),它读取一个打开的文件fin,读取一个字符,然后返回一个int值。为什么返回的是int而不是char呢?因为如果文件结束,fgetc将返回一个特殊标记EOF,它并不是一个char。如果把fgetc(fin)的返回值强制转换为char,将无法把特殊的EOF和普通字符区分开。如果要从标准输入读取一个字符,可以用getchar(),他等价于fgetc(stdin)。

提示 3-14:使用fgetc(fin)可以从打开的文件fin中读取一个字符。一般情况下应当在检查它不是EOF后再将其转换成char值。从标准输入读取一个字符可以用getchar(),它等价于fgetc(stdin)。

  fgetc和getchar()将读取“下一个字符”,因此你需要知道在各种情况下,“下一个字符”是哪个,如果用scanf(“%d”,&n)读取整数n,则要是在输入123后多加了一个空格,用getchar()读取的将是这个空格;如歌在“123”之后紧跟着换行,则读取到的将是回车符‘\n’。

  这里有个潜在的陷阱:不同操作系统的回车换行符是不一致的。Windows是‘\r’和'\n'两个字符,Linux是‘\n’,而MacOS是‘\r’。如果在Windows下读取Windows文件,fgetc()和getchar()会把‘\r’“吃掉”,只剩下‘\n’;但是如果在Linux修啊读取同样的一个文件,它们会忠实地先读取‘\r’,然后才是'\n'。如果编程不注意,你的程序可能会在摸个操作系统上是完美的,但在另一个操作系统上就错的一塌糊涂。当然,比赛的组织应该避免在Linux下使用Windows格式的文件,但是选手也应该把子的程序写的更鲁棒,即容错性更好。

提示3-15: 在使用fgetc和getchar时,应该避免写出和操作系统相关的程序。

  第二种方法是使用fgets(buf,MAXN,fin)读取完整的一行,其中buf的声明为char buf[MAXN]。这个函数读取不超过MAXN-1个字符,然后在末尾添上结束符‘\0’,因此不会出现越界的情况。之所以说可以用这个函数读取完整的一样,是因为一旦读到回车符'\n',读取工作将会停止,而这个'\n'也会是buf字符串中最后一个有效字符(再往后就是字符串结束符‘\0’了)。只有在一种情况下,并且文件的最后一个不是以'\n'结尾:读到文件结束符,并且文件的最后一个不是以‘\n’结尾。尽管比赛的组织方应避免这样的情况(和输出文件一样,输入的文件的每行均已回车符结尾),但正如刚才所说的,选手应该把自己的程序写的更鲁棒。

提示3-16 :fgets(buf,MAXN,fin)将读取完整的一行放在 字符数组buf中。你应当保证buf足够存放下文件的一行内容。除了在文件结束前没有遇到‘\n’这种特殊情况外,buf总是以'\n'结尾。当一个字符都没有读到时,fgets返回NULL。

  和fgetc一样,fgets也有一个“标准输入版”gets。遗憾的是,gets和它的兄弟差别比较大:他的用法是gets(s),没有指明读取的最大字符数。这里就处死安乐一个潜在的问题:gets将不停的往s中塞东西,而不管塞不塞得下。难道gets函数不去管s的可用可用空间有多少么? 你还真说对了。。。(自己抄这块,感觉太二了)

提示3-17 C语言并不禁止程序读写“非法内存”。例如你声明的是char s[100],你完全可以赋值s[1000] = 'a'(甚至-Wall也不会警告),但后果自负。

  正式因为如此,gets已经被废除了,但是为了向后兼容,你仍然可以使用它。从长远考虑,读者最好不要使用它。

提示3-18: C语言中的gets(s)存在缓冲区溢出漏洞,不推荐使用。

  说了那么多,我们终于解决了“输出中有空格”的问题,我们选择的是fgets函数,它可以一次性读取一整行,最为方便。

  接下来,需要解决“判断时忽略标点,输出时却要按原样”的问题。首先,输入的标点符号不能直接删除(否则输出时就没办法了),但如果每次判断时都要跳过标点符号,又似乎不太直接,调试也麻烦。

  这里介绍一个通用的方案:预处理。构造一个新的字符串,不包含原来的标点符号,而且所有字符变成大写(顺便解决了大小写的问题):

n = strlen(buf);
m = 0;
for(i=0;i<n;i++)
if(isalpha(buf[i])) s[m++] = toupprt(buf[i]);

  上面的代码用到一个新函数:ctype.h中的isalpha(c),它用于判断字符c是否为大写字母或小写字母。用toupper(c)返回c的大写形式。在真样的预处理之后,buf保存的就是原串中的所有字母了。顺便说一句,c-'a'+'A'也可以把小写字母变成大写字母(想一想,为什么——+——其实很简单,小写字母的ascii码在大写字母的前面,字符变量c减去a 就是它相对于a的位置,加上A刚好是大写的时候的位置)

 

提示3-19:当任务比较复杂是,可以用预处理的方式简化输入,并提供更多的数据供使用。复杂的字符串处理题目往往可以通过合理的预处理简化任务,便于调试。

提示3-20:头文件中定义的isalpha、isdigit、isprint等工具可以用来判断字符的属性,而toupper、tolower等工具可以用来转换大小写。

  接下来的问题就简单了:美剧回文串的起点和重点,然后判断他是否真的是回文串。

int max = 0;
for(i=0;i<m;i++)
    for(j=i;j<m;j++)
        if(s[i...j]是回文串&&j-i+1>max)max = j-i+1;

  这里用到了“当前最大值”变量max,它保存的是目前为止发现的最长回文子串的长度。如果串烧的第i个字符到第j个字符(记为s[i...j])是回文串,则检查长度j-i+1是否超过max。

  最后判断s[i...j]是否为回文串的方法也不难写出:

int ok=1;
for(k=i;i<=j;i++)
    if(s[k]!=s[i+j-k]) ok = 0;

  注意这里的循环变量不能是i或者j,因为它们已经在外层使用过了。s[k]的“对称“位置是s[i+j-k](想一想为什么————很简单,k是在i到j之间的位置,k-i是k相对于i的位置,j-(k-i)就是k在i到j中的对称位置),因此只要一次比较失败,就应把标记变量ok置为0.

  下面是到目前为止的程序

 

#include <string.h>
#include <ctype.h>
//#include <time.h>
#define INF 100000000
#define LOCAL
#define MAXN 5000+10
char buf[MAXN],s[MAXN];
int main()
{
    int n,m=0,max = 0;
    int i,j,k;
    fgets(buf,sizeof(s),stdin);
    n = strlen(buf);
    for(i = 0;i < n;i++)
        if(isalpha(buf[i])) s[m++] = toupper(buf[i]);
    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)max = j-i+1;
        }
    printf("max = %d\n",max);
        return 0;
}

(唔,这段代码貌似实现了,但是还是有不少问题,比如说ab这个会出现最小子串为1的这种情况。这个会不会跟s[0+0-0] = s[0] 有关,所以必定会出现一个)

  这个程序还有一些功能没有完成,但已离成功不远了——在实际编程时,我们经常编写一个具备主要功能的程序,再加以完善。我们甚至可以先写一个只有输入输出功能的“股价”,但是要确保它正确。这样,每次只添加一点点小功能,而且写一点就测试一点,和一次写完整个程序相比,更加不容易出错。这种方法称为迭代式开发。

提示3-21:在程序比较复杂是,出来在设计阶段可以用伪代码理清思路外,编码阶段可以采用迭代式开发——每次只实现一点小功能,但要充分测试,确保它工作正常。

  经测试,上面的代码已经可以顺利取出两粒输出中最常回文串的长度——17 了。接下来的任务是输出这个回文串:要求原样输出,并且尽量靠左。输出靠左的条件已经满足了:我们是从左到右美剧的,且只在j-i+1严格大鱼max时才更新max。这样就只剩下唯一的问题了:原样输出。

  经过一番思考之后,似乎小小的改动是不足够的——即使在更新max时把i和j保存下来,我们还是不知道s[i]和s[j]在原串buf中的位置。因此,我们必须增加一个数组p,用p[i]保存s[i]在buf中的位置。它可以很容易的在预处理中得到;然后在更新max的同时把p[i]和p[j]保存到x和y,最后输出buf[x]到buf[y]中的所有字符。

  看上去很完美了,不是么? 等一下!题目熟了字符多大500个,程序效率会不会太低?只需要生产一个5000个‘a’的字符串就会发现:确实太慢了。。。(三重循环肯定慢死了。。)

   其实我们可以换一种方式:枚举回文串的“中间”位置i,然后不断往外扩展,知道有字符不同。下面是完整程序,聪明的读者(==|||)你能看得懂么?提示:长度为奇数和偶数的处理方式是不一样的。。。

 

#include "stdafx.h"
#include <stdio.h>
#include "math.h"
#include <string.h>
#include <ctype.h>
//#include <time.h>
#define INF 100000000
#define LOCAL
#define MAXN 5000+10
char buf[MAXN],s[MAXN];
int p[MAXN];
int main()
{
    int n,m=0,max = 0,x,y;
    int i,j;
    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]);
        }
    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];}
        }
    }
    for(i =x;i<=y;i++)
        printf("%c",buf[i]);
    printf("\n");
    return 0;
}

目前就是这样了

posted on 2013-11-20 22:37  混沌奇迹  阅读(551)  评论(0编辑  收藏  举报