下面来总结一下用来匹配字符串的KMP算法,以及怎么求next数组。
当然我们可以用笨办法可以进行字符串匹配和求next数组,但是学习算法不就是追求极致的过程么?
通过参考https://blog.youkuaiyun.com/sun20209527/article/details/79933237,可以通透的理解KMP是个什么过程,但细节处理和他会有些出入。
举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?
我们先说一下next数组代表的是什么?就是一个字符串的每一个子串的前缀后缀的共有元素长度。
以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
所以next[0] = 0 ; next[1] = 1 ; next[2] = 0; next[3] = 0 ; next[4] = 1; next[5] = 2 ; next[6] = 0 ;
下面就是KMP的具体过程:
1.
B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
这里出现了空格和D不匹配的现象.
5.
注意:这个操作一次连续移动了4位,我们把已经匹配的ABCDAB字符串的前缀移动到后缀的位置上就可以了!
6.
因为空格与A不匹配,继续后移一位。
7.
一直匹配。。。。一直到发现C和D不匹配。
8.
已经匹配的字符串ABCDAB还是把最大的 前缀放到后缀的位置上。然后继续匹配ABCDAB后面的字符。
9.
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么我们可以利用next数组记录的信息搜移动之后,就可以来到第二个"AB"的位置。
#include<iostream>
#include<cstring>
using namespace std;
void getnext(string s,int next[])
{
int n = s.size();
memset(next,0,sizeof(next));
int k = 0; //k代表 最大前后缀长度
for(int i = 1,k = 0; i<n; i++)
{
while(k>0 && s[k] != s[i])
{
k = next[k-1];
}
if(s[i] == s[k])
{
k++;
}
next[i] = k;
}
}
//a 是主字符串 b是小字符串
int kmp(string a,string b,int next[])
{
int n = a.size() , i = 0; //主字符串的长度,主字符串的下标
int m = b.size() , j = 0; //小字符串的长度,已经匹配的个数
while(i < n)
{
if(j==0) //如果小字符串中的第一个元素没有找到与大字符串中有匹配的,那就寻找===
{
if(a[i] == b[j])
{
j++; //已经匹配的个数
}
else
{
i++; //主字符串的 下标
}
}else{ //已经有匹配的了
if(a[i+1] != b[j]) //如果不下一个字符不相等,那就移动小字符串,这个过程操作起来就是更改小字符串的下标。
{
j = next[j-1];
}
if(a[i+1] == b[j]) //如果相等那就比较下一个字符
{
i++;
j++;
}
}
if(j == m) //如果小字符串全比较完了,那就是全匹配了,完成! 否则大字符串中不存在 小字符串!
return i; //这里返回小字符串在大字符串中最后匹配成功的字符的下标
}
return 0;
}
int main()
{
int next[1000];
string a = "BBC ABCDAB ABCDABCDABDE";
string b = "ABCDABD";
getNext(next,b);
cout<<"kmp:"<<kmp(a,b,next)<<endl;
}
这里的求next数组我们可以用笨办法枚举的方法如下:
void getNext(int next[],string s)
{
memset(next,0,sizeof(next));
for(int i=0; i<s.size(); i++)
{
int ans = 0;
string str = s.substr(0,i+1);
for(int j=0; j<str.size()-1; j++)
{
string str1 = str.substr(0,j+1);
string str2 = str.substr(str.size()-j-1,str.size());
if(str1==str2)
{
ans = j+1;
}
}
next[i] = ans;
}
}
这样很容易懂,和容易写出来,但是效率很慢!所以在网上找到了一个特别精简的方法!
void getnext(string s,int next[])
{
int n = s.size();
memset(next,0,sizeof(next));
int k = 0; //k代表 最大前后缀长度
for(int i = 1,k = 0; i<n; i++)
{
while(k>0 && s[k] != s[i]) //精髓!!!!
{
k = next[k-1];
}
if(s[i] == s[k])
{
k++;
}
next[i] = k;
}
}
k代表的是前一个字符串最长的前后缀长度 ; a[i]是新加上的字符
这里分几种情况:
1、如果s[0]......s[k-1]和s[i-k].......s[i-1]匹配,并且s[i] == s[k] ,那么next[i] = next[i-1]+1就可以了(简单的情况)
2、但是如果s[i] != s[k]呢?就是上面的标记精简的那段代码!
我们应该利用已经得到的next[0]···next[k-1]来求s[0]···s[k-1]这个子串中最大相同前后缀。因为!!在于s[k]已经和s[i]失配了,而且s[i-k] ··· s[i-1]又与s[0] ···s[k-1]相同,看来s[0]···s[k-1]这么长的子串是用不了了,那么我要找个同样也是s[0]打头、s[k-1]结尾的子串即s[0]···s[j-1](j==next[k-1]),看看它的下一项s[j]是否能和s[i]匹配。
用我们一个例子把:
ABADABA[B]
我们已经知道了 ABADABA的 next[6] = 3,也就是k=3,但是s[3]=D不等于s[7]=B,k= next[k-1] = next[2] = 1,再来比较s[1]和s[i],结果相等,那么next[7]=next[1]+1 = 2;