KMP(Knuth-Morris-Pratt)算法

  1. 云栖社区>
  2. 博客>
  3. 正文

KMP(Knuth-Morris-Pratt)算法

技术mix呢 2017-11-22 09:36:00 浏览1732
展开阅读全文

一、朴素匹配算法

也就是暴力匹配算法。设匹配字符串的长度为n,模式串的长度为m,在最坏情况下,朴字符串匹配算法执行时间为O((n - m + 1)m)。

假设m = n / 2, 那么该算法的复杂度就是Θ(n ^ 2)。因为不须要预处理。朴素字符串匹配算法执行时间即为其匹配时间。

strstr()函数就能够用这种方法实现,虽然效率不高:

//strstr函数
char *strStr(const char *str, const char *substr) {
	if (substr == NULL || str == NULL)
		return NULL;
	if (!*substr)
		return const_cast<char*>(str);
	const char *p1 = str;
	const char *p2 = substr;
	const char *p1_advance = str;
	//p1_advance指针前进strlen(substr)-1位
	//由于当str中还未匹配的位数小于substr的长度时,肯定不可能再匹配成功了
	for (p2 = substr + 1; *p2; ++p2)
		++p1_advance;

	for (p1 = str; *p1_advance; p1_advance++) {
		char *p1_old = (char *)p1;
		p2 = substr;
		while (*p1 && *p2 && *p1 == *p2) {
			++p1;
			++p2;
		}
		if (!*p2)
			return p1_old;
		p1 = p1_old + 1;
	}
	return NULL;
}

int main() {
	char str[100] = {'\0'};
	char substr[100] = {'\0'};
	scanf("%s %s", str, substr);
	if (strStr(str, substr) != NULL)
		printf("true\n");
	else
		printf("false\n");
}</span>

二、KMP算法

參考文章:http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html

July的文章把该算法讲得挺透彻了:KMP算法

       设匹配字符串的长度为n,模式串的长度为m。该算法的匹配时间为Θ(n),用到了一个辅助函数GetNext(),它在Θ(m)时间内依据模式预先计算出来,而且存储在数组next[0...m]中。模式的前缀函数GetNext包括模式与其自身的偏移进行匹配的信息。

这些信息可用于在朴素的字符串匹配算法中避免对没用的偏移进行检測。KMP利用模式串中已知的匹配信息。不再把搜索位置移动到比較过的位置(即不做没用的匹配)。这样提高了效率。

KMP完整代码例如以下:

void GetNext(char* pattern,int next[]) {  
	int k = -1;  
	int j = 0;  
	int length_pattern = strlen(pattern);  
	next[0] = -1;  
	while (j < length_pattern - 1) {  
		//p[k]表示前缀。p[j]表示后缀  
		if (k == -1 || pattern[j] == pattern[k]) {  
			++k;  
			++j;  
			next[j] = k;  
		}  
		else
			k = next[k];
	}  
}

int KmpSearch(char* text, char* pattern) {  
	int i = 0;  
	int j = 0;  
	int length_text = strlen(text);  
	int length_pattern = strlen(pattern);
	int *next = new int[length_pattern];
	GetNext(pattern, next);

	for (int i = 0; i < length_pattern; ++i) 
		cout << next[i] << " ";
	cout << endl;

	while (i < length_text && j < length_pattern) {  
		//①假设j = -1,或者当前字符匹配成功(即text[i] == pattern[j]),令i++。j++      
		if (j == -1 || text[i] == pattern[j]) {  
			++i;
			++j;
		}
		else
			//②假设j != -1,且当前字符匹配失败(即text[i] != pattern[j]),
			//则令i不变,j = next[j]。next[j]即为j所相应的next值        
			j = next[j]; 
	}  
	delete[] next;
	if (j == length_pattern)  
		return i - j;  
	else  
		return -1;  
}  

//int main() {
//	char str[100] = {'\0'};
//	char substr[100] = {'\0'};
//	scanf("%s %s", str, substr);
//	for (int i = 0 ; i < 10; ++i)
//		cout << substr[i] << " ";
//	cout << endl;
//	cout << KmpSearch(str, substr) << endl;
//}



因为须要依据自己的理解对文章内容进行标注,所以将july的文章摘录例如以下:

-----------------------下面为july文章--------------------------------

从头到尾彻底理解KMP


作者:July
时间:最初写于2011年12月,2014年7月21日晚10点 所有删除重写成此文,随后的半个多月不断重复改进。


1. 引言

    本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱。所以一直想找机会又一次写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有改动本文。

    然最近因在北京开了个算法班。专门解说数据结构、面试、算法,才再次细致回想了这个KMP,在综合了一些网友的理解、以及跟我一起讲算法的两位讲师朋友曹博、邹博的理解之后,写了9张PPT,发在微博上。

随后,一不做二不休。索性将PPT上的内容整理到了本文之中(后来文章越写越完整。所含内容早已不再是九张PPT 那样简单了)。

    KMP本身不复杂,但网上绝大部分的文章(包含本文的2011年版本号)把它讲混乱了。

以下。咱们从暴力匹配算法讲起,随后阐述KMP的流程 步骤、next 数组的简单求解 递推原理 代码求解。接着基于next 数组匹配。谈到有限状态自己主动机。next 数组的优化,KMP的时间复杂度分析。最后简要介绍两个KMP的扩展算法。

    全文力图给你一个最为完整最为清晰的KMP,希望很多其它的人不再被KMP折磨或纠缠,不再被一些混乱的文章所混乱。有何疑问。欢迎随时留言评论。thanks。


2. 暴力匹配算法

    如果如今我们面临这样一个问题:有一个文本串S。和一个模式串P,如今要查找P在S中的位置。怎么查找呢?

    如果用暴力匹配的思路,并如果如今文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:

  • 假设当前字符匹配成功(即S[i] == P[j]),则i++,j++。继续匹配下一个字符;
  • 假设失配(即S[i]! = P[j]),令i = i - (j - 1)。j = 0。

    相当于每次匹配失败时。i 回溯。j 被置为0。

    理清楚了暴力匹配算法的流程及内在的逻辑,咱们能够写出暴力匹配的代码,例如以下:
  1. int ViolentMatch(char* s, char* p)  
  2. {  
  3.     int sLen = strlen(s);  
  4.     int pLen = strlen(p);  
  5.   
  6.     int i = 0;  
  7.     int j = 0;  
  8.     while (i < sLen && j < pLen)  
  9.     {  
  10.         if (s[i] == p[j])  
  11.         {  
  12.             //①假设当前字符匹配成功(即S[i] == P[j]),则i++,j++      
  13.             i++;  
  14.             j++;  
  15.         }  
  16.         else  
  17.         {  
  18.             //②假设失配(即S[i]! = P[j]),令i = i - (j - 1)。j = 0      
  19.             i = i - j + 1;  
  20.             j = 0;  
  21.         }  
  22.     }  
  23.     //匹配成功,返回模式串p在文本串s中的位置。否则返回-1  
  24.     if (j == pLen)  
  25.         return i - j;  
  26.     else  
  27.         return -1;  
  28. }  

    举个样例。假设给定文本串S“BBC ABCDAB ABCDABCDABDE”。和模式串P“ABCDABD”,如今要拿模式串P去跟文本串S匹配,整个步骤例如以下所看到的:

    1. S[0]为B。P[0]为A,不匹配,运行第②条指令:“假设失配(即S[i]! = P[j]),令i = i - (j - 1)。j = 0”,S[1]跟P[0]匹配,相当于模式串要往右移动一位(i=1。j=0)

    2. S[1]跟P[0]还是不匹配,继续运行第②条指令:“假设失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,S[2]跟P[0]匹配(i=2,j=0),从而模式串不断的向右移动一位(不断的运行“令i = i - (j - 1)。j = 0”,i从2变到4。j一直为0)

    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)

     

    4. 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]匹配。


    而S[5]肯定跟P[0]失配。

为什么呢?由于在之前第4步匹配中,我们已经得知S[5] = P[1] = B,而P[0] = A。即P[1] != P[0],故S[5]必定不等于P[0],所以回溯过去必定会导致失配。那有没有一种算法。让i 不往回退,仅仅须要移动j 就可以呢?

    答案是肯定的。这样的算法就是本文的主旨KMP算法。它利用之前已经部分匹配这个有效信息。保持i 不回溯。通过改动j 的位置。让模式串尽量地移动到有效的位置。


3. KMP算法

3.1 定义

    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 值(next 数组的求解会在下文的3.3.3节中具体阐述)。即移动的实际位数为:j - next[j]。且此值大于等于1。
    非常快,你也会意识到next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的同样前缀后缀。比如假设next [j] = k,代表j 之前的字符串中有最大长度为k 的同样前缀后缀。
    此也意味着在某个字符失配时,该字符相应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。假设next [j] 等于0或-1。则跳到模式串的开头字符,若next [j] = k 且 k > 0。代表下次匹配跳到j 之前的某个字符,而不是跳到开头。且详细跳过了k 个字符。

    转换成代码表示,则是:
  1. int KmpSearch(char* s, char* p)  
  2. {  
  3.     int i = 0;  
  4.     int j = 0;  
  5.     int sLen = strlen(s);  
  6.     int pLen = strlen(p);  
  7.     while (i < sLen && j < pLen)  
  8.     {  
  9.         //①假设j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
  10.         if (j == -1 || s[i] == p[j])  
  11.         {  
  12.             i++;  
  13.             j++;  
  14.         }  
  15.         else  
  16.         {  
  17.             //②假设j != -1,且当前字符匹配失败(即S[i] != P[j])。则令 i 不变。j = next[j]      
  18.             //next[j]即为j所相应的next值        
  19.             j = next[j];  
  20.         }  
  21.     }  
  22.     if (j == pLen)  
  23.         return i - j;  
  24.     else  
  25.         return -1;  
  26. }  
    继续拿之前的样例来说。当S[10]跟P[6]匹配失败时,KMP不是跟暴力匹配那样简单的把模式串右移一位,而是运行第②条指令:“假设j != -1。且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,即j 从6变到2(后面我们将求得P[6]。即字符D相应的next 值为2),所以相当于模式串向右移动的位数为j - next[j](j - next[j] = 6-2 = 4)。

    向右移动4位后。S[10]跟P[2]继续匹配。为什么要向右移动4位呢。由于移动4位后,模式串中又有个“AB”能够继续跟S[8]S[9]相应着,从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找同样的前缀和后缀,然后依据前缀后缀求出next 数组,最后基于next 数组进行匹配(不关心next 数组是怎么求来的,仅仅想看匹配过程是咋样的,可直接跳到下文3.3.4节)。

3.2 步骤

  • 寻找前缀后缀最长公共元素长度
    • 对于P = p0 p1 ...pj-1 pj。寻找模式串P中长度最大且相等的前缀和后缀。假设存在p0 p1 ...pk-1 pk = pj- k pj-k+1...pj-1 pj。那么在包括pj的模式串中有最大长度为k+1的同样前缀后缀

      举个样例,假设给定的模式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度例如以下表格所看到的:

比方对于字符串aba来说。它有长度为1的同样前缀后缀a。而对于字符串abab来说,它有长度为2的同样前缀后缀ab(同样前缀后缀的长度为k + 1。k + 1 = 2)。

  • 求next数组
    • next 数组考虑的是除当前字符外的最长同样前缀后缀,所以通过第①步骤求得各个前缀后缀的公共元素的最大长度后,仅仅要稍作变形就可以:将第①步骤中求得的值总体右移一位,然后初值赋为-1,例如以下表格所看到的:

比方对于aba来说。第3个字符a之前的字符串ab中有长度为0的同样前缀后缀,所以第3个字符a相应的next值为0;而对于abab来说,第4个字符b之前的字符串aba中有长度为1的同样前缀后缀a,所以第4个字符b相应的next值为1(同样前缀后缀的长度为k,k = 1)。


  • ③依据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的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时。下一步用next [j] 处的字符继续跟文本串i 处的字符匹配,相当于模式串向右移动 j - next[j] 位。

    接下来,分别详细解释上述3个步骤。

3.3 解释

3.3.1 寻找最长前缀后缀

    假设给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别例如以下表格所看到的:
    也就是说,原模式串子串相应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):

3.3.2 基于《最大长度表》匹配

    由于模式串中首尾可能会有反复的字符。故可得出下述结论:
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所相应的最大长度值

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

        

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

  • 2.继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串须要向右移动。但向右移动多少位呢?由于此时已经匹配的字符数为6个(ABCDAB),然后依据《最大长度表》可得失配字符D的上一位字符B相应的长度值为2。所以依据之前的结论,可知须要向右移动6 - 2 = 4 位。
  • 3. 模式串向右移动4位后。发现C处再度失配,由于此时已经匹配了2个字符(AB),且上一位字符B相应的最大长度值为0,所以向右移动:2 - 0 =2 位。

           
  • 4. A与空格失配,向右移动1 位。
  • 5. 继续比較。发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B相应的最大长度2,即向右移动6 - 2 = 4 位。
           
  • 6. 经历第5步后。发现匹配成功,过程结束。

          

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

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

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

    并且。依据这个表能够得出下述结论

  • 失配时。模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所相应的最大长度值
    上文利用这个表和结论进行匹配时。我们发现。当匹配到一个字符失配时。事实上不是必需考虑当前失配的字符。更何况我们每次失配时。都是看的失配字符的上一位字符相应的最大长度值。如此,便引出了next 数组。
    给定字符串“ABCDABD”,可求得它的next 数组例如以下:

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

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


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

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

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

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

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

3.3.4 通过代码递推计算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位。

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

  • 2. 以下的问题是:已知next [0, ..., j],怎样求出next [j + 1]呢?

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

  • 若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 的同样前缀后缀。


    但假设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 。所以,咱们仅仅能去寻找长度更短一点的同样前缀后缀。

    结合上图来讲,若能在前缀“ 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]。直到要么找到长度更小的同样前缀后缀。要么没有长度更小的同样前缀后缀。

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

模式串的后缀:ABDE
模式串的前缀:ABC
前缀右移两位:     ABC

    读到此,有的读者可能又有疑问了,那是否能举一个能在前缀中找到字符D的样例呢?OK。咱们便来看一个能在前缀中找到字符D的样例。例如以下图所看到的:
    给定模式串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。
    综上,能够通过递推求得next 数组,代码例如以下所看到的:
  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 数组是否正确。计算结果例如以下表格所看到的:


    从上述表格能够看出,不管是之前通过“最长同样前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组,还是之后通过代码递推计算求得的next 数组,结果是全然一致的。


3.3.5 基于《next 数组》匹配

    以下,我们来基于next 数组进行匹配。

    还是给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,如今要拿模式串去跟文本串匹配。例如以下图所看到的:

    在正式匹配之前,让我们来再次回想下上文2.1节所述的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。
  • 1. 最開始匹配时
    • P[0]跟S[0]匹配失败
      • 所以运行“假设j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,所以j = -1,故转而运行“假设j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”,得到i = 1。j = 0,即P[0]继续跟S[1]匹配。
    • P[0]跟S[1]又失配,j再次等于-1。i、j继续自增,从而P[0]跟S[2]匹配。

    • P[0]跟S[2]失配后,P[0]又跟S[3]匹配。
    • P[0]跟S[3]再失配。直到P[0]跟S[4]匹配成功。開始运行此条指令的后半段:“假设j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++。j++”。
  • 2. P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到当匹配到P[6]处的字符D时失配(即S[10] != P[6]),因为P[6]处的D相应的next 值为2,所下面一步用P[2]处的字符C继续跟S[10]匹配,相当于向右移动:j - next[j] = 6 - 2 =4 位。

  • 3. 向右移动4位后,P[2]处的C再次失配,因为C相应的next值为0,所下面一步用P[0]处的字符继续跟S[10]匹配,相当于向右移动:j - next[j] = 2 - 0 = 2 位。

  • 4. 移动两位之后,A 跟空格不匹配,模式串后移1 位。

  • 5. P[6]处的D再次失配。由于P[6]相应的next值为2。故下一步用P[2]继续跟文本串匹配,相当于模式串向右移动 j - next[j] = 6 - 2 = 4 位。
  • 6. 匹配成功,过程结束。

    匹配过程一模一样。也从側面佐证了,next 数组确实是仅仅要将各个最大前缀后缀的公共元素的长度值右移一位。且把初值赋为-1 就可以。

3.3.6 基于《最大长度表》与基于《next 数组》等价

    我们已经知道。利用next 数组进行匹配失配时。模式串向右移动 j - next [ j ] 位,等价于已匹配字符数 - 失配字符的上一位字符所相应的最大长度值。原因是:

  1. j 从0開始计数,那么当数到失配字符时,j 的数值就是已匹配的字符数。
  2. 因为next 数组是由最大长度值表总体向右移动一位(且初值赋为-1)得到的,那么失配字符的上一位字符所相应的最大长度值。即为当前失配字符的next 值。

    但为何本文不直接利用next 数组进行匹配呢?由于next 数组不好求。而一个字符串的前缀后缀的公共元素的最大长度值非常easy求。比如若给定模式串“ababa”。要你高速口算出其next 数组,乍一看,每次求相应字符的next值时。还得把该字符排除之外,然后看该字符之前的字符串中有最大长度为多大的同样前缀后缀,此过程不够直接。而假设让你求其前缀后缀公共元素的最大长度,则非常easy直接得出结果:0 0 1 2 3。例如以下表格所看到的:

    然后这5个数字 所有总体右移一位。且初值赋为-1,即得到其next 数组:-1 0 0 1 2。

3.3.7 Next 数组与有限状态自己主动机

    next 负责把模式串向前移动,且当第j位不匹配的时候,用第next[j]位和主串匹配。就像打了张“表”。此外,next 也能够看作有限状态自己主动机的状态,在已经读了多少字符的情况下,失配后,前面读的若干个字符是实用的。


3.3.8 Next 数组的优化

   行文至此。咱们全面了解了暴力匹配的思路、KMP算法的原理、流程、流程之间的内在逻辑联系,以及next 数组的简单求解(《最大长度表》总体右移一位,然后初值赋为-1)和代码求解。最后基于《next 数组》的匹配,看似洋洋洒洒。清晰透彻,但以上忽略了一个小问题。

    比方,假设用之前的next 数组方法求模式串“abab”的next 数组。可得其next 数组为-1 0 0 1(0 0 1 2总体右移一位,初值赋为-1)。当它跟下图中的文本串去匹配的时候,发现b跟c失配,于是模式串右移j - next[j] = 3 - 1 =2位。

    右移2位后,b又跟c失配。其实。由于在上一步的匹配中。已经得知p[3] = b,与s[3] = c失配,而右移两位之后,让p[ next[3] ] = p[1] = b 再跟s[3]匹配时,必定失配。

问题出在哪呢?

   

    问题出在不该出现p[j] = p[ next[j] ]。为什么呢?理由是:当p[j] != s[i] 时,下次匹配必定是p[ next [j]] 跟s[i]匹配,假设p[j] = p[ next[j] ],必定导致后一步匹配失败(由于p[j]已经跟s[i]失配。然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,非常显然。必定失配),所以不能同意p[j] = p[ next[j ]]。假设出现了p[j] = p[ next[j] ]咋办呢?假设出现了,则须要再次递归,即令next[j] = next[ next[j] ]。

    所以。咱们得改动下求next 数组的代码。

  1. //优化过后的next 数组求法  
  2. void GetNextval(char* p, int next[])  
  3. {  
  4.     int pLen = strlen(p);  
  5.     next[0] = -1;  
  6.     int k = -1;  
  7.     int j = 0;  
  8.     while (j < pLen - 1)  
  9.     {  
  10.         //p[k]表示前缀。p[j]表示后缀    
  11.         if (k == -1 || p[j] == p[k])  
  12.         {  
  13.             ++j;  
  14.             ++k;  
  15.             //较之前next数组求法。修改在以下4行  
  16.             if (p[j] != p[k])  
  17.                 next[j] = k;   //之前仅仅有这一行  
  18.             else  
  19.                 //由于不能出现p[j] = p[ next[j ]],所以当出现时须要继续递归,k = next[k] = next[next[k]]  
  20.                 next[j] = next[k];  
  21.         }  
  22.         else  
  23.         {  
  24.             k = next[k];  
  25.         }  
  26.     }  
  27. }  

    利用优化过后的next 数组求法。可知模式串“abab”的新next数组为:-1 0 -1 0。可能有些读者会问:原始next 数组是前缀后缀最长公共元素长度值右移一位。 然后初值赋为-1而得,那么优化后的next 数组怎样高速心算出呢?实际上,仅仅要求出了原始next 数组,便能够依据原始next 数组高速求出优化后的next 数组。还是以abab为例。例如以下表格所看到的:

    

仅仅要出现了p[next[j]] = p[j]的情况,则把next[j]的值再次递归。

比如在求模式串“abab”的第2anext值时,假设是未优化的next值的话。第2a相应的next值为0,相当于第2a失配时。下一步匹配模式串会用p[0]处的a再次跟文本串匹配,必定失配。

所以求第2anext值时。须要再次递归:next[2] = next[ next[2] ] = next[0] = -1此后,依据优化后的新next值可知2a失配时,运行“假设j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++j++,继续匹配下一个字符。同理,第2b相应的next值为0

对于优化后的next数组能够发现一点:假设模式串的后缀跟前缀同样,那么它们的next值也是同样的。比如模式串abcabc。它的前缀后缀都是abc。其优化后的next数组为:-1 0 0 -1 0 0,前缀后缀abcnext值都为-1 0 0

    然后引用下之前3.1节的KMP代码:

  1. int KmpSearch(char* s, char* p)  
  2. {  
  3.     int i = 0;  
  4.     int j = 0;  
  5.     int sLen = strlen(s);  
  6.     int pLen = strlen(p);  
  7.     while (i < sLen && j < pLen)  
  8.     {  
  9.         //①假设j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
  10.         if (j == -1 || s[i] == p[j])  
  11.         {  
  12.             i++;  
  13.             j++;  
  14.         }  
  15.         else  
  16.         {  
  17.             //②假设j != -1,且当前字符匹配失败(即S[i] != P[j])。则令 i 不变。j = next[j]      
  18.             //next[j]即为j所相应的next值        
  19.             j = next[j];  
  20.         }  
  21.     }  
  22.     if (j == pLen)  
  23.         return i - j;  
  24.     else  
  25.         return -1;  
  26. }  

    接下来。咱们继续拿之前的样例说明。整个匹配步骤例如以下:

    1. S[3]与P[3]匹配失败。

    2. S[3]保持不变。P的下一个匹配位置是P[next[3]]。而next[3]=0,所以P[next[3]]=P[0]与S[3]匹配。


    3.  因为上一步骤中P[0]与S[3]还是不匹配。此时i=3,j=next [0]=-1,因为满足条件j==-1。所以运行“++i, ++j”,即主串指针下移一个位置。P[0]与S[4]開始匹配。

最后j==pLen,跳出循环,输出结果i - j = 4(即模式串第一次在文本串中出现的位置),匹配成功。算法结束。

   

3.4 KMP的时间复杂度分析

    相信大部分读者读完上文之后,已经发觉事实上理解KMP很easy,无非是循序渐进把握好以下几点:
  1. 假设模式串中存在同样前缀和后缀。即pj-k pj-k+1, ..., pj-1 = p0 p1, ..., pk-1,那么在pj跟si失配后,让模式串的前缀p0 p1...pk-1相应着文本串si-k si-k+1...si-1,而后让pk跟si继续匹配。
  2. 之前本应是pj跟si匹配,结果失配了。失配后,令pk跟si匹配,相当于j 变成了k,模式串向右移动j - k位。
  3. 由于k 的值是可变的,所以我们用next[j]表示j处字符失配后,下一次匹配模式串应该跳到的位置。换言之。失配前是j,pj跟si失配时,用p[ next[j] ]继续跟si匹配,相当于j变成了next[j]。所以,j = next[j]。等价于把模式串向右移动j - next [j] 位。
  4. 而next[j]应该等于多少呢?next[j]的值由j 之前的模式串子串中有多大长度的同样前缀后缀所决定,假设j 之前的模式串子串中(不含j)有最大长度为k的同样前缀后缀,那么next [j] = k。
    如之前的图所看到的:

    接下来。咱们来分析下KMP的时间复杂度。分析之前,先来回想下KMP匹配算法的流程:

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] 位。

    我们发现假设某个字符匹配成功。模式串首字符的位置保持不动。不过i++、j++。假设匹配失配,i 不变(即 i 不回溯),模式串会跳过匹配过的next [j]个字符。整个算法最坏的情况是,当模式串首字符位于i - j的位置时才匹配成功,算法结束。


    所以,假设文本串的长度为n。模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间。KMP的总体时间复杂度为O(m + n)。


4. 扩展1:BM算法

    KMP的匹配是从模式串的开头開始匹配的,而1977年。德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。

该算法从模式串的尾部開始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。

    BM算法定义了两个规则:

  • 坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时。我们称文本串中的这个失配字符为坏字符,此时模式串须要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。

    此外,假设"坏字符"不包括在模式串之中,则最右出现位置为-1。

  • 好后缀规则:当字符失配时。后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且假设好后缀在模式串中没有再次出现,则为-1。

    以下举例说明BM算法。比如。给定文本串“HERE IS A SIMPLE EXAMPLE”,和模式串“EXAMPLE”,现要查找模式串是否在文本串中,假设存在,返回模式串在文本串中的位置。

    1. 首先,"文本串"与"模式串"头部对齐。从尾部開始比較。"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符,它相应着模式串的第6位。且"S"不包括在模式串"EXAMPLE"之中(相当于最右出现位置是-1)。这意味着能够把模式串后移6-(-1)=7位。从而直接移到"S"的后一位。


    2. 依旧从尾部開始比較,发现"P"与"E"不匹配,所以"P"是"坏字符"。可是,"P"包括在模式串"EXAMPLE"之中。

由于“P”这个“坏字符”相应着模式串的第6位(从0開始编号),且在模式串中的最右出现位置为4,所以。将模式串后移6-4=2位。两个"P"对齐。


    3. 依次比較。得到 “MPLE”匹配。称为"好后缀"(good suffix),即全部尾部匹配的字符串。

注意。"MPLE"、"PLE"、"LE"、"E"都是好后缀。

    4. 发现“I”与“A”不匹配:“I”是坏字符。

假设是依据坏字符规则,此时模式串应该后移2-(-1)=3位。问题是,有没有更优的移法?

    5. 更优的移法是利用好后缀规则:当字符失配时。后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置,且假设好后缀在模式串中没有再次出现。则为-1。
    全部的“好后缀”(MPLE、PLE、LE、E)之中,仅仅有“E”在“EXAMPLE”的头部出现,所以后移6-0=6位。
    能够看出。“坏字符规则”仅仅能移3位,“好后缀规则”能够移6位。每次后移这两个规则之中的较大值。

这两个规则的移动位数。仅仅与模式串有关,与原文本串无关。

    6. 继续从尾部開始比較。“P”与“E”不匹配。因此“P”是“坏字符”。依据“坏字符规则”。后移 6 - 4 = 2位。

由于是最后一位就失配,尚未获得好后缀。

    由上可知,BM算法不仅效率高,并且构思巧妙。easy理解。


5. 扩展2:Sunday算法

    上文中,我们已经介绍了KMP算法和BM算法,这两个算法在最坏情况下均具有线性的查找时间。

但实际上。KMP算法并不比最简单的c库函数strstr()快多少,而BM算法尽管通常比KMP算法快,但BM算法也还不是现有字符串查找算法中最快的算法。本文最后再介绍一种比BM算法更快的查找算法即Sunday算法。

    Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法非常相似:

  • 仅仅只是Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中參加匹配的最末位字符的下一位字符。

    • 假设该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
    • 否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1。

    以下举个样例说明下Sunday算法。假定如今要在文本串"substring searching algorithm"中查找模式串"search"。

    1. 刚開始时,把模式串与文本串左边对齐:
substring searching algorithm
search
^
    2. 结果发如今第2个字符处发现不匹配。不匹配时关注文本串中參加匹配的最末位字符的下一位字符,即标粗的字符 i,由于模式串search中并不存在i,所以模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7。从 i 之后的那个字符(即字符n)開始下一步的匹配,例如以下图:

substring searching algorithm
    search
    ^
    3. 结果第一个字符就不匹配,再看文本串中參加匹配的最末位字符的下一位字符,是'r',它出如今模式串中的倒数第3位。于是把模式串向右移动3位(r 到模式串末尾的距离 + 1 = 2 + 1 =3)。使两个'r'对齐,例如以下:
substring searching algorithm
      search
       ^

    4. 匹配成功。

    回想整个过程,我们仅仅移动了两次模式串就找到了匹配位置,缘于Sunday算法每一步的移动量都比較大。效率非常高。

完。


6. 參考文献

  1. 《算法导论》的第十二章:字符串匹配;
  2. 本文中模式串“ABCDABD”的部分图来自于此文:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
  3. 本文3.3.7节中有限状态自己主动机的图由微博网友@龚陆安 绘制:http://d.pr/i/NEiz
  4. 北京7月暑假班邹博半小时KMP视频:http://www.julyedu.com/video/play/id/5
  5. 北京7月暑假班邹博第二次课的PPT:http://yun.baidu.com/s/1mgFmw7u
  6. 理解KMP 的9张PPT:http://weibo.com/1580904460/BeCCYrKz3#_rnd1405957424876
  7. 具体解释KMP算法(多图):http://www.cnblogs.com/yjiyjige/p/3263858.html
  8. 本文第4部分的BM算法參考自此文:http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
  9. http://youlvconglin.blog.163.com/blog/static/5232042010530101020857
  10. 《数据结构 第二版》,严蔚敏 & 吴伟民编著;
  11. http://blog.csdn.net/v_JULY_v/article/details/6545192
  12. http://blog.csdn.net/v_JULY_v/article/details/6111565
  13. Sunday算法的原理与实现:http://blog.chinaunix.net/uid-22237530-id-1781825.html
  14. 模式匹配之Sunday算法:http://blog.csdn.net/sunnianzhong/article/details/8820123
  15. 一篇KMP的英文介绍:http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm
  16. 我2014年9月3日在西安电子科技大学的面试&算法讲座视频(第36分钟~第94分钟讲KMP):http://www.julyedu.com/video/play/id/7


7. 后记    

    对之前混乱的文章给广大读者带来的困扰表示致歉,对又一次写就后的本文即将给读者带来的清晰表示欣慰。希望大部分的刚開始学习的人,甚至少部分的非计算机专业读者也能看懂此文。有不论什么问题。欢迎随时批评指正,thanks。

    July、二零一四年八月二十二日晚九点。












本文转自mfrbuaa博客园博客,原文链接:http://www.cnblogs.com/mfrbuaa/p/5125044.html,如需转载请自行联系原作者

网友评论

登录后评论
0/500
评论
技术mix呢
+ 关注