数据结构-串的模式匹配

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表

序号01234
s[i]abcac
部分匹配值00010

那么这个表有什么用,看下图,第一趟匹配和第二趟匹配,可以发现第二趟匹配完全多余,(第一趟匹配已经查完了前三个字符,可以知道主串第二个字符和子串的首字符并不匹配),可以直接进行第三趟匹配(即子串向右滑动三位),再如第三趟匹配和第四趟匹配,可以把子串向右移四位,那么,如何使子串知道应该如何滑动?

观察每一趟已经匹配的部分,再回想一下前缀和后缀,此时就找到已匹配部分的前缀,后缀相同部分串的最大长度,即部分匹配值,可以得出滑动的位数等于 已匹配的位数 - 此时的部分匹配值

next数组:把PM表的值整体向右移一位,第一位变成-1,原先的最后一位舍掉;

next01234
next[i]abcac
-10001

右移一位得到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;
}

如有错误,敬请指正!

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值