前言
最近在实践费曼学习法,将其运用于学习中。kmp算法是数据结构与算法中一个艰难的知识点,弄懂这个算法的原理并使用C++实现耗费了数天时间。写下这篇博客梳理KMP算法的原理和我C++实现算法思路。
简介
KMP算法是基于BF算法的优化算法,BF算法是将主串分解成(主串长度-模式串长度+1)个子串,再一一比较这些子串和模式串,每次比较都要回溯主串的指针,而KMP算法就是在回溯部分进行了优化,主串指针不进行回溯,而是回溯模式串的指针到特定的位置继续匹配,KMP算法的关键部分就是在与寻找模式串回溯的特定位置,这个回溯的特定位置使用数组储存,事实上,这个位置只跟模式串有关,为了说明为什么要回溯到到这个位置,需要引入公共前后缀的概念。
int search_kmp(string& M,string& P,int pos)
{
int next[P.length()];//next数组储存匹配失败后模式串回溯的位置
get_next(P,next);//获取模式串指针回溯的位置,这个位置与模式串本身有关
int i=pos,j=0;//i是主串的指针,j是模式串的指针
while(j<P.length()&&i<M.length())
{
if(j==0||M[i]==P[j])
{
++i;
++j;
}
else//匹配失败,模式串指针回溯到特定位置
{
j=next[j-1];
}
}
if(j==P.length())//匹配成功则j指针指向文本串(主串)的尾元素之后一位
return i-P.length();//返回主串中匹配到模式串时的起始位置
else//匹配失败,返回错误信息,说明没有找到
return -1;
}
相关概念
前缀:除去首元素的串的所有子串,例如:串atabat的前缀有t、ta、tab、tatb、tabat。这个串的首元素是a,前缀中不包含首元素a
后缀:除去尾元素的串的所有子串,例如:串atabat的后缀有a、at、ata、atab、ataba。这个串的尾元素是t,后缀中不包含尾元素t
公共前后缀:相同的前后缀成为公共前后缀,如串atabat的公共前后缀有at。
事实上,next数组储存的值就是公共前后缀的长度,也就是在不匹配时模式串指针回溯到的位置。
KMP算法回溯原理
举例:
为什么可以用公共前后缀来进行回溯呢?这个问题困扰了我很久。
例如:
有主串M,可以分解成由若干子串组成,用ABAC表示(A、B、C是M的子串)
有模式串P,可以分解成分由若干子串组成,用ABAD表示(A、B、D是P的子串)
ABAC
ABAD,两个串在匹配到C、D时发现不一致,则将模式串指针从D回溯到第二个A后面
相当于ABAC
ABAD
接下来就是匹配主串的子串C与模式串的子串BCD,这就相当于提前完成了部分匹配,不需要主串指针进行回溯,节省了BF算法中主串回溯的步骤,优化了时间复杂度。
上面使用直观的方式进行字符串匹配,在代码中是通过移动模式串指针来达到这样的效果。相当于移动模式串指针到第一个子串A之后,因为模式串数组从0开始,所以这一位置数值上恰好就是公共前后缀A的长度。
事实上,不匹配时公共前后缀长度应是0,利用的是模式串指针值前一位元素的公共前后缀和。
求取公共前后缀长度的值
在代码中,公共前后缀长度的值用next数组储存
void get_next(string& P,int* next)
{
int j=0;//j是前缀串的尾指针,j是后缀串的尾指针
next[0]=0;//定义next数组的首元素值为0
for(int i=1;i<P.length();i++)//后缀的尾指针从1开始
{
//处理前后缀不匹配情况,前后缀不匹配前缀指针回退
while(j>0&&P[j]!=P[i])
{
//不匹配时j寻找部分匹配的情况,如果存在,则一定是P[0,j-1]的最长公共前后缀
j=next[j-1];
}
//处理前后缀匹配情况
if(P[j]==P[i])
{
j++;//或者是next[i]=j,j此时也是最长公共前后缀长度
}
next[i]=j;
}
}
求取next数组需要处理四种情况
1、初始化
2、前后缀相同时
3、前后缀不同时
4、更新next数组的值
初始化
next【0】=0,for循环处理后缀尾指针从1开始,所以需要先处理前缀尾指针为0的情况
前后缀相同时
即推进前缀的尾指针,j++
前后缀不同时
因为后缀指针i向后推进一位,所以后缀变更了。将前后缀都作为一个模式串,寻找这个模式串前一部分与后一部分部分匹配的子串。指针回溯后,进行后一位元素的比较,知道回溯到0或者匹配后为止。
这一部分简书这篇博客讲得很好:
[算法] KMP算法中如何计算next数组 - 简书 (jianshu.com)
更新next的值
我们可以发现,j即前缀的尾指针的值恰好就等于前缀长度(也等于后缀长度),所以可以利用j的值更新next数组
完整代码
#include<iostream>
#include<string>
using namespace std;
void get_next(string& P,int* next);//修改next数组的值
int search_kmp(string& M,string& P,int pos=0);
void check_next(string& P,int* next);
int main()
{
string m,pattern;
cout<<"输入文本串:\n";
cin>>m;
cout<<"输入模式串:\n";
cin>>pattern;
cout<<"模式串在文本串中的起始位置:\n";
cout<<search_kmp(m,pattern,0);
return 0;
}
void get_next(string& P,int* next)
{
int j=0;//j是前缀串的尾指针,j是后缀串的尾指针
next[0]=0;//定义next数组的首元素值为0
for(int i=1;i<P.length();i++)//后缀的尾指针从1开始
{
//处理前后缀不匹配情况,前后缀不匹配前缀指针回退
while(j>0&&P[j]!=P[i])
{
//不匹配时j寻找部分匹配的情况,如果存在,则一定是P[0,j-1]的最长公共前后缀
j=next[j-1];
}
//处理前后缀匹配情况
if(P[j]==P[i])
{
j++;//或者是next[i]=j,j此时也是最长公共前后缀长度
}
next[i]=j;
}
}
int search_kmp(string& M,string& P,int pos=0)
{
int next[P.length()];
get_next(P,next);
int i=pos,j=0;//i是主串的指针,j是模式串的指针
while(j<P.length()&&i<M.length())
{
if(j==0||M[i]==P[j])
{
++i;
++j;
}
else
{
j=next[j-1];
}
}
if(j==P.length())//匹配成功则j指针指向文本串(主串)的尾元素之后一位
return i-P.length();
else//匹配失败,返回错误信息,说明没有找到
return -1;
}