KMP算法(字符串匹配)

字符串匹配是常见的算法题,就有一个字符串判断里面是否包含另一个字符串。
举例来说,有一个字符串"AAAAAABC"(主串),我想知道,里面是否包含另一个字符串"AAAB"(模式串)?对主串和模式串做匹配。

首先,字符串 "AAAAAABC" 的第一个字符与搜索词 "AAAB" 的第一个字符,进行比较。
AAAAAABCAAAB
字符串有一个字符与搜索词的第一个字符相同,接着比较字符串和搜索词的下一个字符,还是相同。直到字符串有一个字符,与搜索词对应的字符不相同为止。
当字符串的索引为 3 的时候发现不相等,这时,最自然的反应是,将搜索词整体后移一位,再从头逐个比较。
AAAAAABCAAAB
基于这个想法我们可以得到以下的程序:
function bf(ts, ps) {let t = ts;let p = ps;let i = 0; // 主串的位置let j = 0; // 模式串的位置while (i < t.length && j < p.length) {if (t[i] === p[j]) { // 当两个字符串相同,就比较下一个i++;j++;} else {i = i - j + 1; // 一旦不匹配,i后退j = 0; // j归0}}if (j === p.length) {return i - j} else {return -1;}}console.log(bf('AAAAAABC', 'AAAB'))
上面的程序是没有问题的,但不够好!这是暴力解法复杂度 O(nm) 的。这太慢了!

我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。
跳过不可能成功的字符串比较
有些趟字符串比较是有可能会成功的;有些则毫无可能。而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。

一个基本事实是,当 d 不匹配时,你其实知道前面五个字符是"abcab"。如果是人为来寻找的话,肯定不会再把 i 移动到索引为1,我们会直接移动到索引为3!就可以来到第二个"ab"的位置。
所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道指针要移动到哪?
移动位数 = 已匹配的字符数 - 对应的部分匹配值首先,要了解两个概念:"前缀"和"后缀"。"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
"abcab"的前缀为[a, ab, abc, abca],后缀为[bcab, cab, ab, b],共有元素为"ab",长度为2;
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"abcab"之中有两个"ab",那么它的"部分匹配值"就是2("ab"的长度)。搜索词移动的时候,第一个"ab"向后移动到索引为3(字符串长度5-部分匹配值2),就可以来到第二个"ab"的位置。
根据nxt数组加快字符串匹配。
function getNext(p) {let nxt = [];nxt.push(0); // next[0] 必然是0let x = 1; // 因此 nxt[1] 开始求let now = 0;while (x < p.length) {if (p[now] === p[x]) { // 如果 p[now] == p[x] ,则可以向右扩展一位now += 1x += 1nxt.push(now)} else if (now) {now = nxt[now - 1] // 缩小 now,改成 nxt[now - 1]} else {nxt.push(0) // now 已经为0,无法再缩小了, 故 nxt[x] = 0x += 1}}return nxt}console.log(getNext('abcab'))// [ 0, 0, 0, 1, 2 ]
根据nxt数组移动标尺。
function bf(ts, ps)let t = ts;let p = ps;let i = 0; // 主串的位置let j = 0; // 模式串的位置let nxt = getNext(ps)while (i < t.length && j < p.length) {if (t[i] === p[j]) { // 当两个字符串相同,就比较下一个i++;j++;} else {// 失配了if (j) {j = nxt[j - 1] // 根据nxt数组移动标尺} else {i++; // ps[0]失配了,直接把标尺往右移动一位}}}if (j === p.length) {return i - j} else {return -1;}}

