什么是KMP算法
KMP算法是由 D.E.Knuth、J.H.Morris 和 V.R.Pratt 提出的模式匹配算法,简称 KMP 算法。KMP 算法对 BF算法进行了很大改进,提高了匹配效率。
KMP算法和BF算法的具体区别这里就不多做解释,主要讲解如何实现KMP算法
KMP算法分析
有一个子串 ababc,我们需要在主串 xxxxxxxxxxxx 中搜索这个子串中搜索这模式串。用 i 和 j 分别表示当前匹配到主串和子串的哪个位置
字符匹配成功:
当前字符匹配成功时,i 和 j 都 +1,匹配下一个字符
字符匹配失败:
当最后一个字符匹配失败时,那么说明前面的字符都匹配成功,可以确定主串中前四个字符与子串的前四个字符相等。
如果按照BF算法,那么 i 需要指向 主串的第二个字符,j 指向子串的第一个字符,然后重新匹配
每次发生匹配失败时 i 都需要往回退
而 KMP 算法 的 i 则不需要变动
为了方便观察,我们可以在当前字符的左边画一条分界线,分界线左边的字符都是已知的
我们可以让子串每次往左移动一个字符,看看子串的第一个字符与主串的第二个字符是否相等
显然它们不相等,那么就没有必要在匹配一次这两个字符。再把子串往右移动一个字符
主串的第三个字符是 a,子串的第一个字符也是 a,它们两个相等,那么对比子串的下一个字符和主串的下一个字符是否相等,显然也相等。主串下一个字符是未知的,所以不能再移动子串。此时 i 指向的是这个未知的字符,而 j 指向的就是要与这个未知字符匹配的子串字符,即第三个字符(j 之前的字符已经匹配成功了,不需要在进行匹配)。
所以当 第五个字符匹配失败时,我们不需要让 i 往回退,让 i 保持不变,j = 3 即可。
当第四个字符匹配失败时,说明前面三个字符匹配成功,主串与子串的前三个字符都相等
在第四个字符前画一条分界线,分界线左边的字符已知
子串依次往右移动一个字符,对比子串的第一个字符与主串的第二个字符是否相等,不相等时继续移动子串,对比主串的下一个字符。对比到主串的第三个字符时,与子串的第一个字符相等,且下一个字符是未知的字符,停止移动子串。
此时 i = 4,j = 2。所以当第三个字符匹配失败时,直接让 j = 2 即可
当第三个字符匹配失败时,说明前面两个字符匹配成功,主串与子串的前两个字符都相等
同样的在第三个字符前画一条分界线,分界线左边的字符已知
子串往右移动进行对比,a 与 b 不相等,继续移动一个字符
此时子串来到了 i 这个位置,因为 i 所指向的主串的字符未知,所以要和子串的第一个字符进行匹配.此时 i = 3,j = 1
最后剩下第一和第二个字符,这两个位置有点特殊
无论什么情况下,当第一个字符匹配失败时,让 j = 0;当第二个字符匹配失败时,让 j = 1;
当第二个字符匹配失败时,主串的第二个字符是未知的,因为我们只知道它不等于 b,可能等于其他任何值。所以子串需要往右移动一位,和这个未知的字符进行匹配
可以思考一下,不管是什么情况下,只要第二个字符没有匹配上,就可以让 j = 1
当第一个字符匹配失败时,直接从主串的下一位字符开始匹配。 根据前面的规律,i 需要保持不变,j 需要等于某个值。为了保持跟之前的处理逻辑一样,可以先让 j = 0,然后 i 和 j 都 +1
整合以上各个字符匹配错误时对应的 j 的值,可以得到一个 next数组
为了和子串字符的位序保持一致,不使用 0 这个位置,直接从 1 开始表示
next[0] | next[1] | next[2] | next[3] | next[4] | next[5] |
---|---|---|---|---|---|
0 | 1 | 1 | 2 | 3 |
在主串中搜索子串的位置时,当某一个字符匹配错误,可以让 j 等于 next 数组中这个字符所对应的值。这样可以跳过一些不必要的匹配
用 next 数组来模拟搜索一下子串
主串:a c a b a d a b a b c
子串:a b a b c
第一位匹配成功,i 和 j 都 +1
第二位匹配失败,根据 next 数组,让 j = 1
第一位匹配失败,j 置为 0,i 和 j 都 +1
若匹配成功 i 和 j 都 +1,匹配下一个字符,直到遇到匹配失败的字符
第四个字符配对失败,i 保持不变,j = 2
第二个字符配对失败,i 不变,j = 1
第二个字符配对失败,i 不变,j = 1
后面的字符都能配对成功,如果 j 大于了子串的长度,说明主串中能够找到子串
代码实现
定义一个结构体,包含一个存放字符串的字符数组和一个变量记录字符串的长度
C
typedef struct {
char ch[255];
int length;
} SString;
准备主串、子串和一个空的 next 数组
字符串从 1 开始,所以 0 的位置可以用一个特殊字符表示这个位置为空
next 数组也是从 1 开始存储,所以长度为 S2.length+1
C
int main() {
SString S1 = { "#acabadababc", 11 };
SString S2 = { "#ababc", 5 };
int next[S2.length+1];
}
实现 next 数组
C
void nextArr(SString S1, SString S2, int next[]) {
// 第 o 位用 -1 表示这个位置不使用
next[0] = -1;
// 无论什么情况第一位都为 0
next[1] = 0;
// 无论什么情况第二位都为 1
next[2] = 1;
// i 表示第几个字符匹配失败,循环的次数为子串的长度-3
for (int i = 3; i <= S2.length; i++) {
// x 表示每次发生匹配错误,对主串前面已知的字符与子子串对比时主串开始的位置
// y 表示表示子串当前对比到的位置
// 子串和主串的字符配对成功后,x 和 y 都需要 +1,然后对比下一个字符,若主串下一个字符与子串不相等,则需要让主串开始对比的位置往右移一位,所以需要用 z 来记录主串最初开始对比的位置
int x = 2, y = 1, z = 2;
// 循环对比主串已知的字符和子串是否一样。x 超过匹配失败的那个字符时停止循环
while (x < i) {
// 如果一样,x 好 y 同时 +1
if (S1.ch[x] == S2.ch[y]) {
x++;
y++;
// 如果不一样,主串开始对比的位置 +1,让 x 回到这个位置,y 回到子串的第一位
} else {
z++;
x = z;
y = 1;
}
}
// 对比结束时 y 的值就是子串下一个要匹配的字符的位置
next[i] = y;
}
}
把 next 传入搜索函数,根据数组来判断匹配失败时 j 的值应该为多少
C
int main() {
SString S1 = { "#acabadababc", 11 };
SString S2 = { "#ababc", 5 };
int next[S2.length+1];
// 把主串、子串和 next 数组传入 nextArr 函数
nextArr(S1, S2, next);
// 把主串、子串和 next 数组传入 search 函数
int result = search(S1, S2, next);
printf("result: %d\n", result);
}
实现 search 函数
C
int search(SString S1, SString S2, int next[]) {
// i 和 j 表示主串和子串匹配到了哪个字符
int i = 1, j = 1;
// 当 i 大于子串的长度或 j 大于子串的长度结束循环
while (i <= S1.length && j <= S2.length) {
// 如果这两个字符相等,则匹配下一个字符。因为当第一个字符匹配失败时,会让 j = 0,然后 i 和 j 都 +1,所以这里要处理 j = 0 的情况
if (j == 0 || S1.ch[i] == S2.ch[j]) {
i++;
j++;
} else {
// 不相等则直接根据 next 设置 j 的值
j = next[j];
}
}
// j 大于子串的长度说明子串匹配成功,返回子串的位置,也就是子串在主串中第一个字符的位置
if (j > S2.length) {
return i - S2.length;
} else {
// 匹配失败则返回 0
return 0;
}
}
最后运行代码看一下结果是否正确
测试另一个主串和子串
S1 = { "#eabcbcaeeab", 11 }; S2 = { "#bcaeea", 6 };
优化 next 数组
大家有没有发现在上面的字符串中,第三位字符如果匹配失败,根据 next 数组,j 的值应该为 1,也就是说 j 应该指向子串的第一个字符。但是子串的第一个字符和第三个字符相等,都是 a,那么第三个字符匹配失败了,说明主串中这个未知的字符肯定不是 a,第一个字符也肯定会匹配失败。所以我们可以优化一下 next 数组,如果 S2.ch[j] == S2.ch[next[j]] 的话,可以直接让 j 的值等于第一个字符 j 的值,这样可以跳过这次肯定会失败的对比。
基于 next 优化成 nextval 数组
C
int main() {
SString S1 = { "#acabadababc", 11 };
SString S2 = { "#ababc", 5 };
int next[S2.length+1];
nextArr(S1, S2, next);
int nextval[S2.length+1];
nextvalArr(S2, next, nextval);
int result = search(S1, S2, nextval);
printf("result: %d\n", result);
}
实现 nextval 数组
C
void nextvalArr(SString S2, int next[], int nextval[]) {
nextval[0] = -1;
nextval[1] = 0;
for (int j = 2; j <= S2.length; j++) {
if (S2.ch[j] == S2.ch[next[j]]) {
nextval[j] = nextval[next[j]];
} else {
nextval[j] = next[j];
}
}
}
运行优化后的代码,结果也没有问题