高效字符串匹配:KMP算法
本文针对计算机统考408的数据结构复习需求,系统解析KMP算法的核心原理与实现要点。
KMP算法的核心思想
当主串与模式串匹配失败时,利用已匹配部分的最长公共前后缀长度,确定模式串滑动距离,避免主串指针回溯。时间复杂度:O(n+m)。
方便理解我们先来看一个例子:
本次匹配的模式串为abaabc,主串在扫描前未知
匹配到下标3失败时,可以确定主串前三位为aba,第4为不是a
很明显,我们将子串向右移一位是不可能匹配的, 这样一开始就匹配失败了,而我们向右移动两位,有可能可以匹配成功,因此我们令j=1继续匹配。由j=3匹配失败后令j=1,构建next数组(表示该处字符不匹配时应该回溯到的字符的下标),令next[3]=1。
构建next数组 (手算)
假设匹配到第i位失败了,说明主串和模式串前i-1位是相同的,设next[i] = j (n>0)。
由左边的图可知:
由右边的图可知:
联立两式,得:
简单来说,next[j] = 前 j - 1 位的最长相等前后缀长度。
拿刚才模式串abaabc举例
下标i | 前i位子串 | 后i位子串 | 最长相等前后缀长度 | next[i] |
0 | 空(首字符失配) | 空 | 无 | -1 |
1 | 空 | 空 | 0 | 0 |
2 | a | b | 0 | 0 |
3 | a, ab | a, ba | 1 | 1 |
4 | a, ab, aba | a, ba, baa | 1 | 1 |
5 | a, ab, aba, abaa | b, ab, aab, baab | 2 | 2 |
最终next数组中的元素应该是{-1, 0, 0, 1, 1, 2}。
理解上面的思路,接下来用代码来实现求模式串的next数组
匹配字符串
假设此时已经获得next数组,接下来进行匹配,不理解可以画个草图模拟一下代码。
// 字符串匹配,返回匹配的首字母位置
int kmpSearch(const string& S, const string& T, int next[]) {
int i = 0, j = 0; // i为主串的指针,j为模式串指针
int lenS = S.size(), lenT = T.size();
while(i < lenS && j < lenT) {
if(j == -1 || s[i] == p[j]) { // 如果模式串无法右移或者单词匹配成功
i++; // 两指针都往右移一位,继续匹配
j++;
}
else j = next[j]; // 匹配失败,回退j指针
}
if(j == lenT) return i - j; // 模式串全部匹配成功,输出首字母位置
return -1;
}
构建next数组 (机算)
本质上与匹配字符串的操作大差不差,核心跳转逻辑分三种情况:
1. j在初始位置-1 → 重置为0开始匹配
2. 字符匹配 → 同步前进
3. 字符不匹配 → 跳转到前一个匹配位置
void get_next(const string& T, int next[]) {
int i = 0, j = -1;
next[0] = -1;
while (i < n - 1) {
if (j == -1 || p[i] == p[j]) {
j++;
i++;
next[i] = j; // 前后缀相等的最大值
} else {
j = next[j]; // 回退寻找可能匹配的位置
}
}
}
结语
KMP算法通过预处理模式串的next数组,将匹配过程中的无效比较降至最低,是空间换时间的经典设计。备考时需重点掌握next数组的手算推导(最长相等前后缀计算),可以结合408真题巩固练习。