【算法导论】读书笔记——字符串匹配

本文介绍了字符串匹配的基本概念,包括朴素字符串匹配算法和Rabin-Karp算法。朴素算法通过暴力枚举匹配模式,而Rabin-Karp算法则引入了哈希思想,提高了匹配效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字符串匹配

一. 定义

在日常生活中,我们常常需要在文本中找到某个模式的所有出现位置,比如输入字符串,需要找到出现这个字符串的所有位置,解决这个问题的算法叫做字符串匹配算法。

字符串匹配问题的形式化定义:假设文本是一个长度为n的数组T[1...n],而模式是一个床都为m的数组P[1...m],其中m<=n,进一步假设P和T的元素都是来自一个有限字母集Q的字符,如Q={0,1},或者Q={a,b...z},字符数组P和T称为字符串。如果0<=s<=n-m,并且T[s+1...s+m]=P[1...m],那么称模式P在文本T中出现,且偏移为s。字符串匹配问题就是找到所有的偏移s,使得在该偏移下,所给的模式P出现在给定的文本T中

二. 朴素字符串匹配算法

朴素字符串匹配算法即暴力枚举算法,对于每个偏移s,判断其是否匹配,得到结论。上代码

C++代码

#include<iostream>
#include<vector>
#include<string>
using namespace std;
bool match(string &T,string &P,int s,int m){
    for(int i=0;i<m;i++)
    {
        if(T[i+s]!=P[i])
            return false;
    }
    return true;
}
int naive_string_matcher(string T,string P){
    int n=T.size();
    int m=P.size();
    for(int s=0;s<=n-m;s++)
    {
        if(match(T,P,s,m))
            return s;
    }
    return -1;
}
int main(){
    string T;
    string P;
    cin>>T;
    cin>>P;
    int s;
    s=naive_string_matcher(T,P);
    if(s>=0)
        cout<<s;
    else cout<<"NOT FOUND";
}

在naive_string_matcher函数中有一个for循环,循环n-m+1次,循环内部调用的match函数循环了m次,故在检索中的复杂度为O(m(n-m+1))。这种暴力枚举没有对字符串有任何预处理,而是直接搜素,由于检索复杂度高,并不是一个最好的方法。

三. Rabin-Karp算法

Rabin-Karp算法的预处理时间是O(m),并且在最坏的情况下,它的运行时间为O(m(n-m+1))。基于一些假设,在平均情况下,它的运行时间还是比较好的。

Rabin-Karp算法用到了一些数论的知识,为了不失一般性,假设Q={a,b,...,z},这样所有字符都是a-z之间,如果将其减去‘a',得到0-25的数值,于是字符串就可以当做是d=26进制的连续数字。给定模式P[1...m],假设p是其相应的26进制数字,假设ts表示长度为m的T[s+1...s+m]所对应的26进制数字,其中s=0,1...n-m。当且仅当T[s+1...s+m]=P[1...m]时,ts=p。

为了计算p和t0,我们可以运用霍纳法则,时间复杂度为O(m)。

p=P[m]+d(P[m-1]+d(P[m-2)+...+d(P[2]+dP[1])...))

为了计算剩余的值t1,t2,...,t(n-m),我们可以通过递推公式,在常数时间根据ts计算t(s+1)。

t_{s+1}=d(t_{s}-d^{m-1}T[s+1])+T[s+m+1]

而d^m-1又可以通过反复平方法在O(lgm)的时间内完成,因此可以在O(m)内计算p,在O(n-m+1)计算t0..t(n-m)。于是可以用O(m)的处理时间,和O(n-m+1)的匹配时间内找到所有模式P[1..m]在文本T[1..n]中出现的位置。

然而,如果m数值过大,我们看到中间运算会出现一个以m指数增长的d^(m-1)这么一个大数,于是p和ts的值会大得使得我们无法处理,为了解决这个问题,我们使用哈希映射的方式,选取一个合适的模q来计算p和ts的模,于是过程匹配过程转变为,在O(m)的时间内计算出模q的p值,在O(n-m+1)时间内计算模q的所有ts值,如果选取q为一个素数,使得dq刚好在一个字长以内,那么可以在float中完成所有运算,递归式转变为

t_{s+1}mod q=(d*t_{s}mod q-(d^{m}mod q)*T[s+1])+T[s+m+1]) mod q

实际上就是将这些“大数“用它对q取模的数来替代。而基于模q的结果并不完美,存在这哈希冲突,ts mod q==p mod q不能说明ts==q,但如果ts mod q!=p mod q,则说明ts!=q,可作为一种启发式的方式排除错误的匹配,在满足等号的情况下需要进一步进行一次检测T[s+1...s+m]=P[1...m],如果q足够大,那么这种冲突就越少。

C++代码

#include<iostream>
#include<vector>
#include<stdlib.h>
#include<string.h>
using namespace std;

bool match(char* &T,char* &P,int s,int m){
    for(int i=0;i<m;i++)
    {
        if(T[i+s]!=P[i])
            return false;
    }
    return true;
}
int exponentiation(int a,int b,int n){
    int d=1;
    while(b>0)
    {
        if(b&1) /*最低位为1*/
        {
            d=(d*a)%n;
        }
        b=b>>1;
        a*=a;
    }
    return d;
}
int Rabin_Karp_matcher(char* T,char* P,int d,int q){
    int n=strlen(T);
    int m=strlen(P);
    int *text=(int*)malloc(n*sizeof(int));
    int *pattern=(int*)malloc(m*sizeof(int));
    int *t=(int*)malloc((n-m+1)*sizeof(int));
    for(int i=0;i<n;i++)
        text[i]=T[i]-'a';
    for(int i=0;i<m;i++)
        pattern[i]=P[i]-'a';
    int h=exponentiation(d,m,q);/*反复平方法求数的幂的模*/
    int p=0;
    t[0]=0;
    for(int i=0;i<m;i++)
    {
        p=(d*p+pattern[i])%q;
        t[0]=(d*t[0]+text[i])%q;
    }
    for(int s=0;s<=n-m;s++)
    {
        if(p==t[s])
        {
            if(match(T,P,s,m)) /*启发式策略,当等式满足再匹配检查*/
            {
                free(text);
                free(pattern);
                free(t);
                return s;
            }
        }
        else if(s<n-m)
            t[s+1]=(d*t[s]-h*text[s]+text[s+m])%q; /*递推公式,注:T以1开始,text以0开始*/
    }
    free(text);
    free(pattern);
    free(t);
    return -1;
}
int main(){
    char T[100];
    char P[100];
    cin.getline(T,100);
    cin.getline(P,100);
    int s;
    s=Rabin_Karp_matcher(T,P,26,101); /*随便选取一个素数,这里取101*/
    if(s>=0)
        cout<<s;
    else cout<<"NOT FOUND";
}

可以看到在这种启发式检查中,当q越大,它冲突的几率就越小,可以估计两个随机数对q取模相等的概率是1/q,于是Rabin-Karp算法的期望运行时间为O(n)+O(m(v+n/q)),v是实际偏移量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值