版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处连接及本声明。
原文连接:https://blog.csdn.net/qq_37969433/article/details/82947411html
本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱致使写也写得混乱。因此一直想找机会从新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文。面试
KMP自己不复杂,但网上绝大部分的文章(包括本文的2011年版本)把它讲混乱了。下面,我们从暴力匹配算法讲起,随后阐述KMP的流程 步骤、next 数组的简单求解 递推原理 代码求解,接着基于next 数组匹配,谈到有限状态自动机,next 数组的优化,KMP的时间复杂度分析,最后简要介绍两个KMP的扩展算法。算法
全文力图给你一个最为完整最为清晰的KMP,但愿更多的人再也不被KMP折磨或纠缠,再也不被一些混乱的文章所混乱。有何疑问,欢迎随时留言评论,thanks。数组
假设如今咱们面临这样一个问题:有一个文本串S,和一个模式串P,如今要查找P在S中的位置,怎么查找呢?数据结构
若是用暴力匹配的思路,并假设如今文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:ide
int ViolentMatch(char* s, char* p) { int sLen = strlen(s); int pLen = strlen(p); int i = 0; int j = 0; while (i < sLen && j < pLen) { if (s[i] == p[j]) { //①若是当前字符匹配成功(即S[i] == P[j]),则i++,j++ i++; j++; } else { //②若是失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0 i = i - j + 1; j = 0; } } //匹配成功,返回模式串p在文本串s中的位置,不然返回-1 if (j == pLen) return i - j; else return -1; }
举个例子,若是给定文本串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)ui
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)spa
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 的位置,让模式串尽可能地移动到有效的位置。
int KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); while (i < sLen && j < pLen) { //①若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++ if (j == -1 || s[i] == p[j]) { i++; j++; } else { //②若是j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j] //next[j]即为j所对应的next值 j = next[j]; } } if (j == pLen) return i - j; else return -1; }
好比对于字符串aba来讲,它有长度为1的相同前缀后缀a;而对于字符串abab来
好比对于aba来讲,第3个字符a以前的字符串ab中有长度为0的相同前缀后缀,因此第3个字符a对应的next值为0;而对于abab来讲,第4个字符b以前的字符串aba中有长度为1的相同前缀后缀a,因此第4个字符b对应的next值为1(相同前缀后缀的长度为k,k = 1)。
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值 |
下面,我们就结合以前的《最大长度表》和上述结论,进行字符串的匹配。若是给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,如今要拿模式串去跟文本串匹配,以下图所示:
经过上述匹配过程能够看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每一个字符以前的前缀和后缀公共部分的最大长度后,即可基于此匹配。而这个最大长度便正是next 数组要表达的含义。
由上文,咱们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
并且,根据这个表能够得出下述结论
把next 数组跟以前求得的最大长度表对比后,不难发现,next 数组至关于“最大长度值” 总体向右移动一位,而后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解居然如此简单:就是找最大对称长度的前缀后缀,而后总体右移一位,初值赋为-1(固然,你也能够直接计算某个字符对应的next值,就是看这个字符以前的字符串中有多大长度的相同前缀后缀)。
换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别以下:
根据最大长度表求出了next 数组后,从而有
失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值 |
然后,你会发现,不管是基于《最大长度表》的匹配,仍是基于next 数组的匹配,二者得出来的向右移动的位数是同样的。为何呢?由于:
因此,你能够把《最大长度表》看作是next 数组的雏形,甚至就把它当作next 数组也是能够的,区别不过是怎么用的问题。
接下来,我们来写代码求下next 数组。
基于以前的理解,可知计算next 数组的方法能够采用递推:
举个例子,以下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,表明字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串须要向右移动j - next [j] = 6 - 2 =4位。
向右移动4位后,模式串中的字符C继续跟文本串匹配。
对于P的前j+1个序列字符:
void GetNext(char* p,int next[]) { int pLen = strlen(p); next[0] = -1; int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 if (k == -1 || p[j] == p[k]) { ++k; ++j; next[j] = k; } else { k = next[k]; } } }
用代码从新计算下“ABCDABD”的next 数组,以验证以前经过“最长相同前缀后缀长度值右移一位,而后初值赋为-1”获得的next 数组是否正确,计算结果以下表格所示:
从上述表格能够看出,不管是以前经过“最长相同前缀后缀长度值右移一位,而后初值赋为-1”获得的next 数组,仍是以后经过代码递推计算求得的next 数组,结果是彻底一致的。
下面,咱们来基于next 数组进行匹配。
仍是给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,如今要拿模式串去跟文本串匹配,以下图所示:
在正式匹配以前,让咱们来再次回顾下上文2.1节所述的KMP算法的匹配流程:
匹配过程如出一辙。也从侧面佐证了,next 数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位,且把初值赋为-1 便可。
咱们已经知道,利用next 数组进行匹配失配时,模式串向右移动 j - next [ j ] 位,等价于已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。缘由是:
但为什么本文不直接利用next 数组进行匹配呢?由于next 数组很差求,而一个字符串的前缀后缀的公共元素的最大长度值很容易求。例如若给定模式串“ababa”,要你快速口算出其next 数组,乍一看,每次求对应字符的next值时,还得把该字符排除以外,而后看该字符以前的字符串中有最大长度为多大的相同前缀后缀,此过程不够直接。而若是让你求其前缀后缀公共元素的最大长度,则很容易直接得出结果:0 0 1 2 3,以下表格所示:
而后这5个数字 所有总体右移一位,且初值赋为-1,即获得其next 数组:-1 0 0 1 2。
next 负责把模式串向前移动,且当第j位不匹配的时候,用第next[j]位和主串匹配,就像打了张“表”。此外,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 数组的代码。
//优化事后的next 数组求法 void GetNextval(char* p, int next[]) { int pLen = strlen(p); next[0] = -1; int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 if (k == -1 || p[j] == p[k]) { ++j; ++k; //较以前next数组求法,改动在下面4行 if (p[j] != p[k]) next[j] = k; //以前只有这一行 else //由于不能出现p[j] = p[ next[j ]],因此当出现时须要继续递归,k = next[k] = next[next[k]] next[j] = next[k]; } else { k = next[k]; } } }
利用优化事后的next 数组求法,可知模式串“abab”的新next数组为:-1 0 -1 0。可能有些读者会问:原始next 数组是前缀后缀最长公共元素长度值右移一位, 而后初值赋为-1而得,那么优化后的next 数组如何快速心算出呢?实际上,只要求出了原始next 数组,即可以根据原始next 数组快速求出优化后的next 数组。仍是以abab为例,以下表格所示:
只要出现了p[next[j]] = p[j]的状况,则把next[j]的值再次递归。例如在求模式串“abab”的第2个a的next值时,若是是未优化的next值的话,第2个a对应的next值为0,至关于第2个a失配时,下一步匹配模式串会用p[0]处的a再次跟文本串匹配,必然失配。因此求第2个a的next值时,须要再次递归:next[2] = next[ next[2] ] = next[0] = -1(此后,根据优化后的新next值可知,第2个a失配时,执行“若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符”),同理,第2个b对应的next值为0。
对于优化后的next数组能够发现一点:若是模式串的后缀跟前缀相同,那么它们的next值也是相同的,例如模式串abcabc,它的前缀后缀都是abc,其优化后的next数组为:-1 0 0 -1 0 0,前缀后缀abc的next值都为-1 0 0。
而后引用下以前3.1节的KMP代码:
nt KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); while (i < sLen && j < pLen) { //①若是j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++ if (j == -1 || s[i] == p[j]) { i++; j++; } else { //②若是j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j] //next[j]即为j所对应的next值 j = next[j]; } } if (j == pLen) return i - j; else return -1; }
接下来,我们继续拿以前的例子说明,整个匹配过程以下:
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(即模式串第一次在文本串中出现的位置),匹配成功,算法结束。
“KMP的算法流程:
咱们发现若是某个字符匹配成功,模式串首字符的位置保持不动,仅仅是i++、j++;若是匹配失配,i 不变(即 i 不回溯),模式串会跳过匹配过的next [j]个字符。整个算法最坏的状况是,当模式串首字符位于i - j的位置时才匹配成功,算法结束。
因此,若是文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的总体时间复杂度为O(m + n)。
KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的尾部开始匹配,且拥有在最坏状况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。
BM算法定义了两个规则:
下面举例说明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算法不只效率高,并且构思巧妙,容易理解。
上文中,咱们已经介绍了KMP算法和BM算法,这两个算法在最坏状况下均具备线性的查找时间。但实际上,KMP算法并不比最简单的c库函数strstr()快多少,而BM算法虽然一般比KMP算法快,但BM算法也还不是现有字符串查找算法中最快的算法,本文最后再介绍一种比BM算法更快的查找算法即Sunday算法。
Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很类似:
下面举个例子说明下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算法每一步的移动量都比较大,效率很高。完。
对以前混乱的文章给广大读者带来的困扰表示致歉,对从新写就后的本文即将给读者带来的清晰表示欣慰。但愿大部分的初学者,甚至少部分的非计算机专业读者也能看懂此文。有任何问题,欢迎随时批评指正,thanks。
July、二零一四年八月二十二日晚九点。