这里简单介绍 KMP 算法及其实现思路。KMP 算法实现了在线性时间内完成子串的查找,下面首先介绍基本的匹配算法,在考虑如何使用 KMP 的思路加快匹配。
基本的匹配算法
基本的思路,就是使用两个指针分别指向字符串 A
和待查找字符串 B
,然后比较两个指针处的字符是否相等,如果相等就同时推进两个指针,否则回退两个指针 ( 具体来说,两个指针都先回退 B 指针移动的距离
,此时,B
指针回到索引为 0
的字符,然后 A
指针再向前一格,重新匹配 )。遍历完 A
字符串后,如果 B
指针值为 B
的长度,说明得到匹配,否则失配。
1 | function match(A, B) { |
举个例子说明一下回退,其实就是先重置 i
, j
,再将 i
向前移一格:
1 | A = ababd, B = abd |
KMP 算法
KMP 加快匹配的方式是,使 i
不会回退,只改变 j
的位置。下面举个例子:
1 | A = abababc, B = ababc |
上面最后 ^
标记出已经匹配的部分,|
代表现在正在匹配的位置。
拿上面的例子来说,已知失配点前是匹配的,因此,失配点 j = 4
前一定是 abab
,不管是 A 还是 B,说明 abababc
中 i = 4
前一定是 abab
。
1 | [abab]abc i = 4 |
因此,这里可以直接利用失配点前的信息是已知的这一点就可以加快匹配。
例如对于 ababc
,如果在 j = 2
失配,那失配点前内容为 ab
;如果在 j = 3
失配,那失配点前内容为 aba
。而且注意,这里只靠 B 自己就可以知道失配点前的内容了。
因此,对于 ababc
,如果失配点是 j = 4
,说明失配点前是 abab
,又其后缀是 ab
,和 ababc
的前缀匹配,因此此时只需把 j
移动到 j = 2
处即可,省略了 ab == ab
的匹配。
据此,即可实现 i 指针不动,只移动 j 指针:
1 | A = abababc, B = ababc |
由上面可知,j
的跳转只取决于 B
本身,所以,对于一个给定的 j
值,其下一跳是固定的,使用一个 next
数组指明 j
的下一跳,即:j = next[j]
。
由于下一跳的值小于自己 next[j] < j
,因此假设 next[0] = -1
。那么,如果已经知道 j
的下一跳,不后退 i
的匹配算法如下:
1 | function KMP(A, B) { |
相较之前,两处改变,首先如果失配,i
值不变,j
变为下一跳;另一处为匹配条件增加了 -1
的判断。由于只有 next[0] = -1
,因此 j == -1
说明在 0 处就失配了,因此需要让下一个 i
和 j = 0
匹配。代码通过让 i
和 j = -1
都加 1
实现这一点。
接下来说明 next
数组的计算。
next
数组计算也利用 KMP 算法的思路,这里匹配的是 i
之前的子串的前缀 (j) 和后缀 (i)。
i
的索引依旧不后退,当出现 A[j] == A[i]
时,前后缀匹配,所以 next[i + 1]
可以跳到 j + 1
,即对于 ababc
,A[1] == A[3]
,说明前缀 A[0,1] = ab
和 后缀 A[2,3] = ab
匹配,所以有 next[4] = 2
。
具体代码如下:
1 | function getNext(A) { |
小结
本文简单介绍了 KMP 算法及其实现思路。首先说明常规的匹配方式,再对该算法进行改进引入 KMP 算法。然后说明对 KMP 算法的直观理解及具体实现。最后对 next 数组的求解进行了简要介绍。