Bootstrap

KMP算法

  写本篇博客只是为了帮助自己总结一下KMP算法,在总结之前特别感谢这篇博文http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html,因为我就是从那里学来的,这里只是换一下表达方式。 

1、口述KMP算法的优点

2、字符串的前后缀概念

3、构造next数组的传统方法

4、KMP算法举例讲解

5、构建next数组的精简算法

6、KMP算法提醒

7、那道例题 

1、口述KMP算法的优点:

 给定目标字符串target=a1a2···an和模板字符串pattern=b1b2···bm,其中n >=m,我们需要做的是从target字符串中找到一个和pattern字符串相同的子串。

 KMP的算法的优异之处就是使它曾经比较过的信息尽可能地发挥它最大的价值。

2、字符串的前后缀概念:

字符串的前缀:指除了该字符串的最后一个字符之外,一个字符串的全部头组合。 

  例如:"abc"字符串的前缀组成的集合为{“a”, “ab”} 

与此相对的就是后缀,后缀的概念与此相反。 

  例如:"abc”字符串的后主组成的集合为{“c”,”bc”} 

这里需要注意一下:字符串"a”的前缀和后缀为空。 

3、构造next数组的传统方法:

  有了前后缀的概念,我们现在来构造一个名为next的数组,这个数组是这样的,它描述的是字符串的前后缀的最大匹配的字符的个数。 下面举一个例子完成next数组的构建: 

  设字符串s=”abcabd”,那么next数组的长度可以设为6。 

  next[0]表示为s的子串”a”的前后缀最大匹配的字符的个数,由于”a”的前后缀都为空,所以next[0]=0; 

  next[1]表示为s的子串”ab”的前后缀最大匹配的字符的个数,由于前缀集合={“a”},后缀集合={“b”},两个集合中没有公共的元素,所以next[1]=0; 

  next[2]表示为s的子串”abc”的前后缀最大匹配的字符的个数,由于前缀集合={“a”, “ab”},后缀集合={“c”, “bc”},两个集合中没有公共的元素,所以next[2]=0; 

  next[3]表示为s的子串”abca”的前后缀最大匹配的字符的个数,由于前缀集合={“a”,”ab”,”abc”},后缀集合={“a”,”ca”,”bca”},两个集合中有最长的公共的元素”a”,长度为1,所以next[3]=1; 

  next[4]表示为s的子串”abcab”的前后缀最大匹配的字符的个数,由于前缀集合={“a”,”ab”,”abc”,”abca”},后缀集合={“b”,”ab”,”cab”,”bcab”},两个集合中最长的公共的元素”ab”,长度为2,所以next[4]=2······ 

  最后得到的next数组为next={0,0,0,1,2,0}。 

4、KMP算法举例讲解:

  例:判断目标字符串target=”dabcaabcabfabcabe”的子串集合中是否包含模板字符串pattern=”abcabe”。

1)第一个字符a和目标字符串的字符d不匹配,于是跳往下一个字符; 

 2)下一次的不匹配出现在pattern字符串的第5个字符b上 

  

我们一般的思想就是把字符串pattern向右再移动一位即下图所示 

  

现在我们来分析一下这种做法的信息遗漏之处,在把pattern字符串向右移动一位之前,我们是知道在pattern字符串的前4位是和目标串相匹配的。好的,那么现在我们就要利用这个信息使算法的复杂度降低。

3)先上图: 

  

为什么我们直接让pattern字符串向右移动了3位,而不是1位呢? 

这里就要用到我们之前求字符串最大前后缀匹配长度的知识了。我们不难求得abcabe的next数组: 

  

这里计算pattern字符串右移的位数的公式是: 

右移位数=已经匹配的字符串的长度-最后一个匹配字符的next值 

(至于为什么要这么做下面有仔细的分析,现在只需要会用就行了) 

这里已经匹配的字符串为”abca”,长度为4,最后一个匹配的字符为‘a’,该处’a’对应的next值为next[3]=1,故这里应该为3=4-1。 

4)右移位数=已经匹配的字符串的长度-最后一个匹配字符的next值 

1=1-0,结果为下图: 

 5)右移位数=已经匹配的字符串的长度-最后一个匹配字符的next值 

3=5-2,结果为下图: 

 


 

6)右移位数=已经匹配的字符串的长度-最后一个匹配字符的next值 

2=2-0,结果为下图: 

7)右移位数=已经匹配的字符串的长度-最后一个匹配字符的next值 

1=1-0,结果为下图: 

 

算法整个过程结束,现在我们来分析一下,该怎样理解公式: 

   右移位数=已经匹配的字符串的长度-最后一个匹配字符的next值 

我们拿这一步来分析:   

当我们发现’f’和’e’不匹配的时候,我们需要将pattern向右移动3位。 

分析:因为前面有”abcab”和目标串匹配,所以目标串的字符’f’之的前5位也是“abcab”,而针对字符串”abcab”,我们之前计算过它的最大前后缀匹配的字符串的最大长度为2,即”abcab”的前缀的”ab”和后缀的”ab”相同。所以可以直接将前部分的ab移到后部分的ab的位置,需要移动的位置就是字符串的长度-后缀部分的长度,即5-2=3。 

5、构建next数组的精简算法:

  现在给出求next数组的算法,这是最关键的地方!!(思考不通的时候不妨用假设法理解一下)为了方便阅读,我们把图放出来。 

 

给定字符串”abcabe”计算其next数组: 

  我们是从i=0计算到i=5的,为了利用前一次的计算的结果,我们设想一下计算到next[4]的时候,我们已经知道了next[3]=1,也就是说在第二个’b’字符之前最多已经有长度为1的字符串和b之前的字符串相匹配(现实中就是第一个字符’a’和第二个字符’a’匹配)。  那么我们再将第一个字符串”a”后面的一个字符也就是第2个字符’b’与我们需要计算的next[4]对应的字符(第五个字符’b’)比较一下。(其中2是由2=next[3]+1得来)如果相等的话,就是说我们有了最长的前后缀字符串匹配(第一个”ab”和第二个”ab”),当然我们发现他们是相等的所以有next[4]=next[3]+1=2; 

  好了我们现在来计算next[5],由于计算next[5]之前已经有了next[4]=2,也就是说在’e’字符之前最长有长度为2的两个字符串相等,那么我们来比较一下第五个字符和第三个字符是否相等(next[4]+1,注意和上面的2=next[3]+1对应),发现’e’和’c’不相同。

我们现在需要找字符串”ab”的最长前后缀匹配,最最最重要的一句话!!!为什么呢?因为我们想看看”ab”中是否有前后缀相同的串。可以拿字符串s1=ababcababa为例,我们求该字符串的next[9]时知道有next[8]=4,即ababcababa,此时发现"a"!=“c”,因此我们找abab的最长前后缀匹配,发现其最长前后缀为"ab"(即next[3]=2),故再比较前缀ab的后一个字符s1[2]与s1[9]比较,得出s1[2]=s1[9],故而next[9]=next[3]+1。

好了要说的话我说完了,我把代码贴出来方便大家理解,看代码,实在看不懂一步步调试看。

为了让大家更轻易的看清代码,我来陈述一个事实,有没有发现如果前面的字符串的最长前后缀匹配为0,即没有相同的前后缀字符串,那么我们就直接将pattern串右移一位,就是说前面的没啥信息可以使用。

void get_next ( char *pattern ,int * next){
    int i,k;//这里i指的是pattern字符串的下标,k指的是最大前后缀的长度
    int len = strlen(pattern);//m指的是pattern字符串的长度
    next[0]=0;//pattern的第一个字符的最大前后缀匹配字符串的长度为0
    //再次注意这里我们的k是指最大前后缀字符串的长度
    for(i=1,k=0; i<len ; i++){
        //k>0的原因是如果前面有可以匹配的前后缀字符串,我们就要利用那个信息
        //求解pattern[0]...pattern[i]的最大前后缀长度k
        while(k>0 && pattern[i]!=pattern[k]){//思考一下为什么是pattern[k]
            k=next[k-1];//思考一下为什么是k-1
        }
        //当我们发现k=0的时候,也就是说前面的字符串中并没有能够有相匹配的前后缀并且后缀的下一个字符和当前pattern字符串的第i+1个字符相同的
        //当我们发现此时pattern[q]==pattern[k]的时候,说明前后缀相同的字符串有相同的并且后缀字符串的后面的一个字符和pattern字符串的第i+1个字符相同,相同的话我们的k就要+1了
        if(pattern[k]==pattern[i]){
            k++;
        }

        //最后赋值
        next[i]=k;

    }
}

6、贴题目:

(代码中我目前想到存在有三个可以优化的地放,但是不影响整体时间复杂度,所以我就没有改)

                               子串逆置

【问题描述】

输入两行字符串s和t(s和t可以含空格,length(t)≤length(s)≤50),将s串中首次与t匹配的子串逆置,并将处理后的s串输出。

【输入形式】

输入文件为当前目录下的invertsub.in。 文件中有两行字符串s和t,分别以换行符作为结束符,其中换行符可能是Linux下的换行符,也可能是Windows下的换行符。

【输出形式】

输出文件为当前目录下的invertsub.out。 输出文件只有一行,包含一个串,为要求的输出结果。行末要输出一个回车符。

【输入样例】

helloworld 

llowor

【输出样例】

herowollld

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int get_length(char *);
void cal_next(char *,int *,int );

int main()
{
    char target[52],pattern[52];
    int len1,len2,i,j,tmp;
    int next[52];
    FILE *fin,*fout;

    if((fin=fopen("invertsub.in","r"))==NULL){
        printf("In can not be opened!");
        exit(1);
    }
    if((fout=fopen("invertsub.out","w"))==NULL){
        printf("OUT can not be opened!");
        exit(1);
    }

    memset(target,'\0',52);
    memset(pattern,'\0',52);
    fgets(target,52,fin);
    fgets(pattern,52,fin);

    len1=get_length(target);
    len2=get_length(pattern);

    cal_next(pattern,next,len2);

    for(i=0;i<len1;){
        for(j=0,tmp=i;tmp<len1 && j<len2;j++){
           if(pattern[j]==target[tmp]){               tmp++;
                continue;
           }else {
                if(tmp==i)
                    i++;
                else
                    i+=j-next[j-1];
                break;
           }
        }
        if(j==len2)
            break;

    }
    for(j=len2-1;j>=0;j--,i++){
        target[i]=pattern[j];
    }
    target[len1]='\n';
    fprintf(fout,"%s",target);
    fclose(fin);
    fclose(fout);
    return 0;
}

int get_length(char *s){
    int i;
    for (i=51;s[i]!='\n';)
        i--;
    s[i]='\0';
    return i;
}

void cal_next(char *s,int* next,int len){
    int k,i;

    for(i=1,next[0]=0,k=0;i<len;i++){
        while(k>0 && s[i]!=s[k])
            k=next[k-1];
        if(s[k]==s[i]){
            k++;
        }
        next[i]=k;
    }
}

;