这里推荐B站大佬的关于kmp算法的视频:
https://www.bilibili.com/video/BV1Px411z7Yo/?spm_id_from=333.788.videocard.0
https://www.bilibili.com/video/BV1hW411a7ys/?spm_id_from=333.788.videocard.0
视频里讲得很直观,但是原理讲得并不是很清楚。其实kmp算法的原理也很简单,当我们要做字符串匹配的时候,如果用暴力的方法去匹配,可能会很浪费时间。这时候我们维护一个数组,记录子串的前缀。以视频中的例子说明:
0 a
0 a b
1 a b a
2 a b a b
0 a b a b c
这里的前缀,长度是小于原来的字符串的。kmp之所以能够提高算法的效率,就是通过相同的前缀和后缀来减少匹配的次数。假设abcaaabcd是我们要寻找的子串,对于abceaaabc来说,当前缀和后缀相同时,最长为3,即abc。假设我们在原来的字符串匹配到子串的最后一位d(假设对于原来的字符串来说是第i位,对子串来说是第j位)的时候,发现不相等。这个时候注意,之所以能够匹配到d,是因为前面的字符都相等。对于子串来说,能在原字符串中第i位之前最多能够匹配的字符,就是最长的前缀,而这个前缀在原字符串中是作为后缀的。这也就是为什么要令前缀和后缀相同,也就是kmp算法的原理,其中蕴含递归的思想,可以好好体会一下。
#include<bits/stdc++.h>
using namespace std;
void prefix_table(char pattern[], int prefix[], int n){
prefix[0] = 0;
int len = 0;
int i = 1;
while(i < n){
if(pattern[i] == pattern[len]){
len++;
prefix[i] = len;
i++;
}else{
if(len > 0){
len = prefix[len - 1];
}else{
prefix[i] = 0;
i++;
}
}
}
}
void move_prefix_table(int prefix[], int n){
for(int i = n - 1; i > 0; i--)
prefix[i] = prefix[i - 1];
prefix[0] = -1;
}
void kmp_search(char text[], char pattern[]){
int n = strlen(pattern);
int m = strlen(text);
int *prefix = new int[n];
prefix_table(pattern, prefix, n);
move_prefix_table(prefix, n);
//text[i] len(text) = m;
//pattern[j] len(pattern) = n;
int i = 0, j = 0;
while(i < m){
if(j == n - 1 && text[i] == pattern[j]){
cout << "pattern found at " << i - j << endl;
j = prefix[j];
}
if(text[i] == pattern[j]){
i++; j++;
}else{
j = prefix[j];
if(j == -1){
i++; j++;
}
}
}
}
int main(){
/*
char pattern[] = "ABABCABAA";
int prefix[9];
int n = 9;
prefix_table(pattern, prefix, n);
move_prefix_table(prefix, n);
for(int i = 0; i < n; i++)
cout << prefix[i] << " ";
cout << endl;
*/
char pattern[] = "ABABCABAA";
char text[] = "ABABABCABAABABABAB";
kmp_search(text, pattern);
return 0;
}
一开始的prefix[i]是这么定义的,假设它对应的字符串为abab,它的最长前缀和最长后缀(相同)是ab,它的值是2。可是我们在匹配的时候往往遇到这样的情况,就是匹配到最后一位b的时候发现不相同,这时我们看的是aba的前后缀。所以我们用prefix[i]来记录第i位之前字符串最长前后缀的长度,它的值为1。也就有了move_prefix_table()这个操作。
建立prefix数组的时候也是依据前面的情况来建立的,aba的最长前缀为1,abab新添加的字符b和第一个a之后的字符是相同的,所以prefix[i] = ++len。如果不同,我们看len = prefix[len - 1]。以ABA'B'CABA'A',最后一位A之前最长前缀是3。此时相互比较的是引号内的'B'和'A'。他们之前的字符都是相等的,我们看‘B’的前一个A之前的最长前缀,它标记的位置是否能与后面匹配,以此建立prefix数组。(这一段还是参考视频更加清楚)