KMP
KMP
KMP 解决的问题与朴素模式匹配相同:在主串中查找模式串第一次出现的位置。区别在于:KMP 发生失配时,主串指针 i 不回退,只根据模式串自身结构调整模式串指针 j。
核心思想
当 S[i] != T[j] 时,T[1..j-1] 已经与主串中的一段字符完全相同。KMP 利用这段“已知匹配信息”,判断模式串应该滑到哪里继续比较。
next[j] 表示:当 T[j] 与 S[i] 失配,且 T[1..j-1] 已经匹配时,模式串指针 j 应退到的位置。
next[1] = 0:第一个字符失配,说明当前主串字符无法匹配模式串开头;下一步应移动主串指针。next[2] = 1:第二个字符失配时,尝试让模式串第一个字符与当前主串字符比较。其他
next[j]的计算方法:- 在失配位置前画一条分界线。
- 只看分界线之前已经匹配的部分。
- 让模式串一步一步后退,直到分界线之前模式串部分能与相应位置的主串部分还能对上,或分界线前的模式串部分为。至少后退一次。
- 此时
j指向的位置,就是next[j]。
1
2
3
4
5abaab |
abaab | c j = 6
abaa | b j = 5 (abaa != baab)
aba | a j = 4 (aba != aab)
ab | a j = 3 (ab == ab, success!)
代码实现
next 数组计算
1 |
|
KMP 匹配
1 | int KMP_Index(SString s, SString t, int next[]) { |
j == 0 的含义:模式串第一个字符也无法匹配当前主串字符,需要让主串指针前进,并让 j 回到 1。
复杂度
对于空间复杂度,保存 next 数组需要 的额外空间。
对于时间复杂度,设主串长度为 ,模式串长度为 。KMP 的时间包括构造 next 和匹配两部分。
构造 next 需要遍历整个模式串,因此为 。
在匹配阶段,时间由主串指针 i 的移动决定,而主串指针 i 只向后移动,不会因失配而回退。 因此,主串最多被从头到尾扫描一遍,所以匹配阶段时间复杂度最坏为 。
因此,KMP 的时间复杂度为:
优化
若 next[j] = k 且 T[j] == T[k],那么当 T[j] 与当前主串字符失配时,当前主串字符一定也不等于 T[k]。此时再退到 k 比较一次,必然再次失配。
因此可以直接跳到 next[k],跳过这次必失配的比较。称这个优化后的 next 数组为 nextval。
以下代码展示在 next 数组计算出来后再进行优化的写法。当然可以在计算 next 的同时就进行优化。
1 | void GetNextval(SString t, int next[], int nextval[]) { |
nextval 在 kmp 算法中使用方式与 next 数组一样。
它优化的是常数级比较次数,不改变 KMP 的总体时间复杂度 。
[! exmaple]
- 例 1:
ababaa
j 1 2 3 4 5 6 T[j] a b a b a a next[j] 0 1 1 2 3 4 nextval[j] 0 1 0 1 0 4
j=3时,T[3]=a,next[3]=1,且T[1]=a,退到 1 必然失配,所以nextval[3]=nextval[1]=0。j=6时,T[6]=a,next[6]=4,T[4]=b,不同,保留 4。
- 例 2:
abaabc
j 1 2 3 4 5 6 T[j] a b a a b c next[j] 0 1 1 2 2 3 nextval[j] 0 1 0 2 1 3
j=3时,T[3]=a,next[3]=1,T[1]=a,退到 1 必然失配,所以nextval[3]=0。j=5时,T[5]=b,next[5]=2,T[2]=b,退到 2 必然失配,所以继续退到nextval[2]=1。j=4、j=6退回位置的字符不同,保留原next值。
- 例 3:
aaaab
j 1 2 3 4 5 T[j] a a a a b next[j] 0 1 2 3 4 nextval[j] 0 0 0 0 4 连续相同字符会让普通
next产生多次冗余回退,nextval会把这些回退压缩掉。