KMP-字符串匹配

来源:http://www.matrix67.com/blog/archives/115

来源:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

来源 http://blog.csdn.net/v_july_v/article/details/7041827

来源:塞奇威克 算法

解决的问题:给定两个字符串A和B,长度分别为M和N,要问B是否是A的一个子串;

暴力

复杂度O(M*N) 

  塞奇威克评价:对于一般的英文甚至中文匹配,由于字符相同的概率很低,所以实际上的复杂度是和O(N+M)成正比的,而对于二进制匹配这种字符很少的情况才有可能达到O(M*N)的最坏复杂度

<pre name="code" class="cpp">int search(char *a,char *b){
	int len1=strlen(a);
	int len2=streln(b);
	for(int i=0;i<=len1-len2;i++){
		int j;
		for(j=0;j<len2;j++){
			if(a[i+j]!=b[j])
				break;
		}
		if(j==len2) break;
	}
	if(j==len2) return i-j;
	return -1;
}

运用KMP,我们可以通过一个O(N)的预处理把复杂度降低到O(M+N)

 我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件KMPlaer),而是一种算法。KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A=”I’m matrix67″,字符串B=”matrix”,我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?”

 解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= “aaaaaaaaaaaaaaaaaaaaaaaaaab”,B=”aaaaaaaab”。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。

当然我们可以看到这个算法针对的是子串有对称属性,如果有对称属性,那么就需要向前查找是否有可以再次匹配的内容。

next数组的求法

next 数组考虑的是除当前字符外的最长相同前缀后缀

  • ③根据next数组进行匹配
    • 匹配失配,j = next [j],模式串向右移动的位数为:j – next[j]。换言之,当模式串的后缀pj-k pj-k+1, …, pj-1 跟文本串si-k si-k+1, …, si-1匹配成功,但pj 跟si匹配失败时,因为next[j] = k,相当于在不包含pj的模式串中有最大长度为k 的相同前缀后缀,即p0 p1 …pk-1 = pj-k pj-k+1…pj-1,故令j = next[j],从而让模式串右移j – next[j] 位,使得模式串的前缀p0 p1, …, pk-1对应着文本串 si-k si-k+1, …, si-1,而后让pk 跟si 继续匹配。如下图所示:

《KMP-字符串匹配》

    综上,KMP的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟文本串i 处的字符匹配,相当于模式串向右移动 j – next[j] 位。

 解释

 寻找最长前缀后缀

    如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
《KMP-字符串匹配》     也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(
下简称《最大长度表》):

《KMP-字符串匹配》

 基于《最大长度表》匹配

    因为模式串中首尾可能会有重复的字符,故可得出下述结论:

失配时,模式串向右移动的位数为:已匹配字符数 – 失配字符的上一位字符所对应的最大长度值

    下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

        《KMP-字符串匹配》

  • 1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:

《KMP-字符串匹配》

  • 2继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 – 2 = 4 位。

《KMP-字符串匹配》

  • 3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 – 0 =2 位。

           
《KMP-字符串匹配》

  • 4. A与空格失配,向右移动1 位。

《KMP-字符串匹配》

  • 5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 – 2 = 4 位。

           
《KMP-字符串匹配》

  • 6. 经历第5步后,发现匹配成功,过程结束。

          《KMP-字符串匹配》

    通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。

 根据《最大长度表》求next 数组

特别注意:next和前后缀长度的对应有一定的延迟

    由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:

《KMP-字符串匹配》

    而且,根据这个表可以得出下述结论

  • 失配时,模式串向右移动的位数为:已匹配字符数 – 失配字符的上一位字符所对应的最大长度值

    上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。     给定字符串“ABCDABD”,可求得它的next 数组如下:

《KMP-字符串匹配》

    把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

    换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:

《KMP-字符串匹配》

    根据最大长度表求出了next 数组后,从而有

失配时,模式串向右移动的位数为:失配字符所在位置 – 失配字符对应的next 值

    而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:

  • 根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 – 失配字符的上一位字符的最大长度值
  • 而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 – 失配字符对应的next 值
    • 其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。

    所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。

 通过代码递推计算next 数组

    接下来,咱们来写代码求下next 数组。

    基于之前的理解,可知计算next 数组的方法可以采用递推:

  • 1. 如果对于值k,已有p0 p1, …, pk-1 = pj-k pj-k+1, …, pj-1,相当于next[j] = k
    • 此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j – next[j] 位。

举个例子,如下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j – next [j] = 6 – 2 =4位。

《KMP-字符串匹配》

向右移动4位后,模式串中的字符C继续跟文本串匹配。

《KMP-字符串匹配》

下面的问题是:已知next [0, …, j],如何求出

next [j + 1]呢?

    对于P的前j+1个序列字符:

已知next[j]=k

  • 若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;
  • 若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。 相当于在字符p[j+1]之前不存在长度为k+1的前缀”p0 p1, …, pk-1 pk”跟后缀“pj-k pj-k+1, …, pj-1 pj”相等,那么是否可能存在另一个值t+1 < k+1,使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀 “pj-t pj-t+1, …, pj-1 pj” 呢?如果存在,那么这个t+1 便是next[ j+1]的值,此相当于利用已经求得的next 数组(next [0, …, k, …, j])进行P串前缀跟P串后缀的匹配。

   一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理,故接下来,我再来着重说明下。     如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。

《KMP-字符串匹配》

   
如果pk != pj 呢
说明“p0 pk-1 pk”  ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。
所以,咱们只能去寻找长度更短一点的相同前缀后缀。

《KMP-字符串匹配》

    结合上图来讲,若能
在前缀
“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk’-1 pk’ = pj-k’ pj-1 pj,则最大相同的前缀后缀长度为k’ + 1,从而next [j + 1] = k’ + 1 = next [k’ ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0。
    那
为何递归前缀索引k = next[k],就能找到长度更短的相同前缀后缀呢
?这又归根到next数组的含义()。
我们拿前缀 p0 pk-1 pk 去跟后缀pj-k pj-1 pj匹配,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 继续匹配,如果p[ next[k] ]跟pj还是不匹配,则需要寻找长度更短的相同前缀后缀,即下一步用p[ next[ next[k] ] ]去跟pj匹配
。此过程相当于
模式串的自我匹配
,所以不断的递归k = next[k],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。如下图所示:
(
对于自我匹配的个人理解:求next[j+1]的时候,我们已知的信息是在前面j个位置已经完成了是否匹配的检测,P[next[j]]与P[j+1]的关系判断十分显然。但是后面递归的过程就不是那么好理解了,为什么P[next[next[j]]]会和P[j+1]有关系?
这是因为next[i]其实是一个对称性的结果。
如果对于值k,已有p0 p1, …, pk-1 = pj-k pj-k+1, …, pj-1,相当于next[j] = k。那么P[next[j]]和P[j+1]的关系能够判断next[j+1]能否继承next[j]的前缀数量。

如果next[j+1]无法继承next[j]的前缀数量,即P[next[j]] != P[j+1],那么我们就考虑是否存在更小的前缀数量,这时候,满足条件的正是next[next[j]],它代表的正是p0,p1,到p next[next[j]]中的最小前缀数量,并且我们知道0,1,到next[next[j]]和 j,j-1到j-next[j]+1也一定是匹配的(根据定义,next计算的是前缀和后缀匹配,next[next[j]]显然是小于next[j]的,所以这里说明其实是一个更小的前缀和一个更小的后缀匹配)


    
《KMP-字符串匹配》

    所以,因最终在前缀ABC中没有找到D,故E的next 值为0:

模式串的后缀:AB
DE

模式串的前缀:AB
C

前缀右移两位:     
ABC


    读到此,有的读者可能又有疑问了,那能否举一个能在前缀中找到字符D的例子呢?OK,咱们便来看一个能在前缀中找到字符D的例子,如下图所示:

《KMP-字符串匹配》

    给定模式串DABCDABDE,我们很顺利的求得字符D之前的“DABCDAB”的各个子串的最长相同前缀后缀的长度分别为0 0 0 0 1 2 3,但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时,我们发现pj处的字符D跟pk处的字符C不一样,换言之,前缀DABC的最后一个字符C 跟后缀DABD的最后一个字符D不相同,所以不存在长度为4的相同前缀后缀。     怎么办呢?既然没有长度为4的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀,最终,因在p0处发现也有个字符D,p0 = pj,所以p[j]对应的长度值为1,相当于E对应的next 值为1(即字符E之前的字符串“DABCDABD”中有长度为1的相同前缀和后缀)。     综上,可以通过递推求得next 数组,代码如下所示:
[cpp] 
view plain
 copy
 print
?
《KMP-字符串匹配》
《KMP-字符串匹配》

  1. void GetNext(char* p,int next[])  
  2. {  
  3.     int pLen = strlen(p);  
  4.     next[0] = -1;  
  5.     int k = -1;  
  6.     int j = 0;  
  7.     while (j < pLen – 1)  
  8.     {  
  9.         //p[k]表示前缀,p[j]表示后缀  
  10.         if (k == -1 || p[j] == p[k])   
  11.         {  
  12.             ++k;  
  13.             ++j;  
  14.             next[j] = k;  
  15.         }  
  16.         else   
  17.         {  
  18.             k = next[k];  
  19.         }  
  20.     }  
  21. }  

    用代码重新计算下“ABCDABD”的next 数组,以验证之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组是否正确,计算结果如下表格所示:

《KMP-字符串匹配》



算法正确性的思考

(设定i为原来串的指针,j为模式串的指针。)

KMP的本质是减少回溯。为什么我们可以实现原来串i的不回溯?是因为我们匹配到某个位置j+1出现不匹配需要回溯的时候,我们已经获得的信息是前面j个位置已经完成了匹配,所以我们下面要做的就是在原串的i+1到i+j个位置中找到开头能够匹配的最大位置,这就是next数组的作用,因此,next数组考虑的是模式串的前缀与后缀的对称性。

KMP适用场合

   最后补充一点:由于KMP算法只预处理B串,因此这种算法很适合这样的问题:给定一个B串和一群不同的A串,问B是哪些A串的子串。

    串匹配是一个很有研究价值的问题。事实上,我们还有后缀树,自动机等很多方法,这些算法都巧妙地运用了预处理,从而可以在线性的时间里解决字符串的匹配。我们以后来说。

KMP实现



#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int len1,len2;
const int maxn=100005;
char a[maxn];
char b[maxn];
int Next[maxn];
void GetNext(){
    int pLen = strlen(b);  
    Next[0] = -1;  
    int k = -1;//开始的时候并不存在前缀  
    int j = 0;  
    while (j < pLen - 1)  
    {  //考虑到前缀和Next的延迟性,当p[j]==p[k]的时候,其实是Next[j+1]=k+1;
    	//这样做派出了头和尾的影响
        //p[k]表示前缀,p[j]表示后缀  
        if (k == -1 || b[j] == b[k])   
        {  
            ++k;  
            ++j;  
            Next[j] = k;  
        }  
        else   
        {  
            k = Next[k]; //只用前缀指针会移动
        }  
    }  
}  
int kmp(){
	GetNext();
	int i,j=0;
	while(i<len1){
		if(j==-1 || a[i]==b[j]){
			i++;
			j++;
			if(j==len2) break;
		}
		else{
			j=Next[j];
		}
	}
	if(j==len2) return i-j;//返回的位置是文本串上开始的第一个位置
	//比如ABABABDDABCABC
	//ABDDABC 返回4
	return -1;
}
int main(int argc, char const *argv[])
{
	scanf("%s",a);
	len1=strlen(a);
	scanf("%s",b);
	len2=strlen(b);
	printf("%d\n",kmp() );
	return 0;
}

塞奇威克的DFA实现


(以下内容晦涩难懂,若不理解可以跳过,内容转自豆瓣https://book.douban.com/subject/19952400/discussion/59623403/) 塞奇威克使用了一个有限状态自动机(DFA)来实现(来源:塞奇威克算法:p493)
dfa[text.charAt(i)][j]是指当文本字符串的字符s[i]与模式字符串的字符p[j]比较后下一次与文本字符串的字符s[i+1]比较的模式字符串的字符位。 


   当文本字符串的第i位字符与模式字符串的第j位进行匹配时,如果此两位字符匹配,则说明下一步应该为i++ j++之后再比较,也就是说当txt.charAt(i)==pat.charAt(j)时,有dfa[pat.charAt(j)][j]=j+1。 
   
   当文本字符串的第i位字符与模式字符串的第j位检测到不匹配时,设txt.charAt(i)==c,c!=pat.charAt(j),但文本字符串的s[i-j…i-1]这部分与模式字符串的p[0…j-1]这部分是匹配的。这时从文本字符串的i-j位置起已不可能出现匹配字符串,现在已不用管s[i-j]字符,现在的问题是依次输入s[i-j+1…i-1]c后会进入什么状态,由于s[i-j…i-1]这部分与模式字符串的p[0…j-1]这部分是匹配的,也就是说现在的问题是依次输入p[1…j-1]c后会进入什么状态。引入一个状态指示数组X,X[j]是指正确输入p[1…j-1]后进入的状态,输入p[1…j-1]c后进入的状态就是dfa[c][X[j]](在X[j]状态时输入c),即dfa[c][j]=dfa[c][x[j]]。 
   而计算X[]数组的方法为递推方法:X[j+1]为正确输入p[1…j]后进入的状态,即正确输入p[1…j-1]p[j]后进入的状态,也就是在X[j]状态时输入p[j]时进入的状态,就是dfa[pat.charAt[j]][X[j]],即递推公式为:X[j+1]=dfa[pat.charAt[j]][X[j]],而X[0]手动初始化为0。 
   由于X[]数组为辅助数据,且为递推的,所以书中仅使用了一个变量X来指示当前的X[j],我觉得理解起来很不方便,所以这里由数组代替。 
 

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/sentimental_dog/article/details/51907243
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞