朴素的字符串查找的复杂度是 O(MN) 的,显然这里有很大的优化空间,KMP 算法 (Knuth-Morris-Pratt) 可优化至 O(M+N)。
引入 next[]
注:本文使用 Python 的 slice,即 [b:e] 为左闭右开
假设比较如下字符串(上行为 S,下行为 W)
当发现 ‘D’ 与 ‘X’ 不符时,并不需要重新比较 ‘BCABX……’ 和 ‘ABCABD……’ ,而是根据 W 本身的性质,紧接着比较 ‘X……’ 和 ‘CABD……’
可视为在匹配 S 的 ‘X’ 时,比较的字符从 ‘D’(W[5]) “跳至” ‘C’(W[2])
之所以可以这样比较,是因 为 W[0:2] == W[3:5] == ‘AB’
对于 W 中的所有字符,都有这样的允许“跳至”的位置,不妨记录至 next[]数组,例如对于上例的 W,next[5] = 2
参考上文,对于 W,我们这样定义 next[] 数组
1 | next[i] = max{j | W[0:j] == W[i-j:i] for -1 <= j < i} |
next[] 的有效性
- 由 j 的 max 性质可保证查找不会遗漏匹配
- 查找匹配时,当 W[i] 不符时,跳至 W[next[i]] 继续比较即可
next[] 的推导
先进行简单考察
- 显然 next[0] = -1
- 当 next[i] = j 时(可知 W[0:j] == W[i-j:i]),考察 next[i+1]
- 若 W[i] == W[j]
则有 W[0:j+1] == W[i-j:i+1]
猜想 next[i + 1] = j + 1 - 若 W[i] != W[j],记next[j] = k
因为 W[0:k] == W[j-k:j] == W[i-k:i]
猜想问题递归为 j = k 时的小问题
- 若 W[i] == W[j]
整理易得如下递归形式:
1 | def solve_next(W): |
不难转为递推形式
1 | def solve_next(W): |
利用 next[] 即可高效查找匹配
1 | def search(W, S): |
next[] 的小优化
对于下图情况,当 W 中后 ‘A’ 不符时,根本无需再匹配前 ‘A’ ,即 next[1] = -1比next[1] = 0 更高效
针对这样的情况,我们可以对 next[] 的定义稍稍修改
1 | next[i] = max{j | W[0:j] == W[i-j:i] and W[j] != W[i] for -1 <= j < i} |
同样可先推导递归形式( max 性质同样可归纳证明,已略去)
1 | def solve_next(W): |
转为递推形式
1 | def solve_next(W): |