文档讲解
KMP算法 前缀表 后缀表
28.找出字符串中第一个匹配项的下标
字符串匹配的问题:
1.暴力解法(很多书上叫做朴素法)
其实从这里我们可以回顾一下暴力解法是怎样的思考过程呢?
举个例子:
出现不匹配的情况,那么文本串的指针回到这次匹配初始的位置的下一个位置,而模式串的指针回到模式串的最开始,然后开始新的一轮匹配!
class Solution {
public:
int strStr(string haystack, string needle) {
int n=haystack.size();
int m=needle.size();
for(int i=0;i+m<=n;++i)
{
bool flag=true;
for(int j=0;j<m;++j)
{
if(haystack[i+j]!=needle[j])
{
flag=false;
break;
}
}
if(flag)
{
return i;
}
}
return -1;
}
};
故从上文的朴素解法,你会发现一个问题,我需要去遍历一遍文本串,每次遍历一个位置的时候,我都需要去遍历一遍模式串,这样它的时间复杂度是O(m*n),那么有没有什么方法能将时间复杂度降低呢?有的!那就是KMP算法,他的核心道理说起来都懂,就是如果文本串与模式串发生不匹配的时候,这个时候我不再是回到上次匹配的下一个位置,而且利用模式串本身的一些信息,告诉文本串,可以从某个位置开始匹配,而且从这个位置前面的其实与文本串是匹配上的。而这个信息是通过模式串本身来获取的,即KMP中的next数组!
next数组是一个前缀表,其作用就是记录模式串中,每个位置的最大相同前缀后缀子串的数量是多少?
文本串:aabaabaafa;模式串:aabaaf。
举个例子:aabaaf。其子串分别为a,aa,aab,aaba,aabaa,aabaaf
当子串为a,其前缀表为0
当子串为aa,其前缀表为1
当子串为aab,其前缀表为0
当子串为aaba,其前缀表为0
当子串为aabaa,其前缀表为2
当子串为aabaaf,其前缀表为0
故next数组为010020
这个时候,会发现一个问题,那就是如果我文本串的指针不动,模式串的指针跳回到b这个位置,那么b之前aa就不用判断了,因为b之前的aa和f之前的aa是一样的!看看上面的next=[010020]
会发现f前一个数据的下标是2,这表示跳到模式串索引为2的地方,即是b。仔细想想这是巧合吗?首先,要选择不等这个位置,前面的一个最大子串,因为不等这个位置选进去的话,且其前缀表在这个位置的值不为0,跳转的位置的一定是等于这个不等这个位置的值的,那么此时与文本串肯定是不匹配的,故考虑不等这个位置的前面一个位置,即其的最大子串!对于子串aabaa而言,其前缀表的两个元素和后缀表的前两个元素其实是相等的,这个时候得到前缀表为2,那么既然我们已经确定了模式串在f这个位置是不相等,那么模式串f之前的位置都是相等的,则f之前的两个元素一定是与文本串在此处向前的两个元素是相等的!那么从模式串最初的位置往后走前缀表中的数量肯定也是与文本串在此处向前的两个元素是相等,故我们可以从索引为2的地方开始匹配!而且还有一个更大的好处,那就是我们的文本串指针不需要往回跳了,只需要遍历一遍,那么此时的时间复杂度是O(n)!
接下来就是求解next数组:(我感觉这个是最难的地方)
有三步:
1.初始化
2.处理前后缀不相同的情况
3.处理前后缀相同的情况
void getnext(int *next,const string&s)
{
int j=0;
next[0]=j;
int i=1;
}
首先这里初始化就很难理解,定义了两个指针j
和i
,分别指向前缀表的末尾和后缀表的末尾,这个时候很多人会纳闷了,为什么前缀表的末尾是指向0,而后缀表的末尾是1呢?其实这里我们指的是子串的啦,这也是为什么有初始化的这一步,你想想看当子串是一个的时候,例如a
,那么前缀末尾是空,后缀末尾是空,这种情况无法列入进去,故先进行初始化这一步。当子串是二的时候,例如aa
,那么前缀的末尾是不是j=0,后缀的末尾是不是i=1,故我们是从子串为2开始循环!
void getnext(int *next,const string& s)
{
int j=0;
next[0]=j;
for(int i=1;i<s.size();i++)
{
while(j>0&&s[i]!=s[j])
{
j=next[j-1];
}
if(s[i]==s[j])
{
j++;
}
next[i]=j;
}
}
那么接着问题又来了,为什么要从j
要从next[j]
这个位置跳呢?我们想一下哈,我们next数组记录的是什么呢?是在此位置后缀和前缀有多少一样的,那么此时我们s[j]!=s[i]
了但是j
的前面还是存在和i
的前面相等的元素的呀,这个地方是后缀,那么前缀肯定也有相等的点,故我们此时应该跳到前缀同样相等的位置后一个位置,此时比较s[j]
是否等于s[i]
,如果相等的话,那么j
就该从这个位置继续往后和i
比较,这样就能保证最大相同的前后缀了。
class Solution {
public:
void getnext(int *next,const string& s)
{
int j=0;
next[0]=j;
for(int i=1;i<s.size();i++)
{
while(j>0&&s[i]!=s[j])
{
j=next[j-1];
}
if(s[i]==s[j])
{
j++;
}
next[i]=j;
}
}
int strStr(string haystack, string needle) {
int n=haystack.size();
int m=needle.size();
if(m==0)
{
return 0;
}
int next[m];
getnext(next,needle);
int j=0;
for(int i=0;i<n;i++)
{
while(j>0&&haystack[i]!=needle[j])
{
j=next[j-1];
}
if(haystack[i]==needle[j])
{
++j;
}
if(j==m)
{
return (i-m+1);
}
}
return -1;
}
};
最后再唠叨一句,其实两个字符串的匹配问题,模式串的指针的跳转也和构建next数组中的思路一样的,就是要去找到最大的相同的前后缀的位置!可以细细感悟一下!
459.重复的子字符串
思路:还是利用KMP的next数组,如果是重复的子串,最长的前缀数组和最长的后缀数组之间的差的子串就是重复的最小子串!
class Solution {
public:
void getnext(int* next,const string& s)
{
int j=0;
next[0]=j;
for(int i=1;i<s.size();i++)
{
while(j>0&&s[i]!=s[j])
{
j=next[j-1];
}
if(s[i]==s[j])
{
++j;
}
next[i]=j;
}
}
bool repeatedSubstringPattern(string s) {
if(s.size()==0)return false;
int next[s.size()];
getnext(next,s);
int len=s.size();
if(next[len-1]!=0&&len%(len-(next[len-1]))==0)return true;
return false;
}
};