算法—KMP字符串匹配

算法—KMP字符串匹配

现在有一个问题,要从一个字符串中查找出指定子串的位置(初始下标),通常地,我们会使用朴素的字符串匹配算法,如下面这道题
给出主串和需要查找的子串,输出子串是否存在,并输出子串的首位在主串中的下标

image.png

  • 首先,在主串中设下标i,在字串中设下标j,均从0开始

image.png

这时,将子串第一位与主串的第一位进行比较,结果是不同的,那么尽然不同,子串初始位置就一定不在当前的i处,所以i++,j还是0

image.png

i=1时,主串当前位置字符仍然与子串中的第一位不同,由上可得,i继续加一,j还是0

image.png

i=2时,匹配成功,这时i++,j++,继续判断下一位是否还能匹配

image.png

i=3时,仍旧匹配成功,重复上述操作

image.png

i=4,仍旧匹配。这时我们发现,i还没有到达主串的末尾,但是j已经到达子串的末尾。这就是查找成功的标志,返回i-j=2就是子串起始在主串中的下标

扫描二维码关注公众号,回复: 2842770 查看本文章
  • 下面是这种朴素字符串匹配算法的代码
#include<stdio.h>
#include<string.h>

void search(char str[], char son[],int pos)
{
    int index;
    int i = pos;    //主串中的位置下标 
    int j = 0;  //字串中的位置下标
    int len1 = strlen(str);
    int len2 = strlen(son);

    while(i < len1 && j < len2)
    {
        if(str[i] == son[j])
        {
            i++;
            j++;
        }
        else
        {
            i = i - j + 1;
            j = 0;
        }
    }
    if(j = len2)
    {
        if(pos >= len1-len2 || i >= len1)
        {
            printf("不存在子串了\n");
            return;
        }
        printf("查找到子串,起始索引在主串的%d处\n",i-len2);
        search(str,son,i-len2+1);               //递归继续搜索 
    }
}

int main()
{
    char str[1000];
    char son[1000];
    gets(str);
    gets(son); 
    search(str,son,0);
    return 0;
}

上面的字符串查找确实简单易懂,但有时它却有着很大的缺点,我们看下面的这个例子

image.png

  • 现在我们要从上面的主串中查找相对应的子串,我们按之前的朴素字符串查找过一遍

image.png

在 i=5,j=5时,判断字符不相符,这时,我们的i就需要回溯到i=1(第一个位置),j需要回溯到j=0(子串起始),然后继续开始如下的几个判断

image.png

image.png

image.png

image.png

image.png

  • 这个时候你会发现,i居然又回到了5,而且字符匹配也没有丝毫进展!那么上面做的那几步岂不是无用功?为什么这么说呢?
我们仔细观察,子串的每一位的字符都是不同的,所以在第一次匹配到i=5,j=5

时,我们是已经知道主串的前五位是和子串的前五位相同的(也就是说主串的前五位也各不相同),所以中间的这四次比较都是可以省略的,我们可以直接跳到最后一步。

有人说这个例子太特殊? 那我们下面再看一个例子

image.png

我们继续使用朴素字符串查找

image.png

i=j=5时停下,i回溯到i=1,j回溯到0

image.png

image.png

image.png

  • 这次我们又发现了什么? i又回到了5,j停留在2。在第一次比较到i=j=5时,我们已经知道主串和子串的前五位相同,也就是说,我们知道了主串的前三位各不相同,但主串的第一位和第四位相同,第二位和第五位相同,那么其实这中间的几步都是可以省略的,我们可以直接让i=5,j=2.
综上,我们发现主串中的i值是在不断回溯的,但最终又会回到原来的值,那么我们干脆不让它回溯了,即当判断主串与子串字符不同时,不改变i的值。
既然i值不会苏,也就是不可以变小,那么要考虑变化的就是j值了。通过观察也可以发现,我们屡次提到了字串的首字符与自身后面字符的比较,发现如果有相同字符,j值的变化就会不同,其实这个j值的变化与主串没关系,关键取决于子串的结构中是否有重复的问题。
  • 在上面的第一个例子中,子串=”abcdex”时,当中没有任何相同的字符,所以j就由5变成了0。第二个例子中,子串=”abcabx”,前缀的 “ab” 与后面的 “ab”是重复的,j就由5变成了2。因此我们可以得出规律:j值的的多少取决于当前字符之前的串的前后缀的相似度

现在就是整个KMP算法最核心的地方了,我们把子串各个位置的j值变化定义为一个数组next,那么next的长度就是子串的长度。我们有这样的函数定义:

  • 当j=0时,next[j]=0
  • 当{k| 1
子串=”abcdex”
  • j=0时:next[0]=0
  • j=1时:next[1]=1
  • j=2时:2之前的串中没有对称前后缀,next[2]=1
  • j=3时:同上
  • j=4时:同上
  • j=5时:同上
最终next数组={0,1,1,1,1,1};

这次,当我们判断到这一步时,

image.png

i不变,令j回溯到(next[j]-1),即i=5,j=0,我们直接来到了这一步:

image.png

第二个例子:

image.png

子串=”abcabx”
  • j=0时:next[j]=0
  • j=1时:next[j]=1
  • j=2时:2之前的串中无对称的前后缀,next[2]=1
  • j=3时:3之前的串中无对称的前后缀,next[3]=1
  • j=4时:4之前的串中,第一位 ‘a’和第四位 ‘a’相同,故next[4]=2
  • j=5时:5之前的串中,前两位 “ab”和后两位 “ab”相同,故next[5]=3
最终next数组={0,1,1,1,2,3}

那么我们在第一次判断后:

image.png

遵守KMP算法的规则,i不回溯,即i仍然等于5,j这时等于5,则应该回溯到(next[j]-1)=2处,我们就直接到了这一步:

image.png

是不是又直接跳过了中间的几步? 的确很神奇,这大大提高了字符串查找的效率

下面是KMP字符串匹配算法的代码:
#include<stdio.h>
#include<string.h>

void get_next(char son[],int next[])
{
    int i,j;
    int len = strlen(son);
    i = 0;
    j = -1;
    next[0]=0;
    while(i < len)
    {
        if(j == -1 || son[i] == son[j])
        {
            i++;
            j++;
            next[i] = j + 1;
        }
        else
            j = next[j] -1;         //j回退到合适的位置,i不变 
    }
}

void KMP(char str[],char son[],int pos)
{
    int i = pos;    //主串中的位置索引 
    int j =-1;      //子串中的位置索引

    int len1 = strlen(str);
    int len2 = strlen(son);

    int next[1000]; //next数组
    get_next(son,next);

    while(i < len1 && j <len2)
    {
        if(j == -1 || str[i] == son[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j] - 1;
        }
    }
    if(j = len2)
    {
        if(pos >= len1-len2 || i >= len1)
        {
            printf("不存在子串了\n");
            return;
        }
        printf("查找到子串,起始索引在主串的%d处\n",i-len2);
        KMP(str,son,i-len2+1);
    }
}

int main()
{
    char str[1000];
    char son[1000];
    gets(str);
    gets(son);

    KMP(str,son,0);
    return 0;
} 

按理说这里应该结束了,但KMP算法并不是完美的,比如下面这种情况:

image.png

子串=”aaaaax”
  • j=0时:next[0]=0
  • j=1时:next[1]=1
  • j=2时:’a’与 ‘a’对称 , next[2]=2
  • j=3时: “aa” 与”aa”对称 next[3]=3
  • j=4时:”aaa” 与”aaa”对称, next[4]=4
  • j=5时:”aaaa” 与”aaaa”对称, next[5]=5
next数组为:{0,1,2,3,4,5}

image.png

然后i不变,令j根据所求的next数组回溯

image.png

image.png

image.png

image.png

image.png

我们发现,j又回到了0,我们知道子串中的前五位是相同的,那我们为什么还要一次次的将其与主串中的 ‘b’进行比较呢? 所以我们还需要对KMP算法进行优化
  • 上面的例子中,我们可以直接来到最后一部,即i=5,j=0.所以在next数组中,第二三四五位的next值应该与第一位的next值相等。因此我们需要对求next数组的算法进行优化。

规则:在计算出next数组的同时,如果a字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值。如果不等,则该a位的nextval值就是它自己a位的next值

以上面的这个题为例

image.png

next数组为:{0,1,2,3,4,5}
  • j=0时:nextval[0]=0
  • j=1时:next指向j=0,串中j=1与j=0位置的字符相同,nextval[1]=nextval[0]=0
  • j=2时:next指向j=1,串中j=2与j=1位置的字符相同,nextval[2]=nextval[1]=0
  • j=3时:next指向j=2,串中j=3与j=2位置的字符相同,nextval[3]=nextval[2]=0
  • j=4时:next指向j=3,串中j=4与j=3位置的字符相同,nextval[4]=nextval[3]=0
  • j=5时:next指向j=4,串中j=5与j=4位置的字符不同,nextval[5]=next[5]=5

image.png

这时直接回溯到j=-1,然后

image.png

下面是优化过的KMP算法的代码:
#include<stdio.h>
#include<string.h>

void get_nextval(char son[],int nextval[])
{
    int i,j;
    int len = strlen(son);
    i = 0;
    j = -1;
    nextval[0] = 0;
    while(i < len)
    {
        if(j == -1 || son[i] == son[j])
        {
            i++;
            j++;
            if(son[i] != son[j])    //改进部位:若当前字符与前缀字符不同 
            {
                nextval[i] = j + 1;     //则当前的j为nextval在i位置的值 
            }
            else
                nextval[i] = nextval[j];    //如果与前缀字符相同,则将前缀字符的nextval值赋值给nextval在i位置的值
        }
        else
            j= nextval[j] -1;
    }
}

void KMP(char str[],char son[],int pos)
{
    int i = pos;    //主串中的位置索引
    int j =-1;      //子串中的位置索引

    int len1 = strlen(str);
    int len2 = strlen(son);

    int next[1000]; //next数组
    get_nextval(son,next);

    while(i < len1 && j <len2)
    {
        if(j == -1 || str[i] == son[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j] - 1;
        }
    }
    if(j = len2)
    {
        if(pos >= len1-len2 || i >= len1)
        {
            printf("不存在子串了\n");
            return;
        }
        printf("查找到子串,起始索引在主串的%d处\n",i-len2);
        KMP(str,son,i-len2+1);
    }
}

int main()
{
    char str[1000];
    char son[1000];

    gets(str);
    gets(son);

    KMP(str,son,0);
    return 0;
} 

猜你喜欢

转载自blog.csdn.net/wintershii/article/details/81661447