这里简单介绍 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 数组的求解进行了简要介绍。