KMP算法详解

在介绍KMP算法之前,先看下一般得解法。
主字符串以下简称位(T)
模式串以下简称位(P)
题目: 在字符串T"abcdefghijk"中查找P"abce",如果出现就返回P得具体位置,否则返回-1.
一般的解法

int my_strstr(char * T, char * P) {
    int lenT = strlen(T), lenP = strlen(P);
    int i = 0, j = 0;
    int res = -1;
    for (i = 0; i <= (lenT - lenP); i++) {
        for (j = 0; j < lenP; j++) {
            if (T[i + j] == P[j]) {
                continue;
            }
            break;
        }
        if (j == lenP) {
            res = i;
            break;
        }
    }
    return res;
}

上面代码没有问题,但是还可以进行优化。

如何优化
参考上面的算法,我们串中的位置指针i,j来说明,第一个位置下标以0开始,我们称为第0位。下面看看,如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置(i=3)前面除了第一个A之外再也没有A了,我们为什么能知道主串前面只有一个A?因为我们已经知道前面三个字符都是匹配的!(这很重要)。移动过去肯定也是不匹配的!有一个想法,i可以不动,我们只需要移动j即可,如下图:
在这里插入图片描述
或者如下图所示
在这里插入图片描述
总结:当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。
公式:当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]
在这里插入图片描述
该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。我们将这个解释定义成最大重复子串解释。
这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:

在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;
在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;
在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;
下面我们用这个解释,来再一次手动求解上面的过程:
首先如下图所示:
在这里插入图片描述
如图:C和D不匹配了,我们要把j移动到哪?j位前面的子串是ABA,该子串的前缀集是{A,AB},后缀集是{A,BA},最大的重复子串是A,只有1个字符,所以j移到k即第1位。
在这里插入图片描述
再分析下图的情况:
在这里插入图片描述
在j位的时候,j前面的子串是ABCAB,前缀集是{A,AB,ABC,ABCA},后缀集是{B,AB,CAB,BCAB},最大重复子串是AB,个数是2个字符,因此j移到k即第2位。
在这里插入图片描述
上面说的,如果分解成计算机的步骤,则是如下的过程:

1)找出前缀pre,设为pre[0~m];

2)找出后缀post,设为post[0~n];

3)从前缀pre里,先以最大长度的s[0~m]为子串,即设k初始值为m,跟post[n-m+1~n]进行比较:

如果相同,则pre[0~m]则为最大重复子串,长度为m,则k=m;

   如果不相同,则k=k-1;缩小前缀的子串一个字符,在跟后缀的子串按照尾巴对齐,进行比较,是否相同。

如此下去,直到找到重复子串,或者k没找到。

根据上面的求解过程,我们知道子串的j位前面,有j个字符,前后缀必然少掉首尾一个字符,因此重复子串的最大值为j-1,因此知道下一次的j指针最多移到第j-1位。

我为什么要补充上面这段说明,是因为该说明能便于我们理解下面的求解next数组的过程,上面实际也是指出了人工求解next[j]的过程。不知道next[j]为何物没关系,看到下面的定义以后,请到时再绕回来回味就行了。

next数组
在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。另一个非常有用且恒等的定义,因为下标从0开始的,k值实际是j位前的子串的最大重复子串的长度。请时刻牢记next数组的定义,下面的解释是死死地围绕着这个定义来解释的。

int * getNext(char * P) {
    int len = strlen(P);
    int i, k;
    int *next = (int *)malloc(sizeof(int) * len);
    next[0] = k = -1;
    i = 0;
    while(i < len - 1) {
        if (k == -1 || P[k] == P[i]) {
            next[++i] = ++k; 
        } else {
            k = next[k];
        }
    }
    return next;
}

有了next数组之后就一切好办了,我们可以动手写KMP算法了:

int my_strstr(char * T, char * P) {
    int lenT = strlen(T), lenP = strlen(P);
    int i = 0, j = 0;
    int res = -1;

    int *next = getNext(P);
    i = j = 0;
    while(i <= lenT - lenP + j + 1) {
        if (j == -1 || T[i] == P[j]) {
            i++, j++;
        } else {
            j = next[j];
        }
        if (j == lenP) {
            res = i - j;
            break;
        }
    }
    free(next);
    return res;
}

原文链接 : https://www.cnblogs.com/dusf/p/kmp.html

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值