算法训练营 查找算法(字符串模式匹配)

本文探讨了字符串的基本概念,如串、子串、空串和空格串,并详细介绍了BF(暴力穷举)和KMP算法在模式匹配中的应用。通过对比两种算法,展示了KMP算法如何通过预计算next数组避免重复比较,提高搜索效率。实例演示了如何在实际场景中使用这些算法来统计单词频率和定位子串出现位置。
摘要由CSDN通过智能技术生成

字符串模式匹配

  • 串:又叫作字符串,是由零个或多个字符组成的有限序列
  • 对字符串通常用双引号括起来,例如S = "abcdef"S为字符串的名称,双引号里面的内容为字符串的
  • 串长:串中字符的个数,需要注意的是:空格也算一个字符
  • 空串:零个字符的串,串长为0
  • 子串:串中任一连续的字符组成的子序列,被称为该串的子串,原串被称为子串的主串
  • 空格串:全部由空格组成的串为空格串,空格串不是空串

BF算法

  • 模式匹配:子串的定位运算被称为串的模式匹配或串匹配。
  • 假设有两个串ST,设S为主串,也称之为正文串;T为子串,也称之为模式
  • 在主串S中查找与模式T相匹配的子串,如果查找成功,则返回匹配的子串的第1字符在主串中的位置
  • 穷举所有S的所有子串,判断其是否与T匹配,该算法被称为BF(暴力穷举)算法

算法设计

  1. i = 0j = 0,如果S[i] = T[j],则i++j++,继续比较,否则转向下一步
  2. i = 1j = 0,如果S[i] = T[j],则i++j++,继续比较,否则转向下一步
  3. i = 2j = 0,如果S[i] = T[j],则i++j++,继续比较,否则转向下一步
  4. 如果T比较完毕,则返回TS中第1个字符出现的位置
  5. 如果S比较完毕,则返回0,说明TS中未出现

算法实现

#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

输出:

一共比较了154

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
一共比较了104

算法优化

  • 求取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
一共比较了148

训练1 统计单词数

一般文本编辑器都有查找单词的功能,可以快速定位特定单词在文章中的位置,有点还能统计特定单词在文章中出现的次数。给定一个单词,请输出它在给定的文章中出现的次数和第1次出现的位置。匹配单词时,不区分大小写,但要求完全匹配,即给定的单词必须与文章中的某一独立的单词在不区分大小写的情况下完全相同,如果给定的单词仅是文章中某一单词的一部分,则不算匹配。

输入:第1行是一个单词字符串,只包含字母;第2行是一个文章字符串,只包含字母和空格。1 ≤ \leq 单词长度 ≤ \leq 10,1 ≤ \leq 文章长度 ≤ \leq 1000000

输出:如果在文章中找到给定的单词,则输出以空格隔开的两个整数,分别表示单词在文章中出现的次数和第1次出现的位置;如果单词在文章中没有出现,则输出-1

算法设计

  1. 读入单词和文章,首尾分别补空格。
  2. 将单词和文章全部转换为小写字母。
  3. 在文章中查询单词首次出现的位置posfirst,如果查询失败,则输出-1,算法结束。
  4. 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 1s1,s2106,字符串只含大写英文字母。

输出:首先输出若干行,没行一个整数,按从小到大的顺序输出 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的长度

算法设计

  1. 输入字符串 s s s t t t,采用KMP算法从头开始查找 t t t s s s中出现的位置。
  2. 求解字符串 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽星_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值