KMP算法

KMP算法是什么

是用来解决字符串匹配的问题。在一个 strstr 字符串中找到一个子字符串 subsub,返回索引的位置。
常规能想到的最简单的方式就是一个一个比对,匹配失败的话就向下挪一个位置继续匹配。这样做时间复杂度是 O(n2)O(n^2)。
KMP算法可以在 O(n)O(n) 的时间复杂度内解决这个问题。

next数组是什么

nextnext 数组的本质是寻找子串中“相同前后缀的长度,并且最长”。并且不能是字符串本身。举个例子而言,如下所示:

A B A B C
0 0 1 2 0

第 11位 AA 只有一个字符,不能是字符串本身,为 00;
第 22位 BB 和 AA 比较,不相等,为 00;
第 33位 AA 和 AA 比较,相等,所以相同前缀为 11;
第 44位 BB 和前缀为 11 后的 BB 比较,相等,和前缀组成了 ABAB 的相同模式的串,所以相同前缀为 22;
第 55 位 CC 和前缀为 22 后的 AA 比较,不相等,和前缀长度为 00 的比,相同前缀为00;
关于最长前缀的理解是,如下图所示。
next
最后一位的 AA 很显然可以和 ABAABA 组成长度为 33 的相同前缀,也可以自己 AA 组成长度为 11 的相同前缀。这种情况下选择较大的那个值。

求解next数组

nextnext 数组求解的本质是动态规划。假设我们已经知道当前数字的共同前后缀了,如下图所示。
1
接下来求第 ii 位的 nextnext 数组,分为两种情况。

  1. 下一位也和前缀相等
    那就很明显等于 next[i−1]next[i - 1] 加上 11。
  2. 下一位和前缀不相等
    这里不是直接等于 00 哦。如下图所示
    2
    虽然 ABABABAB 和 ABACABAC 并不相等,但是也不能直接设为 00。因为这个新来的 BB 可以和前面的 AA 组成 ABAB,达到 22 的长度。那么这个数难道要暴力求解吗?其实不然。此时第 i−1i - 1 位的 nextnext 的可利用的值在前缀的 nextnext 数组中可以得到答案,在本图中为 next[2]next[2],即是 11。此时修改前缀长度为 next[2]next[2],则又回到了最开始的状况,匹配长度为 11 并且匹配下一位,下一位 BB 相等,所以 next[i]next[i] 为 1+1=21 + 1 = 2。
    代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vector<int> buildNext(string sub) {
vector<int> next;
next.push_back(0);
int prefix_len = 0;
int i = 1; // 处理的索引,不会后退,以保证时间复杂度为O(n)
while (i < sub.size()) {
if (sub[i] == sub[prefix_len]) { // 下一位相等,next数组数值+1
prefix_len++;
next.push_back(prefix_len);
i++;
} else { // 下一位不相等
if (prefix_len == 0) {
next.push_back(prefix_len); // 目前没有相等的,就是0了
i++;
} else {
prefix_len = next[prefix_len - 1]; // 有不为0的前缀选择,继续回到开始状况
}
}
}
return next;
}

next数组在KMP中的应用

KMP算法在匹配失败的时候,回去看最后失败的字符前一个字符所对应的 nextnext 值。nextnext 数组中的值是什么作用呢?它代表我们可以“跳过匹配”的字符数量。
3
4
这样就可以保证匹配的 ii 不向后退,达到时间复杂度 O(n)O(n) 的目的。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int kmp(string s, string sub) {
vector<int> next = buildNext(sub);
int i = 0; // 主串指针
int j = 0; // 子串指针
while (i < s.size()) {
if (s[i] == sub[j]) { // 匹配,指针后移
i++;
j++;
} else if (j > 0) { // 匹配失败,但是next跳过了一些字符继续匹配
j = next[j - 1];
} else { // 子串第一个字符就匹配失败
i++;
}
if (j == sub.size()) {
return i - j; // 成功,返回初始下标
}
}
return -1; // 失败
}