您的位置:首页 > 其它

KMP 算法图文详解

2017-05-16 10:53 239 查看
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)



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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: