KMP(Knuth-Morris-Pratt) #
Question #
如果有 s 串 abcazdabcabc
和 P 串 abcab
两个字符串,求 p 串第一次出现在 s 串的位置。
暴力法 #
以朴素或者说暴力的方式解决上面的问题,应该是使用双重循环:
for i := range s {
si := i
for j := range p {
if s[si] != p[j] {
break
}
si++
}
if si - i == len(p){
// 匹配成功 break 或 return
}
}
外层循环遍历 s 串的每个字符,内层循环遍历 p 串的每个字符是否和 s 串位置依次相同,直到匹配成功或循环结束即可得到所需的位置。
不过当 s、p 串的长度变长之后,该算法的复杂度最坏会上升到 len(s)*len(p)
,即 n2,那么可以减小时间复杂度吗?
尝试优化 #
想要减小时间复杂度,那就要找出该算法中,是否有无效的计算。
(轮次 1)
0 1 2 3 4 5 6 7 8 9
+ a b c a z a b c a b c
- a b c a b
^
当计算上面的位置时,发现 s 和 p 不一致,校验失败,此时进入新的轮次:
(轮次 2)
+ a b c a z a b c a b c
- a b c a b
^
(轮次 3)
+ a b c a z a b c a b c
- a b c a b
^
(轮次 4)
+ a b c a z a b c a b c
- a b c a b
^
(轮次 5)
+ a b c a z a b c a b c
- a b c a b
^
(轮次 6)......
此时 p 串依次后移,从 s 串次位进行对比。
仔细观察,在已知 轮次 1 在 p4 对比时失败,接着进行了 轮次 2、3 且 s 串首位都不是 p0 a
。
这里就可以做优化了,原因如下:
轮次 1 失败后得知 s 串前四位和 p 串前四位一致,即:a b c a
,那么如果要保证下一轮次 s 串首位是 a
,至少要从 s 串的下一个 a
字符开始比较。正好我们知道:
- s0 = p0
- s1 = p1
- s2 = p2
- s3 = p3
- p3 为
a
所以我们就找到了 s 串的下一个 a
,直接跳过 轮次 2、3,进入 轮次 4。
分析 #
匹配失败后,获得了一个 s、p 串部分匹配的前缀,如何利用这个前缀将 p 串尽可能最大化移动到 s 串的末端是优化的关键。
如何得知最大移动位数呢,再来看 轮次 1 中 s、p 串相同的前缀 abca
,这个前缀串的前缀和后缀有公共相同的子串 a
。p5 配对失败后,将 p 移动 3 位(公共串长度 - 公共串的最大公共前后缀长度)。
我们将公共串 abca
假设成任何其他的串,例如 a b c a s d c a b ?
:
0 1 2 3 4 5 6 7 8
+ ? a b c a s c a b * ?
- a b c a s c a b ? ?
^
如果此时匹配到 p8( 第一个 ?
)不相同,此时公共前缀为 abcascab
,那么要移动的距离就是串 p0-8 的公共前后缀,即 ab
。所以最后问题的关键就落在了如何求出 p 的子串对应的公共前后缀长度问题。
部分匹配表(Partial Match Table) #
概念
前缀 和 后缀:前缀 指除了最后一个字符以外,一个字符串的全部头部组合;后缀 指除了第一个字符以外,一个字符串的全部尾部组合。
例如对于串 abcdcba
前后缀
a
:无;无ab
:a
;b
abc
:a
、ab
;c
、cb
abcd
:a
、ab
、abc
;d
、dc
、dcb
abcdc
:a
、ab
、abc
、abcd
;c
、cd
、cdc
、cdcb
abcdcb
:a
、ab
、abc
、abcd
、abcdc
;b
、bc
、bcd
、bcdc
、bcdcb
abcdcba
:a
、ab
、abc
、abcd
、abcdc
、abcdcb
;a
、ab
、abc
、abcd
、abcdc
、abcdcb
这样就能计算出部分匹配表
0 1 2 3 4 5 6
+ a b c d c b a
- 0 0 0 0 0 0 4
那么如何以算法的形式计算出来呢?可以使用动态规划:
获取当前位前一位的串的部分匹配值,移动到匹配值的位置,如果匹配值的位置的下一位和当前位一致,则当前位的匹配值加一,否则为 0