字符串匹配是常见的算法题,就有一个字符串判断里面是否包含另一个字符串。

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

KMP算法(字符串匹配)_字符串比较


首先,字符串 “AAAAAABC” 的第一个字符与搜索词 “AAAB” 的第一个字符,进行比较。

AAAAAABC
AAAB

字符串有一个字符与搜索词的第一个字符相同,接着比较字符串和搜索词的下一个字符,还是相同。直到字符串有一个字符,与搜索词对应的字符不相同为止。

当字符串的索引为 3 的时候发现不相等,这时,最自然的反应是,将搜索词整体后移一位,再从头逐个比较。

AAAAAABC
AAAB

基于这个想法我们可以得到以下的程序:

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) 的。这太慢了!

KMP算法(字符串匹配)_字符串_02


我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。

跳过不可能成功的字符串比较

有些趟字符串比较是有可能会成功的;有些则毫无可能。而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。

KMP算法(字符串匹配)_i++_03

一个基本事实是,当 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"的位置。

function getNext(p) {
let nxt = [];
nxt.push(0); // next[0] 必然是0
let x = 1; // 因此 nxt[1] 开始求
let now = 0;
while (x < p.length) {
if (p[now] === p[x]) { // 如果 p[now] == p[x] ,则可以向右扩展一位
now += 1
x += 1
nxt.push(now)
} else if (now) {
now = nxt[now - 1] // 缩小 now,改成 nxt[now - 1]
} else {
nxt.push(0) // now 已经为0,无法再缩小了, 故 nxt[x] = 0
x += 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;
}
}

KMP算法(字符串匹配)_字符串比较_04