目录
问题背景
逐步剖析 KMP如何优化暴力做法
思考
公共前后缀
next数组
如何构建next数组:
代码实现
问题背景
给定一个字符串 S,以及一个模式串P, P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
逐步剖析 KMP如何优化暴力做法
暴力做法为双重for循环一一比对,时间复杂度为O(len_s*len_p)。
暴力枚举
for(int i=0;i<s_len;i++){
for(int j=0;j<p_len;j++){
if(s[i+j]==p[j])
j++
if(j==p_len){
//匹配成功逻辑
};
}
}
思考
那么某一个位置出现匹配失败的时候,我们能否利用已知信息来免去不必要的比对?
如图当指针分别指向 i,j处时,黑色框中的部分是完全一致的,能否挖掘这条信息的价值来提高效率?
答案时肯定的:KMP三位大佬(D.E.Knuth,J.H.Morris,V.R.Pratt)通过研究,找到了可以利用已知信息来优化暴力算法的方式,并引入了"公共前后缀"的概念。
公共前后缀
此时此时黄色和红色都是匹配失败时s,p相同部分的共同前后缀,而黄色是"最长公共前后缀"。
而KMP三位学制通过研究发现 当在某个位置出现匹配失败时,只需让指向p的指针回退到相同部分的"最长公共前缀"的后一个位置,继续比对即可,不需要回退指向s的指针。
因此最长公共前后缀不能刚好是匹配失败时p,s的相同部分,不然j=j,陷入死循环。
若还是匹配不上即继续回退j指针,若j回退到开头并且依旧匹配失败,则直接让i++。
伪代码:
int i=0,j=0;
while(i<len_s){
if(s_arr[i]==p_arr[j]) i++,j++;
else{
if(j!=0) 回退j至公共前缀后;
else i++;
continue;
}
if(j==len_p){
//匹配成功逻辑
}
}
现在关键点就变成了如何高效地求得最长公共前后缀的长度呢?
next数组
这里就要引入另一个关键概念:"next数组"
我们可以通过预先遍历一遍p字符串构建一个名为next的数组,next[i]的值就代表:区间[p[0],p[i]]的最长公共前后缀长度,匹配失败时让j=next[j-1]即可。
如何构建next数组:
思路:
1.设定初始值next[0]=0(最长前后缀不能是子串本身) ,指针 j从1开始遍历p字符串。
2.如果[p[0]~p[j-1]]的最长前后缀长度为len,此时若p[len]==p[j]那么[p[0]~p[j]]的最长前后缀长度就为len+1,next[j]=len+1;
3.若p[len]!=p[j],我们就要尝试更短的前后缀
此时len为4的行不通,要尝试更短的"bab"或"ab"相当于是要找后缀的后缀与前缀的前缀的最长共同部分,因为前后缀相同,所以等价于找前缀的最长共同前后缀:len=next[len-1]。
代码实现
#include <iostream>
using namespace std;
const int N = 1e7 + 10;
// KMP 算法
// 核心思想:
// 通过每次匹配失败时已经掌握的信息来跳过暴力做法中"必定失败"和"一定成功"的一一比对
char p_arr[N], s_arr[N];
int len_p, len_s;
int next_arr[N];
//
// 构建next数组
void next_arr_init()
{
int len = 0;
next_arr[len] = 0;
// i 从1开始
int i = 1;
// 遍历
while (i < len_p)
{
// 相等++
if (p_arr[i] == p_arr[len])
next_arr[i++] = ++len;
// 不相等
else
// len 已经为0
if (len == 0)
next_arr[i++] = 0;
// len不为0
else
len = next_arr[len - 1];
}
}
int main()
{
cin >> len_p >> p_arr >> len_s >> s_arr;
next_arr_init();
int i = 0, j = 0;
// 遍历 s
while (i < len_s)
{
// 相等 更新 i,j
if (s_arr[i] == p_arr[j])
i++, j++;
// 不相等
else
{
if (j) // j不在开头 即查表
j = next_arr[j - 1];
else // 在开头 则这个位置匹配失败 更新 i
i++;
continue;
}
// 匹配成功
if (j == len_p)
{
cout << i - len_p << ' ';
// 更新j寻找下一个子串
j = next_arr[j - 1];
}
}
return 0;
}
KMP创始人之一 Donald Ervin Knuth.