KMP算法
¶算法思想
¶暴力优化
给定主串S和模式串P:
S:BBC ABCDAB ABCDABCDABDE
P:ABCDABD
暴力算法就是一个一个地匹配,失配了就模式串P整体后移一位,从头开始与主串S的失配字符的下一个字符开始匹配.
如图:
暴力算法做了太多无用功,如下图所示:
D
与空格失配后,失配字符前的ABCDAB
字串显然是配对的,换言之我们已经知道失配字符前的所有字符,那我们可以根据这些已知字符来跳过一些不可能的情况.
如上图,失配字符D
之前的字串ABCDAB
具有相同且最大的前后缀AB
,故我们可以直接跳转使得失配字符前的字串的前缀(即模式串的前缀)与后缀(即主串的后缀)相对应.
也就是说,我们从头到尾假设每个字符失配,得到失配字符前的字串中相同的前缀和后缀,并记录下来,即可省略一大批步骤.那么怎么得到这个相同前后缀呢?
¶KMP步骤
- 寻找前后缀最长公共元素长度
- 对于字串
P = P[0] P[1] P[2] ... P[j-1] P[j]
,找其最长公共前后缀. - 如果有
P[0] P[1] ... P[k] = P[j-k] P[j-k+1] ... P[j]
即某条公共前后缀,那么对于一条包含这个公共前后缀的字串T来说,其最大公共前后缀长度是k+1,当然也可能是0… - 举例说明:
- 对于字串
- 求next数组
- 为了更好进行计算机计算,引入next数组,即失配发生后模式串P需要移动到哪个位置.
- 其值是将第一步中的最大公共元素长度整体右移一位,并令初值为-1.
- 根据next数组进行匹配
- 匹配失败,令j = next[j].
- 即当模式串字串
P[0] P[1] ... P[j-1]
与主串字串... S[i-1]
匹配失败,i与j匹配失败时, next[j] = k, 故将k移动到j位置, 让P[\k]与S[i]继续匹配.
- 匹配失败,令j = next[j].
¶机算next数组
- 对于值k,已经有
P[0] P[1] ... P[k-1]
与P[j-k] P[j-k+1] ... P[j-1]
,即next[j] = k. - 怎么求next[j+1]?
- 若P[k] == P[j], 那么公共长度可以加一, 即 next[j+1] = k + 1.
- 若P[k] ≠ P[j]:
- 若P[next[k]] == P[j], 则next[j+1] = next[k] + 1.
- 若P[next[k]] ≠ P[j], 继续递归前缀索引k = next[k], 重复此过程知道条件成立. 这个过程相当于: 在字符P[j+1]前不存在长度k+1的前后缀相同, 那么是否有某个长度更小的前后缀相同呢?
如果存在, 那么为什么是next[k]呢?
- 递归解释
- 如下图所示,已知next[k] = 2(直接看出),那么因为P[k] == P[j], 故next[k+1] = 2 + 1 = 3.
- 再如下图,P[k] ≠ P[j], 显然因为C不同于D, 所以ABC 跟 ABD不相同, 即字符E前的模式串没有长度为k+1的相同前缀后缀, 也就不能再简单的令:next[j + 1] = next[j] + 1 .所以,咱们只能去寻找长度更短一点的相同前缀后缀.
- 那么为何要递归呢? 其实就是C与D不同, 但其前面相同(AB == AB, 这里假设是 ABAB = ABAB, 即 ABABC 与 ABABD 进行匹配), 那么只能是前缀AB(A)与后缀AB(D)匹配, 故要递归向前找长度更小的前缀.
- 如下图所示,已知next[k] = 2(直接看出),那么因为P[k] == P[j], 故next[k+1] = 2 + 1 = 3.
- 代码:
1 | void getNext(char *P,int next[]) |
¶next数组优化(nextval)
如下图,b和c失配后,模式串右移2,b和c又失配,这显然是算法缺陷.
问题出在不该出现P[j] = P[next[j]]
代码:
1 | void getNextVal(char *P,int next[]) |
¶手算next数组
考研不需要机算,故使用如下方法,在失配字符前画一条线,然后一步一步后退直到匹配或者过线.
串的下标从1开始.
下标0的位置是留给-1的,只要在下标0的位置加上0就是以-1开头,串下标从0开始计算的next数组.
计算nextval数组:
1 | for j from 2 to end: |
因为我们只利用了失配元素之前的元素, 但其实失配元素也能利用起来, 也就是说失配元素失配后, 再下次跳转匹配时若还是相同元素就不可能匹配成功.
即当前元素若失配, 跳转到next对应元素, 若还是相同元素, 就应该继续跳到更短的公共前后缀对应位置.
通俗说就是在当前元素(从2到最后), 如果其next值对应的元素与当前元素相等, 就令当前next值等于对应元素的next值, 重复直到不相等(如当前坐标11,对应next=5,坐标5还是a,继续跳到坐标3依然是a,直到跳到坐标0不相等,所以next[11]=0).