字符串模式匹配
- 串:又叫作字符串,是由零个或多个字符组成的有限序列
- 对字符串通常用双引号括起来,例如
S = "abcdef"
,S
为字符串的名称,双引号里面的内容为字符串的值 - 串长:串中字符的个数,需要注意的是:空格也算一个字符
- 空串:零个字符的串,串长为0
- 子串:串中任一连续的字符组成的子序列,被称为该串的子串,原串被称为子串的主串
- 空格串:全部由空格组成的串为空格串,空格串不是空串
BF算法
- 模式匹配:子串的定位运算被称为串的模式匹配或串匹配。
- 假设有两个串
S
、T
,设S
为主串,也称之为正文串;T
为子串,也称之为模式 - 在主串
S
中查找与模式T
相匹配的子串,如果查找成功,则返回匹配的子串的第1字符在主串中的位置 - 穷举所有
S
的所有子串,判断其是否与T
匹配,该算法被称为BF(暴力穷举)算法
算法设计
i = 0
,j = 0
,如果S[i] = T[j]
,则i++
,j++
,继续比较,否则转向下一步i = 1
,j = 0
,如果S[i] = T[j]
,则i++
,j++
,继续比较,否则转向下一步i = 2
,j = 0
,如果S[i] = T[j]
,则i++
,j++
,继续比较,否则转向下一步- …
- 如果
T
比较完毕,则返回T
在S
中第1个字符出现的位置 - 如果
S
比较完毕,则返回0,说明T
在S
中未出现
算法实现
#include<iostream>
#include<string>
using namespace std;
int BF(string s,string t,int pos){
int i=pos,j=0,sum=0;
int slen=s.length();
int tlen=t.length();
while(i<slen&&j<tlen){
sum++;
if(s[i]==t[j])//如果相等,则继续比较后面的字符
i++,j++;
else{
i=i-j+1; //i回退到上一轮开始比较的下一个字符
j=0; //j回退到第1个字符
}
}
cout<<"一共比较了"<<sum<<"次"<<endl;
if(j>=tlen) // 匹配成功
return i-tlen+1;
else
return 0;
}
int main(){
string s,t;
cin>>s>>t;
cout<<BF(s,t,0)<<endl;
return 0;
}
输入:
abaabaabeca
abaabe
输出:
一共比较了15次
4
KMP算法
- 实际上,完全没有必要从S的每一个字符开始暴力穷举每一种情况,对该算法进行改进称为KMP算法。
算法实现
#include<iostream>
#include<string>
using namespace std;
int slen,tlen,nex[1000+5];
void get_next(string t);//求模式串T的nex函数
int KMP(string s,string t,int pos);//KMP字符串匹配算法
int main(){
string s,t;
cin>>s>>t;
cout<<KMP(s,t,0)<<endl;
return 0;
}
void get_next(string t){//求模式串T的nex函数
int j=0,k=-1;
nex[0]=-1;
while(j<tlen){//模式串t的长度
if(k==-1||t[j]==t[k])
nex[++j]=++k;
else
k=nex[k];
}
for(int i=0;i<=tlen;i++)
cout<<nex[i]<<" ";
cout << endl;
}
int KMP(string s,string t,int pos){
int i=pos,j=0,sum=0;
slen=s.length();
tlen=t.length();
get_next(t);
while(i<slen&&j<tlen){
sum++;
if(j==-1||s[i]==t[j])//如果相等,则继续比较后面的字符
i++,j++;
else
j=nex[j];//j回退到nex[j]
}
cout<<"一共比较了"<<sum<<"次"<<endl;
if(j>=tlen) // 匹配成功
return i-tlen+1;
else
return -1;
}
输入:
abaabaabeca
abaabe
输出:
-1 0 0 1 1 2 0
一共比较了10次
4
算法优化
- 求取
next[]
数组的优化函数get_next2
。
#include<iostream>
#include<string>
using namespace std;
int slen,tlen,nex[1000+5];
void get_next2(string t);//求模式串T的nex函数
int KMP(string s,string t,int pos);//KMP字符串匹配算法
int main(){
string s,t;
cin>>s>>t;
cout<<KMP(s,t,0)<<endl;
return 0;
}
void get_next2(string t){ //改进的nex函数
int j=0,k=-1;
nex[0]=-1;
while(j<tlen){//模式串t的长度
if(k==-1||t[j]==t[k]){
j++,k++;
if(t[j]==t[k])
nex[j]=nex[k];
else
nex[j]=k;
}
else
k=nex[k];
}
for(int i=0;i<=tlen;i++)
cout<<nex[i]<<" ";
cout << endl;
}
int KMP(string s,string t,int pos){
int i=pos,j=0,sum=0;
slen=s.length();
tlen=t.length();
get_next2(t);
while(i<slen&&j<tlen){
sum++;
if(j==-1||s[i]==t[j])//如果相等,则继续比较后面的字符
i++,j++;
else
j=nex[j];//j回退到nex[j]
}
cout<<"一共比较了"<<sum<<"次"<<endl;
if(j>=tlen) // 匹配成功
return i-tlen+1;
else
return -1;
}
输入:
aabaaabaaaabea
aaaab
输出:
-1 -1 -1 -1 3 0
一共比较了14次
8
训练1 统计单词数
一般文本编辑器都有查找单词的功能,可以快速定位特定单词在文章中的位置,有点还能统计特定单词在文章中出现的次数。给定一个单词,请输出它在给定的文章中出现的次数和第1次出现的位置。匹配单词时,不区分大小写,但要求完全匹配,即给定的单词必须与文章中的某一独立的单词在不区分大小写的情况下完全相同,如果给定的单词仅是文章中某一单词的一部分,则不算匹配。
输入:第1行是一个单词字符串,只包含字母;第2行是一个文章字符串,只包含字母和空格。1 ≤ \leq ≤ 单词长度 ≤ \leq ≤ 10,1 ≤ \leq ≤ 文章长度 ≤ \leq ≤ 1000000
输出:如果在文章中找到给定的单词,则输出以空格隔开的两个整数,分别表示单词在文章中出现的次数和第1次出现的位置;如果单词在文章中没有出现,则输出-1
算法设计
- 读入单词和文章,首尾分别补空格。
- 将单词和文章全部转换为小写字母。
- 在文章中查询单词首次出现的位置
posfirst
,如果查询失败,则输出-1,算法结束。 - 令
t = posfirst_len1-1
,出现的次数cnt = 1
。如果t<len2
,则从t
位置开始在文章中查找单词,如果匹配成功,t = BF(word,sentence,t)
,则cnt++
,更新t = t + len1-1
,继续搜索
算法实现
#include<iostream>
#include<cctype>
#include <cstring>
using namespace std;
int len1,len2;//两个字符串长度
void tolower(char *a); //全部大写转小写
int BF(char *w,char *s,int pos); //模式匹配BF算法
int main(){
char word[16],sentence[1000010];
cin.getline(word+1,16);//输入时,0单元空出来不存储
cin.getline(sentence+1,1000005);
word[0]=' ';//首尾补空格
len1=strlen(word);
word[len1++]=' ';
word[len1]='\0';
sentence[0]=' ';//首尾补空格
len2=strlen(sentence);
sentence[len2++]=' ';
sentence[len2]='\0';
tolower(word); //全部转换为小写
tolower(sentence);
int posfirst=BF(word,sentence,0);//记录单词首次出现的位置
if(posfirst==-1){
cout<<-1;
return 0;
}
int cnt=1;//能走到这说明单词已出现一次了
int t=posfirst+len1-1;
while(t<len2){
t=BF(word,sentence,t);
if(t==-1)
break;
cnt++;
t=t+len1-1; //更新查找起点
}
cout<<cnt<<" "<<posfirst;
return 0;
}
void tolower(char *a){
for (int i = 0; a[i]; ++i) {
if(isupper(a[i])){
a[i]+=32;
}
}
}
int BF(char *w,char *s,int pos){
int i=pos;
int j=0;//下标从0开始
while(j<len1&&i<len2){
if(s[i]==w[j]){
i++;
j++;
}
else{
i=i-j+1;
j=0;
}
}
if(j>=len1)//匹配成功
return i-len1;
return -1;
}
训练2:KMP字符串匹配
题目描述
给定两个字符串 s 1 s_1 s1和 s 2 s_2 s2,若 s 1 s_1 s1的 [ l , r ] [l,r] [l,r]区间的子串与 s 2 s_2 s2完全相同,则称 s 2 s_2 s2在 s 1 s_1 s1中出现了,其出现位置为 l l l。请求出 s 2 s_2 s2在 s 1 s_1 s1中所有出现的位置。定义一个字符串 s s s的 b o r d e r border border为 s s s的一个非 s s s本身的子串 t t t,满足 t t t既是 s s s的前缀,又是 s s s的后缀。对于 s 2 s_2 s2,还需要求出对于每个前缀的最长 b o r d e r border border的长度。
输入:第1行为字符串 s 1 s_1 s1:第二行为字符串 s 2 s_2 s2。 1 ≤ ∣ s 1 ∣ , ∣ s 2 ∣ ≤ 1 0 6 1 \leq |s_1|,|s_2| \leq 10^6 1≤∣s1∣,∣s2∣≤106,字符串只含大写英文字母。
输出:首先输出若干行,没行一个整数,按从小到大的顺序输出 s 2 s_2 s2在 s 1 s_1 s1中出现的位置。最后后一行输出 ∣ s 2 ∣ |s_2| ∣s2∣个整数,第i个整数表示 s 2 s_2 s2的长度为i的前缀的最长 b o r d e r border border的长度
算法设计
- 输入字符串 s s s和 t t t,采用KMP算法从头开始查找 t t t在 s s s中出现的位置。
- 求解字符串 t t t的 n e x t [ ] next[] next[]数组。 n e x t [ i ] next[i] next[i]表示 t t t的长度为 i i i的前缀的最长 b o r d e r border border的长度
算法实现
#include<iostream>
#include<string>
using namespace std;
int slen,tlen;
int nex[1000000+5];
void get_nex(string t){//求模式串T的nex函数值
int j=0,k=-1;
nex[0]=-1;
while(j<tlen){//模式串t的长度
if(k==-1||t[j]==t[k])
nex[++j]=++k;
else
k=nex[k];
}
}
void KMP(string s,string t){
int i=0,j=0;
slen=s.length();
tlen=t.length();
get_nex(t);
while(i<slen){
if(j==-1||s[i]==t[j]){//如果相等,则继续比较后面的字符
i++;
j++;
}
else
j=nex[j]; //j回退到nex[j]
if(j==tlen){ //匹配成功
cout<<i-tlen+1<<endl;
j=nex[j];//不允许重叠,j从0重新开始,如果允许重叠,j=nex[j]
}
}
}
int main(){
string s,t;
cin>>s>>t;
KMP(s,t);
for(int i=1;i<=tlen;i++)
cout<<nex[i]<<" ";
return 0;
}
输入:
ABABABC
ABA
输出:
1
3
0 0 1