KMP算法

1.字符串匹配

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

  举个例子,如果给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配,整个过程如下所示:

  1.S[0]BP[0]A,不匹配,执行第②条指令:如果失配(即S[i]! = P[j]),令i = i - (j - 1)j = 0”S[1]P[0]匹配,相当于模式串要往右移动一位(i=1j=0

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

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

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

  5.直到S[10]为空格字符,P[6]为字符Di=10j=6),因为不匹配,重新执行第②条指令:如果失配(即S[i]! = P[j]),令i = i - (j - 1)j = 0”,相当于S[5]P[0]匹配(i=5j=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],所以回溯过去必然会导致失配。

2.KMP算法

  Knuth-Morris-Pratt 字符串查找算法,简称为 “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] 位。

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

  1.当S[10]跟P[6]匹配失败时,KMP不是跟暴力匹配那样简单的把模式串右移一位,而是执行第②条指令:“如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,即j 从6变到2,所以相当于模式串向右移动的位数为j - next[j](j - next[j] = 6-2 = 4)。

  2.向右移动4位后,S[10]跟P[2]继续匹配。为什么要向右移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟S[8]S[9]对应着,从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配。

  步骤:

  1.寻找前缀后缀最长公共元素长度

  对于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)。

  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)。

  3.根据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.最大长度表

  如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:

  也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为

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

  如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

  1.因为模式串中的字符A跟文本串中的字符BBC、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符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.继续比较,发现DC 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。

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

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

4.next 数组

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

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

  我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。

  给定字符串“ABCDABD”,可求得它的next 数组如下:

 

  next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于最大长度值整体向右移动一位,然后初始值赋为-1

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

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

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

  给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

  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 = 1j = 0,即P[0]继续跟S[1]匹配。

  • P[0]S[1]又失配,j再次等于-1ij继续自增,从而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++”

  2P[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 位。

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

  6匹配成功,过程结束。

  next 数组只要将各个最大前缀后缀的公共元素的长度值右移一位,且把初值赋为-1 即可。

5.next 数组的优化

  用之前的next 数组方法求模式串“abab”next 数组,可得其next 数组为-1 0 0 10 0 1 2整体右移一位,初值赋为-1),当它跟下图中的文本串去匹配的时候,发现bc失配,于是模式串右移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] != 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] ]

  只要出现了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

  整个匹配过程如下:

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

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

  3由于上一步骤中P[0]S[3]还是不匹配。此时i=3j=next [0]=-1,由于满足条件j==-1,所以执行“++i, ++j”,即主串指针下移一个位置,P[0]S[4]开始匹配。最后j==pLen,跳出循环,输出结果i - j = 4(即模式串第一次在文本串中出现的位置),匹配成功,算法结束。

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

posted @ 2015-01-22 00:01  iak  阅读(252)  评论(0编辑  收藏  举报