原文地址:【算法笔记】KMP-字符串匹配问题
算法目标
解决字符串匹配问题,查找模式串p在主串s中第一次出现的位置,实现strstr()函数。
例子
主串 s : aabaabaaf
主串长度 m
模式串 p : aabaaf
模式串长度 n
{: width=“400”}
暴力解法
从主串s的每个字符开始,尝试匹配模式串,如果以s[i]作为开头匹配不成果,则尝试匹配以s[i+1]开头的字符串,算法时间复杂度为o(m*n)。
/*
* param s 主串
* param p 模式串
*
* Return p第一次出现在s中的位置,不存在则返回-1
*/
int BruteForce(string s, string p){
for(int i=0,j=0; i<s.size(); ++i){
int tmpi = i;
while(j < p.size()){
if(s[tmpi] == p[j]){
tmpi++;
j++;
}
else break;
}
if(j == p.size()) return i;
j = 0;
}
return -1;
}
KMP算法基本思路
基本名词解释
前缀:包含首字母,不包含尾字母
后缀:包含尾字母,不包含首字母
KMP算法的基本思路与人为匹配的方式相同,减少匹配过程中的重复操作。
首先看一下人为匹配方式,我们设当前主串匹配位置为i,模式串匹配位置为j
开始时,依旧从主串s与模式串p的开头开始匹配,当s[i] == s[j]时,说明当前位置上主串与模式串匹配成功,i与j都向后移动一位。
{: width=“400”}
当主串匹配到b,模式串匹配到f是,此时s[i] != s[j],说明当前位置上主串与模式串匹配失败,主串i-j的位置上并不是模式串第一次出现的位置,尝试将模式串整体向后移动。
{: width=“400”}
显而易见的是,模式串开头aa与主串已匹配部分后缀aa相同,所以将模式串后移至模式串前缀aa与主串已匹配部分的后缀aa重叠的部分,继续匹配重叠部分的下一位。
{: width=“400”}
反复重复上述操作,直至匹配到模式串的最后一位(匹配成功),或者主串的m-j+1位(匹配失败)。
移动过程遵循的原则是,将主串已匹配部分的后缀尽可能与模式串的前缀相等。
KMP算法实现的关键就在于,算法如何知道模式串应该向后移动几位呢?
前缀表
仔细观察主串与模式串不匹配时的情况,可以发现,因为i,j之前已经匹配,模式串中j为后缀尾的子串与i之前的i-j个字符完全匹配。所以,想要实现移动的过程中主串已匹配部分的后缀尽可能与模式串的前缀相等,则可转化为模式串的前缀尽可能与以j之前的后缀相等,所以需要记录模式串的每个前缀子串的最长相同前后缀长度。
{: width=“400”}
我们可以使用一个next数组来表示以模式串当前位置为前缀尾的前缀的最长前后缀相同长度。所以,当前模式串的next数组为[0,1,0,1,2,0],next数组所存的内容也就是前缀表。
{: width=“300”}
前缀表的计算
前缀表的计算分为4个步骤:
1)初始化
2)处理前后缀不相同的情况
3)处理前后缀相同的情况
为方便理解,下面描述过程不按实际执行顺序
初始化
该过程需要初始化next[0]=0(第一位的最长前后缀相同长度为0),后缀尾位置i=1,前缀尾位置j=0。
{: width=“400”}
处理前后缀相同的情况
当p[i] == p[j]时,说明当前位的前后缀匹配成功,将同时将前缀尾j和后缀尾i位置向后移动一位,并更新next数组值为j,因为j同时也表示i之前的子串最长相同前后缀长度;
{: width=“400”}
处理前后缀不相同的情况
当p[i] != p[j]时,说明当前位的前后缀匹配不成功,需要尝试将匹配的最大长度缩减,缩减前缀需要将前缀尾向前移动,也就是将j向前移动,缩减后缀尾需要将后缀头向后移动,后缀尾i不用移动。
如果,当红色部分和蓝色部分已经完全匹配时,若 x != y,考虑j如何移动。
{: width=“400”}
下图紫色部分为前缀缩减结果,绿色部分为后缀缩减结果。我们当然希望有尽可能长紫色部分与绿色部分相同,从而减少重复匹配的操作。
{: width=“400”}
由于红色部分与蓝色部分完全匹配,所以绿色部分与灰色部分也是完全相同的。那么希望有尽可能长的紫色部分与绿色部相同就可以等价于有尽可能长的紫色部分与灰色部分相同。
{: width=“400”}
紫色部分和灰色部分可以分别看做是红色部分子串的前缀和后缀,也就是希望红色部分的子串有尽可能长的相同前后缀,这与next数组的定义相同。next[j-1]也就表示红色部分子串的最长相同前后缀长度,前缀的长度也就表示了下一个待匹配的位置。所以,j=next[j-1]
实现代码
/*
* param next 前缀表
* param p 模式串
*
*/
void GetNext(vector<int>& next, const string& p){
//i表示后缀尾,j表示前缀尾
//同时j表示i之前的最长相等前后缀长度
int i = 1, j = 0;
next[0] = 0;
for(;i<p.size(); ++i){
while(j>0 && p[i] != p[j]){
//当i和j不匹配时,j向前跳转
//当j已经跳转到开头时,不再跳转,防止死循环
j = next[j-1];
}
if(p[i] == p[j]) j++;
next[i] = j;
}
}
使用next前缀表进行匹配
思路与next前缀表的计算相同,通用分为三个步骤:
1)初始化
2)主串与模式串不匹配情况
3)主串与模式串匹配情况
初始化
直接初始化两个下标i,j,分别指向主串s与模式串p的开头
{: width=“400”}
主串与模式串不匹配情况
当s和p的红色部分已经完全匹配时,x != y。
{: width=“400”}
我们希望在s和p种有尽可能相同的绿色部分。同时由于红色部分完全匹配,所以等效于希望在p种有尽可能长的相同绿色部分与紫色部分,也就是希望p中红色子串有尽可能长的相同前后缀。这与next数组定义相同,所以j = next[j]。
{: width=“400”}
主串与模式串匹配情况
当s[i] == p[j]时,s和p当前位匹配, i和j分别向后移动一位
代码实现
/*
* param s 主串
* param p 模式串
*
* Return s中第一次出现p的位置,不存在则返回-1
*/
int KMP(const string& s, const string& p){
vector<int> next(p.size(),0);
GetNext(next,p);
for(int i=0,j=0; i<s.size(); ++i){
while(j > 0 && s[i] != p[j]){
j = next[j-1];
}
if(s[i] == p[j]){
j++;
}
if(j == p.size()) return i - p.size() + 1;
}
return -1;
}
参考资料
https://www.bilibili.com/video/BV1PD4y1o7nd?from=search&seid=4632787176795168721
https://www.bilibili.com/video/BV1M5411j7Xx/?spm_id_from=333.788.videocard.0