KMP 算法图文详解

17 篇文章 0 订阅
1 篇文章 0 订阅

1、暴力匹配算法

假设我们现在面临这样一个问题,有一个文本串 S 和一个模式串 P,现在要查找P在S中的位置,那么应该如何查找呢?

我们很容易就想到暴力匹配的方法,假设现在文本串S匹配到 i 位置,模式串 P 匹配到 j 位置,则有:

1.如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;

2.如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

暴利匹配算法非常简单,下面直接上代码:

int ViolentMatch(string s,string p)
{
   int s_len=s.length();
   int p_len=p.length();
   int i=0;
   int j=0;
   while(i<s_len && j<p_len)
   {
       if(s[i]==p[j])
       {
           i++;
           j++;
       }
       else{
           i=i-j+1;
           j=0;
       }
   }
   //匹配成功,返回模式串P在文本串S中的位置
   if(j==p_len)
   {
      return i-j;
   }
   else{
      return -1;
   }
}

详细流程见下图
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
1.S[0]=B,P[0]=A,不匹配.则令i=i-j+1(i=1,j=0)

这里写图片描述

2.S[1]跟P[0]还是不匹配,则令i=i-j+1,j=0,如果失配,S就不断的向右移

这里写图片描述

3.直到S[4]跟P[0]匹配成功(i=4,j=0),此时按照上面的暴力匹配算法的思路,转而执行第①条指令:“如果当前字符匹配成功(即S[i] == P[j]),则i++,j++”,可得S[i]为S[5],P[j]为P[1],即接下来S[5]跟P[1]匹配(i=5,j=1)

这里写图片描述

  1. S[5]跟P[1]匹配成功,继续执行第①条指令:“如果当前字符匹配成功(即S[i] == P[j]),则i++,j++”,得到S[6]跟P[2]匹配(i=6,j=2),如此进行下去

这里写图片描述

5.直到S[10]为空格字符,P[6]为字符D(i=10,j=6),因为不匹配,重新执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,相当于S[5]跟P[0]匹配(i=5,j=0)

这里写图片描述

6.由上图可以看到,按照暴力匹配的思想,尽管文本串和模式串已经匹配到了S[9]和P[5],但因为S[10]和P[6]不匹配,所以文本串回溯到S[5],模式串回溯到P[0],从而让S[5]跟P[0]匹配。

这里写图片描述

由此可以看到,暴力匹配算法最坏情况下时间复杂度为O(n*m),即每次不匹配都发生在模式串的最后一个字符。为什么暴力匹配这么耗时呢,因为这之中有不必要的回溯。

那么有没有一种算法,可以让i不回退,只需要移动j即可呢???

答案是肯定的!!!这种算法就是本文要说的KMP算法。

2、KMP算法

定义:Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

下面给出KMP算法的流程:(看不懂没关系,毕竟这是一个难度很大的算法,后面我会用图文来解说)

*假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置

如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;

如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。

换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。

next数组各值的含义:代表当前字符之前的字符串中,有多大长度相同的前缀和后缀。例如如果next[j]=k,代表j之前的字符串中有最大长度为K的相同前缀和后缀。这意味着在某个字符失配时,该字符对应的next值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next[j]的位置)。

重点来了,如何求next数组呢???

KMP的next数组求法是很难理解的一部分,同时也是KMP算法中最重要的部分。

next数组其实就是查找 模式串P 中每一位前面的子串的前后缀有多少位匹配,从而决定j失配时应该回退到哪个位置

下面直接上图:

图片来源于:http://www.cnblogs.com/tangzhengyue/p/4315393.html

这里写图片描述

这个图画的就是 模式串P。假设我们有一个空的next数组,我们的工作就是要在这个next数组中填值。
下面我们用数学归纳法来解决这个填值的问题。
这里我们借鉴数学归纳法的三个步骤(或者说是动态规划?):
1、初始状态
2、假设第j位以及第j位之前的我们都填完了
3、推论第j+1位该怎么填

初始状态我们稍后再说,我们这里直接假设第j位以及第j位之前的我们都填完了。也就是说,从上图来看,我们有如下已知条件:
next[j] == k;
next[k] == 绿色色块所在的索引;
next[绿色色块所在的索引] == 黄色色块所在的索引;

接下来这个图非常重要,希望大家仔细体会,同时为原作者画出这样简洁清晰的图表示由衷的感谢。

这里写图片描述

1.由”next[j] == k;”这个条件,我们可以得到A1子串 == A2子串(根据next数组的定义,前后缀那个)。

2.由”next[k] == 绿色色块所在的索引;”这个条件,我们可以得到B1子串 == B2子串。

3.由”next[绿色色块所在的索引] == 黄色色块所在的索引;”这个条件,我们可以得到C1子串 == C2子串。

4.由1和2(A1 == A2,B1 == B2)可以得到B1 == B2 == B3。

5.由2和3(B1 == B2, C1 == C2)可以得到C1 == C2 == C3。

6.B2 == B3可以得到C3 == C4 == C1 == C2

接下来,我们开始用上面得到的条件来推导如果第j+1位失配时,我们应该填写next[j+1]为多少?

next[j+1]即是找P从0到j这个子串的最大前后缀:

@:(@:在这里是个标记,后面会用)我们已知A1 == A2,那么A1和A2分别往后增加一个字符后是否还相等呢?我们得分情况讨论:

(1)如果P[k] == P[j],很明显,我们的next[j+1]就直接等于k+1。

  用代码来写就是next[++j] = ++k;

(2)如果P[k] != P[j],那么我们只能从已知的,除了A1,A2之外,最长的B1,B3这个前后缀来做文章了。

那么B1和B3分别往后增加一个字符后是否还相等呢?

由于next[k] == 绿色色块所在的索引,我们先让k = next[k],把k挪到绿色色块的位置,这样我们就可以递归调用”@:”标记处的逻辑了。

由于j+1位之前的next数组我们都是假设已经求出来了的,因此,上面这个递归总会结束,从而得到next[j+1]的值。

我们唯一欠缺的就是初始条件了:

next[0] = -1, k = -1, j = 0

另外有个特殊情况是k为-1时,不能继续递归了,此时next[j+1]应该等于0,即把j回退到首位。

即 next[j+1] = 0; 也可以写成next[++j] = ++k;

下面直接上代码:

这里写图片描述

之后再加上KmpSearch的代码,程序就完整了:

这里写图片描述

最后把完整的C++代码贴上来,程序比较简单,还有待优化,同时next数组还可以进一步优化,读者们自己去思考吧

#include<iostream>
using namespace std;

int NEXT[100]={0};

void getNext(string p,int NEXT[])
{
    int len=p.length();
    int j=0;
    int k=-1;
    NEXT[0]=-1;
    while(j<len-1)
    {
        //ps[k]表示前缀,ps[j]表示后缀
        if(k==-1 || p[j]==p[k])
        {
            NEXT[++j]=++k;
        }
        else
        {
            k=NEXT[k];
        }
    }
}

int KmpSearch(string s,string p,int NEXT[])
{
    int i=0;
    int j=0;
    int s_len=s.length();
    int p_len=p.length();
    while(i<s_len && j<p_len)
    {
        //如果 j=-1或当前字符匹配成功
        if(j==-1 ||s[i]==p[j])
        {
            i++;
            j++;
        }
        else{
        //如果j!=-1,且当前字符串匹配失败,则令i不变,j=next[j]
            j=NEXT[j];
        }
    }
    if(j==p_len)
    {
        return i-j;
    }
    else
    {
        return -1;
    }

}

int main()
{
    string s="abababacabad";
    string p="ad";
    getNext(p,NEXT);
    cout<<KmpSearch(s,p,NEXT);
    system("pause");
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值