1.朴素算法(暴力)
主串:'ababcabcacbab'
子串:'abcac'
算法设计思想:模式串从主串第一个字符开始开始比较,当主串的字符和子串的字符相等时,逐步比较后续字符,若不相等,从主串的下一个字符开始重新比较(这里的主串下一个字符:如第三趟匹配和第四趟匹配这种,表述能力有些差),就这样以此类推,直到子串中的字符和主串的字符全部匹配成功。
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
int Index(string s,string p){
int i=0,k=0,j=0;
while(i<s.length()&&j<p.length()){
if(s[i]==p[j]){
i++;
j++;
}
else{
k++;
i=k;
j=0;
}
}
if(j>=p.length()) return k;
return -1;
}
int main()
{
string s,p;
cin>>s;
cin>>p;
int index = Index(s,p);
cout<<index<<endl;
return 0;
}
时间复杂度分析: 主串长n,子串长m
最好的情况下:O(m) for example 主串:'abcacsdfg' 子串:'abc'
主串的前三个字符正好和子串匹配,一共需要匹配m次即可
不成功匹配时的最好情况: O(n-m+1) 例子:主串:'abcabcabcabc' 子串:'def'
一直匹配到倒数第三个字符发现失败,则匹配失败,代码中没有考虑检测到主串剩余字符不足以继续匹配的情况;
最坏情况下:O(mn) for example 主串:'aaaaaaaaaaaaaaaab' 子串:'aaaab';
为一共匹配了(n-m)*m+m次,时间复杂度可以看为O(nm)
2.串的模式匹配-kmp算法
kmp算法:主串的下标i(也可以说i指针)不往前回溯,匹配失败时移动子串,将子串向右移动。
那么如何移动子串?
首先需要了解前缀、后缀和部分匹配值
举个栗子(主要表达能力太差):串 'ababa'的子串有{a,ab,aba,abab,ababa}
{a},没有前缀后缀,部分匹配值为0
{ab},前缀{a},后缀{b},前缀和后缀没有匹配值,部分匹配值为0
{aba},前缀{a,ab},后缀{a,ba} ,a为前后缀所匹配的,长度为1,部分匹配值为1
{abab},前缀{a,ab,aba},后缀{b,ab,bab},ab是前缀和后缀相同的部分,长度2,部分匹配值2
{ababa},前缀{a,ab,aba,abab},后缀{a,ba,aba,baba},其中a和aba是他们相同的部分,最长的匹配子串为aba长度为3,部分匹配值为3;
可以得出PM表
序号 | 0 | 1 | 2 | 3 | 4 |
s[i] | a | b | c | a | c |
部分匹配值 | 0 | 0 | 0 | 1 | 0 |
那么这个表有什么用,看下图,第一趟匹配和第二趟匹配,可以发现第二趟匹配完全多余,(第一趟匹配已经查完了前三个字符,可以知道主串第二个字符和子串的首字符并不匹配),可以直接进行第三趟匹配(即子串向右滑动三位),再如第三趟匹配和第四趟匹配,可以把子串向右移四位,那么,如何使子串知道应该如何滑动?
观察每一趟已经匹配的部分,再回想一下前缀和后缀,此时就找到已匹配部分的前缀,后缀相同部分串的最大长度,即部分匹配值,可以得出滑动的位数等于 已匹配的位数 - 此时的部分匹配值
next数组:把PM表的值整体向右移一位,第一位变成-1,原先的最后一位舍掉;
next | 0 | 1 | 2 | 3 | 4 |
next[i] | a | b | c | a | c |
值 | -1 | 0 | 0 | 0 | 1 |
右移一位得到next数组的原因:使用部分匹配值时,每当匹配失败,就找它前一个元素的部分匹配值,所以不妨将pm表右移一位,这样哪个元素匹配失败,直接看它自己的部分匹配值即可;
第一位改为-1的原因,表示此时表示子串的第一个字符和主串的字符匹配失败,只需要将主串的i加一位即可,不需要计算移动的位数
最后一位舍掉的原因,最后一位元素的部分匹配值是给下一个元素使用的,显然没有下一个元素;
有时为了方便还会将next数组整体+1
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
#define MaxSize 100
string s;
string p;
int next[MaxSize];
void getNext(string p,int *next){ //字符串的下标从0开始,和下标从1开始略有不同
int i=0,j=-1;
next[0] = -1;
while(i<p.length()){
if(j==-1||p[i]==p[j]){
i++;j++;
next[i] = j;
}
else{
j = next[j];
}
}
}
int kmp(string s,string p){
int i=0,j=0;
while(i<s.length()&&j<p.length()){
if(j==-1||s[i]==p[j]){
i++;
j++;
}
else{
j = next[j];
}
}
if(j>=p.length())
return i - p.length();
else
return -1;
}
int main()
{
cin>>s;
cin>>p;
getNext(p,next);
for(int i=0;i<p.length();i++){ //下标从0开始的next数组
cout<<next[i]<<" ";
}
cout<<endl;
int index = kmp(s,p);
cout<<index<<endl;
}
3.kmp算法的进一步优化
如:主串:'aaabaaaab',子串:'aaaab'
next数组的值为:-1 0 1 2 3,当s[4]和p[4],即i = 4,j = 4时,并不匹配,此5时还需要进行s4和p2,p1,p0依次比较,但是和p2和p1的比较并不是必须的,前三个字符完全相同,p2,p1的比较没啥意义,问题的原因是啥呢?
此时也可以看做p[j]=p[next[j]],当p[j]!=s[j]时,下次匹配就是s[j]和p[next[j]]比较,如果p[j]=p[next[j]],那么相当于用一个和p[j]相等的字符和s[j]比较,这样一点意义没有,所以当出现这种情况时,应该使得next[j] = next [ next [ j ] ]
#include<iostream>
#include<string>
using namespace std;
#define NUM 100
string s;
string p;
int nextval[NUM];
void getNextval(string p,int *nextval){
int j = -1;
int i = 0;
nextval[0] = -1;
while(i<p.length()){
if(j==-1||p[i]==p[j]) {
i++;
++j;
if(p[i]==p[j]){
nextval[i] = nextval[j];
}
else{
nextval[i] = j;
}
}//前缀比后缀
else{
j = nextval[j];
}
}
}
int kmpNextval(string s,string p){
int i=0,j=0;
while(i<s.length()&&j<p.length()){
if(j==-1||s[i]==p[j]){
j++;
i++;
}
else
j = nextval[j];
}
cout<<"aaa "<<i<<" aa"<<endl;
if(j>=p.length())
return i-p.length();
else
return -1;
}
int main()
{
cin>>s;
cin>>p;
getNextval(p,nextval);
for(int i=0;i<p.length();i++){
cout<<nextval[i]<<" ";
}
cout<<endl;
int index = kmpNextval(s,p);
cout<<index<<endl;
return 0;
}
如有错误,敬请指正!