@Dale-Lin
2022-11-29T08:58:28.000000Z
字数 1862
阅读 553
算法分析
Knuth-Morris-Pratt(KMP)算法是一种改进的字符串匹配算法,核心是利用匹配失败后的信息,减少pattern串和主串的匹配次数以达到快速匹配的目的。
时间复杂度是 O(m+n)
例如有字符串 S 和 P,判断模式串 P 是否是 S 的子串:
| 串 | - | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| S | A | C | A | C | A | C | A | C | A | A | C | T | G | P | A | C | Y | |
| P | A | C | T | G | P | A | C | Y |
按照普通的匹配方式,设 P 串开始匹配的位置是 i,S 串开始匹配的位置是 j:
在每一轮匹配时,需要重复对 P 串的 AC 进行比较,并且前面的 AC 越多,浪费的时间就越多。
假设 S 的长度为 m,P 的长度为 n,理论上来说,最坏情况下迭代 m - n + 1 轮,每轮最多进行 n 次对比,当 m >> n 时,渐进时间复杂度为 O(mn)
再看一个例子
| 串 | - | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| S | A | C | T | G | P | A | C | T | G | K | A | C | T | G | P | A | C | Y | |
| P | A | C | T | G | P | A | C | Y |
按照原来的思路,在比较到 i = 7 时,我们需要重新从 s = 1, i = 0 继续比较;但 KMP 算法告诉我们,在重新比较的时候,只需将字符串 P 需要比较的位置重置到 i = 2 即可。
原因是我们发现 P 串有子串 ACT 和 ACY,当 i = 7 发现 T 和 Y 不匹配的时候,我们确认 S 串 j = 5 开始的 AC 不匹配对应 P 串 i = 5 开始的 AC,但可能匹配 P 串 i = 0 开始的 AC。
这就是 KMP 算法的核心思想:使用一个数组 next 来保存 P 串元素不匹配时,下一步重新开始的 P[i] 的位置;其中 next[i] 表示 P 串 [0, i - 1] 的子串里 最长公共前后缀的长度,next[0] 为 -1,表示需要 j 移到 j + 1 开始匹配。
最长公共前后缀:例如对于字符串 abcba:
a, ab, abc, abca,不包括本身;bcba, cba, ba, a,不包括本身;a,长度为 1.所以对于 S 串和 P 串,有以下 next:
| 串 | - | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| S | A | C | T | G | P | A | C | T | G | K | A | C | T | G | P | A | C | Y | |
| P | A | C | T | G | P | A | C | Y | |||||||||||
| next | -1 | 0 | 0 | 0 | 0 | 0 | 1 | 2 |
匹配过程变成:
next[7] = 2,意味着只需要将 P[2] 和 S[7] 开始继续比较即可;next[4] = 0,我们重置 i = 0;next[0] = -1,把 j 设成 10;
const kmp = (s, p) => {const next = buildNext(p)let matchPIdx = 0;for (let i = 0; i < s.length; i++) {if (matchPIdx === p.length) return trueif (s[i] !== p[matchPIdx]) {while (next[matchPIdx] !== -1 && p[matchPIdx] !== s[i]) {matchPIdx = next[matchPIdx]}}matchPIdx++}return matchPIdx === p.length}
const buildNext = p => {const next = [-1]let nextIdx = 0let matchPIdx = -1 // nextIdx - 1 的子串里找while (nextIdx < p.length - 1) {if (matchPIdx === -1 || p[matchPIdx] === p[nextIdx]) {const pos = nextIdx + 1 // next 数组要设置的位置const curLen = matchPIdx + 1 // 前后缀的长度next[pos] = curLen // 赋值// 下个位置nextIdx++matchPIdx++} else {/*** next[matchPIdx]* 含义是 p[0]~p[matchPIdx - 1] 的最长前后缀长度;* matchPIdx = next[matchPIdx]* 把匹配指针设置到不匹配的位置 next[matchPIdx] - 1的下一个位置;** 利用了 next 的中间产物,是一种类似 KMP 的思想*/matchPIdx = next[matchPIdx]}}return next}