KMP算法

KMP算法

1 前言

没什么好说的,KMP算法对于字符串匹配问题的重要性不言而喻,相比于暴力算法的O(mn),KMP算法的需求只有O(m+n),在字符串匹配用到如今的方方面面的情况下(搜素引擎、关键字查找、DNA匹配等等),趁着自己刚学会,赶紧来记录一下自己学习的过程,并且好好地理顺这算法。

2 部分匹配表(Partial Match Table)

知乎:如何更好地理解和掌握 KMP 算法?

我很赞同知乎这位答主的看法,固然从问题出发逐步优化学习KMP也是很有效的方法,但如果一开始就掌握了next数组的本质PMT,将是事半功倍的。
因此,即使还不清楚PMT和字符串匹配有什么关系,也最好先搞清楚PMT即next数组的清晰定义。

字符串的前缀不包含最后一个字符的所有以第一个字符开头的连续子串,假设一个字符串视为AB,其中B为任意非空字符串,那么所有A所组成的集合即为前缀,例如字符串Leet中,其前缀集合为{“L”,“Le”,“Lee”}。
同理后缀不包含第一个字符的所有以最后一个字符结尾的连续子串,在上述的例子中,A为任意非空字符串,那么所有B组成的集合即为后缀,Leet的后缀集合为{“t”,“et”,“eet”}。

那么加下来所介绍的next数组(PMT)所存储的值就是最大相同前后缀的长度,换言之就是前缀集合与后缀集合的交集中元素的最大长度,以"ababab"为例,前缀集合为{a,ab,aba,abab,ababa},而后缀集合为{b,ab,bab,abab,babab},那么两者的交集就是{ab,abab},其元素的最大长度为abab的4
KMP的思想就是利用这样一个PMT辅助字符串的匹配过程。

3 从朴素匹配法出发


现在让我们抛开必须记住的next数组,字符串匹配最简单的思路无疑是两重循环,i指向目标串t,j指向模式串p,如果t[i]=p[j],就继续比较t[i+1]和p[j+1],如果不一致就i=i+1,j=0,目标串到下一位,模式串从头开始。

这个算法最大问题在于忽略了模式串本身有穷的特性,反复的回溯浪费了已经比较所得到的的信息,如果我们能在比较开始之前就确定了模式串每个位置比较失败后的适当移动距离,那么利用这么一个静态信息库就可以避免每次都回溯到模式串到0造成的浪费。

懒得画图,用代码示意一下。

目标串:abababc
模式串:abc
#朴素算法
abababc
abc    ->
 abc   ->
  abc  ->
   abc ->
    abc
#KMP
abababc
abc    ->
  abc  ->
    abc

4 KMP算法过程

看到这已经可以意识到我们之前所讨论的PMT、next数组、相同前后缀最大长度十有八九和我们所需要的适当移动距离静态信息库有关了。

  • 静态认知
    但在这,我们还需要有一个清晰的认知,这么一个信息库应该是静态的,意味着我们可以在匹配之前计算出来,而不是在匹配过程中随着模式串和目标串的配对情况实时计算得到,不然就没有意义了。
    现在让我们无视目标串,只专注于模式串,如果我们在考虑模式串p的p[j]的匹配情况时,意味着p[0]~p[j-1]均为匹配成功,也就意味着模式串t[i]前的p个字符与模式串p[0]-p[j-1]一致,那么这个信息库是一个模式串和目标串解耦的结果,只与模式串有关。


铺垫了这么久,我们终于来讲解next数组与指针移动距离之间的关系了,显然我们直接移动指针需要保证两点:

  1. 新指针前的所有字符应该与匹配失败指针前相同长度的字符串相同。
  2. 如果符合条件1的新指针不止一个,应该选择移动距离最短的,即指针索引最大的。

粗糙地判定一下next数组中的值意义,next[i]代表着字符串0~i-1的最大相同前后缀长度,不妨设next[i]=k,这意味着i前k位(后缀):i-k,…,i-1前缀0,…,k-1必然是相同的,这天然地符合了我们的条件1与条件2

满足了条件1,意味着我们略过大量没有意义的必然错误的匹配比较,不再回溯。
满足了条件2,意味着我们不会越过满足匹配的情况,也意味着k越小,我们移动的距离越大。

换个角度思考,模式串越小的索引与目标串的索引j比对,意味着模式串越靠近目标串的末尾

我们假设next[0]=-1,同时必然地next[1]=0(这只是一种习惯规范)
现在我们有了next数组,我们就可以轻松地进行匹配,可以详细地看注释。

int strStr(string haystack, string needle,int* next) {
    //KMP算法
    int h_size=haystack.size();
    int n_size=needle.size();
    int i=0;
    int j=0;
    while(i<h_size && j<n_size)
    {
        //字符相等比较下一位,十分容易理解
        //j==-1意味着找到了next[0],代表模式串没有相同前后缀,意味着我们可以直接用模式串头指针去对目标串的下一个指针。
        if(j==-1||haystack[i]==needle[j])
        {
            ++i;
            ++j;
        }
        //字符串不相同,移动模式串
        else
        {
            j=next[j];
        }
    }
    if(j==n_size)return i-n_size;
    return -1;
}


现在我们只剩下最后一个问题,如何生成next数组

5 next数组生成

在next数组已知情况下,计算复杂度为O(n),但如果next生成复杂度过高,那KMP算法也没意义。
还是从定义出发,next[i]代表0~i-1字符串的相同前后缀的最大长度,我们初始化next[0]=-1。

不妨假设next[i-1]=k-1,我们考虑next[i]的递推公式如何计算。

示意图(事实上可能有重叠,即[…]反而是负长度):
字符串分段:[0,1,…,k-2] [k-1] [k] […] [i-k,i-k+1,…i-2] [i-1] [i]
因为[0,1,…,k-2]=[i-k,i-k+1,…,i-2]=S,简化后:
[S] [k-1] [k] […] [S] [i-1] [i]

  • 若p[i-1]=p[k-1]时,那么[S][k-1]=[S][i-1],意味着next[i]=k。
  • 若p[i-1]!=p[k-1],那么我们要继续寻找一个更短的相同前后缀,[0,…,k-1]的最长相同前缀也是[0,…,i-1]的相同前缀(非最长),令k=next[k],迭代直到满足条件1或者k==-1。

至此,我们也已经知道了next数组的生成,其复杂度为O(m),KMP总体复杂度为O(m+n),全部代码如下:

class Solution {
public:
    int strStr(string haystack, string needle) {
        //KMP算法
        int h_size=haystack.size();
        int n_size=needle.size();
        int next[n_size];
        int i=0;
        int j=0;
        //初始化next[0]与k
        //k代表next[i],needle_i的最长相同前缀的长度
        next[0]=-1;
        int k=-1;
        while(i<n_size-1)
        {
            //如果k==-1,代表着在已有的前缀表中不存在needle[i]==needle[k]
            //那么意味着needle[i]无法与needle进行匹配,那么直接将字符串needle的头移动到i+1
            //此时,needle[i+1]=0
            //另外,因为needle[i]=k,因此0~k-1和i-k~i-1一致
            //若needle[i]==needle[k],则0~k和i-k~i一致,为新的最长相等前缀
            //此时needle[i+1]=k+1
            if(k==-1||needle[i]==needle[k])
            {
                next[i+1]=k+1;
                ++i;
                ++k;
            }
            //needle[i]!=needle[k]时,需要缩短前缀长度
            //寄希望于needle[next[k]],因为next[k]的最大相等长度也是needle[i]的部分相等长度
            //如果needle[next[k]]==needle[i],就相当于找到一个新的略短但也是最大的相等长度
            else
                k=next[k];
        }
        //查询
        i=0;
        while(i<h_size && j<n_size)
        {
            if(j==-1||haystack[i]==needle[j])
            {
                ++i;
                ++j;
            }
            else
            {
                j=next[j];
            }
        }
        if(j==n_size)return i-n_size;
        return -1;
    }
}; 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值