典型字符串匹配算法实现 - 单字符串匹配算法

博客源址:http://www.jimye.com/dian-xing-zi-fu-chuang-pi-pei-suan-fa-shi-xian/

提示:要继续向下看

相信大家对快捷键ctrl+F是做什么用的都应该很熟悉了,无论是文本编辑、网页浏览等程序上它都意味着字符串搜索,我们提供一个关键字,它将找到当前页面上的所有该关键字所在的位置。关键字称为模式串,在文本T中寻找模式串P出现的所有出现的位置,解决这种问题的算法叫做字符串匹配算法。字符串匹配算法可以说是计算机科学中最古老、研究最广泛的问题之一,并且字符串匹配的应用也随处可见,特别是信息检索领域和计算生物学领域。

字符串匹配算法有很多很多,多到可以出一本书了《柔性字符串匹配》...感兴趣的同学可以查阅一下这本书。

其中最著名可算是Knuth-Morris-Pratt(KMP)算法和Boyer-Moore(BM)算法,因为这两个经典算法都将匹配算法的时间复杂度的理论值从O(m*n)降到了线性的O(m+n),由此促使我好好研究了一下。除了经典算法,简洁但不失效率的较新的Horspool算法和Sunday算法也是我不可错过的。接下来先从最最最简单的朴素匹配算法开始讲起吧:

( 注:文中的算法程序,输入文本T、模式串P,打印出P在T中的所有位置起点。)

1、朴素匹配算法

尽管很多资料上将最简单的匹配算法成为暴力匹配,但我仍然喜欢算法导论上的叫法,朴素的匹配算法。

文本T长度为m,模式串P长度为n。算法从文本第1位从左向右开始与模式串P进行匹配,无论是否匹配成功,模式串都后移1位开始继续进行重新匹配,总共进行m-n+1次匹配。算法极其简单,因此效率极其有限,时间复杂度为O(m * n),故不常被用。

图例:

第一步:

第二步:(模式串后移1位)

第三步、第四步...

代码实现:

// 1.朴素的字符串匹配算法
void NaiveMatcher(const char *T, const char *P)
{
    int m = strlen(T);
    int n = strlen(P);  

    for (int i=0; i<=m-n; i++)
    {
        int j;
        for (j=0; j<n; j++)
            if (T[i+j] != P[j]) break;  

        if (j==n) cout<<i<<" ";
    }
    cout<<endl;
}

2、KMP算法

要提高匹配算法的速度,关键点就在于每次匹配结束后,模式串需要向后偏移的位数。朴素匹配算法中不管三七二十一,每次就移一位,效率低下。所以人们开始思考是否能够通过之前的匹配结果产生某些知识,使得每次向后偏移的位数尽量得大来获得高效性。

后来人们成功了,最有名的的便是Knuth、Morris、Pratt三个人设计的线性时间字符串匹配算法,俗称KMP算法,在学校的算法课中必备,在算法导论中也是字符串匹配章节里压轴的。这个算法被人骂很难理解,个人但弄明白之后其实还是挺好懂得。算法通过匹配成功的模式串P的前缀来得到每次向后偏移的位数。

  1. 首先,算法需要对模式串P进行预处理,得到一个部分匹配表kt:

    表中kt[i]等于字符串P[0-i]的前缀和后缀的最长的共有元素的长度。(例如p[0-3] = "abab",它的前缀有”a“,”ab“,”aba“,后缀有”b“,”ab“,”bab“,共有元素为”ab“,其长度为2,所以kt[3]等于2)。

  2. 然后每次开始从左向右匹配,因此此类算法隶属于基于前缀搜索的方法:

    2.1 若前k位匹配成功,则 后移位数s = 已匹配的字符数k - 部分匹配表对应值kt[k]:

    2.2 若第1位就匹配不成功,则 后移1位。

代码实现:

// 2.KMP算法
int* PartialMatchTable(const char *P)
{
    int m = strlen(P);
    int *kt = new int[m];
    kt[0] = 0;  

    for (int i = 1, k = 0; i < m; ++i)
    {
        while (k>0 && P[k]!=P[i])
            k = kt[k];
        if (P[k] == P[i])
            k = k+1;
        kt[i] = k;
    }
    return kt;
}  

void KmpMatcher(const char *T, const char *P)
{
    int m = strlen(T);
    int n = strlen(P);
    int *kt = PartialMatchTable(P);  

    int i=0, k=0;
    while (i<=m-n)
    {
        while (k<n && T[i+k] == P[k])
            k++;  

        if (k==0) {
            k = 0;
            i = i+1;
        }else
        {
            if (k==n) cout<<i<<" ";
            i = i+k-kt[k-1];
            k = kt[k-1];
        }
    }
    cout<<endl;
    delete []kt;
}

KMP算法分为两个步骤,先通过P计算出部分匹配表,时间复杂度为O(n);再通过此表进行匹配位移,时间复杂度为O(m),故理论上的总时间复杂度为O(m+n),达到线性效率。理论效率高,但实际上却未特别突出。

参考文章:

  1. 《字符串匹配的KMP算法》
  2. 《算法导论》第32章"字符串匹配"

3、Boyer-Moore算法

Boter-Moore又是一个经典算法,传说中比KMP还快,由Bob Boyer与J Strother Moore在1977年提出。

与KMP有两个不同点:①每次匹配时,匹配方向不同,BM算法每次从右向左匹配,此类算法隶属于基于后缀搜索的方法。②匹配后向后偏移的位数由两种计算方式得到:坏字符偏移和好后缀偏移。下面详细介绍一下这两种方式:

上图中,文本中匹配成功的“AGAG”称为“好后缀”,文本中匹配失败的“C”称为“坏字符”。

一、坏字符偏移

当出现一个坏字符时,从模式串中找到最靠右的这个字符,将其位移到与坏字符相对,然后继续匹配。此时会有两种情况:

  • ①模式串中存在坏字符,则

  • ②模式串中不存在坏字符,则将模式串移动到坏字符后一位

二、好后缀偏移

在模式串中找到除“好后缀”外,最靠右的与“好后缀”相同的子串,且子串前一位字符不等于“好后缀”前一位字符,则将此子串位移到与“好后缀”对应,然后继续匹配。此时会有三种情况:

  • ①模式串存在除“好后缀”外,最靠右的与“好后缀”相同的子串,且子串前一位字符不等于“好后缀”前一位字符,则

  • ② 模式串不存在上述子串,但模式串的前缀存在与“好后缀”的后缀相同的字符串,则此时将此相同字符串位移对应

  • ③模式串不存在上述各种子串,则将模式串后移自己的长度n

**BM算法依然分为两步:

  1. 根据模式串P先计算出坏字符表bc和好后缀表gs,至于如何高效计算出两个表,其中gs表需要先求后缀表suff,详细请参考下面的参考文章,几位大牛已经讲得很详细了。图例:

  2. 根据坏字符表bc和好后缀表gs,依次匹配位移。

**BM总位移量计算:

IF 匹配完全成功
  根据好后缀偏移。
ELSE
  计算坏字符偏移量和好后缀偏移量,选择较大的偏移量进行偏移。

代码实现:

// 3.Boyer-Moore算法
int* BadCharacter(const char *P)
{
    int m = strlen(P);
    int *bc = new int[256];
    for (int i=0; i<256; i++)
        bc[i] = m;
    for (int i=0; i<m; i++)
        bc[(int)P[i]] = m - i - 1;
    return bc;
}  

int* suffixes(const char *P)
{
    int m = strlen(P);
    int f = 0, g = m-1;
    int *suff = new int[m];
    for (int i=0; i<m; i++)
        suff[i] = 0;
    suff[m-1] = m;  

    for (int i=m-2; i>=0; --i)
    {
        if (i>g && suff[i+m-1-f]<i-g)
            suff[i] = suff[i+m-1-f];
        else
        {
            if (i<g)
                g = i;
            f = i;
            while (g>=0 && P[g] == P[g+m-1-f])
                g--;
            suff[i] = f - g;
        }
    }
    return suff;
}  

int* GoodSuffix(const char *P)
{
    int m = strlen(P);
    int *suff = suffixes(P);
    int *gs = new int[m];  

    for (int i=0; i<m; i++)
        gs[i] = m;  

    for (int i=m-1, j=0; i>=0; i--)
    {
        if (suff[i] == i+1)
        {
            for (j=0; j<m-1-i; j++)
                if (gs[j] == m)
                    gs[j] = m-1-i;
        }
    }  

    for (int i=0; i<=m-2; i++)
        gs[m-1-suff[i]] = m-1-i;  

    delete []suff;
    return gs;
}  

void BoyerMooreMatcher(const char *T, const char *P)
{
    int m = strlen(T);
    int n = strlen(P);
    int *bc = BadCharacter(P);
    int *gs = GoodSuffix(P);  

    int i=0, k=n-1;
    while (i<=m-n)
    {
        for (k = n-1; k>=0 && T[i+k] == P[k]; k--);  

        if (k==-1)
        {
            cout<<i<<" ";
            i = i + gs[0];
        } else {
            int offset = bc[(int)T[i+k]] - m + 1 + k;
            if (offset < gs[k]) offset=gs[k];
            i = i + offset;
        }
    }
    cout<<endl;  

    delete []bc;
    delete []gs;
}

BM算法计算坏字符表bc和好后缀表gs需要的时间复杂度最快都为O(n),第二步匹配位移时间复杂度为O(m),所以总的时间复杂度依旧为O(m+n),但理论上要比KMP快、快、快。。。

**参考文章:

1.《Boyer-Moore算法学习》

2.《Boyer-Moore算法详解》

4、Horspool算法

看完了两种理论上很经典的算法,是不是觉得有点头疼那,那么...现在忘了它们吧,为啥?因为,它们太复杂了,在字符串匹配研究领域中的一人所共知的事实便是“算法的思想越简单,实际应用效果越好”。KMP算法看起来很牛逼吧,但它在实际应用中比朴素算法还要慢一倍,BM算法牛逼吧,但其高度简化的后版本在实际中比它本身要快上很多。所以,让我们把该死的KMP算法和BM算法给忘了吧,来认识两种既简单又高效的字符串匹配算法。

首先介绍的是Horspool算法,它便是首个对BM算法进行简化的算法。它认为原BM算法中坏字符算法总能产生更大的偏移位移,于是它对坏字符算法进行了小修改,使其易于计算并能产生更大的偏移位移。

在BM算法中“坏字符”的定义是,已经匹配串前一位的文本字符,而在Horspool算法中,我们考虑的是自右向左匹配后匹配成功串的最后一个字符,然后在模式串P中找到非尾最靠右的这个字符,将其位移到对应位置。此时也会出现两种情况:

  1. 模式串P中能找到这个字符,则位移到对应位置

    此例中匹配串为“cd”,最后一位为“d”,找到P中最靠右且非尾的字符“d”,将其位移到对应位置后继续匹配)

  2. 模式串中不能找到这个字符,则位移n位

    因此,本算法同样分两步:第一步先通过P计算出“坏字符”表的变异版本,然后通过此表依次匹配位移。简单吧,比什么KMP和BM算法简单和好记多了,代码也简单。

实现代码:

// 4.Horspool算法
int* HorspoolTable(const char *P)
{
    int n = strlen(P);
    int *ht = new int[256];
    for (int i=0; i<256; i++)
        ht[i] = n;
    for (int i=0; i<n-1; i++)
        ht[(int)P[i]] = n - i - 1;
    return ht;
}  

void Horspool(const char *T, const char *P)
{
    int m = strlen(T);
    int n = strlen(P);
    int *ht = HorspoolTable(P);  

    int i=0, k=n-1;
    while (i<=m-n)
    {
        for (k=n-1; k>=0 && T[i+k]==P[k]; k--) ;  

        if (k==-1)
            cout<<i<<" ";
        i = i + ht[(int)T[i+n-1]];
    }
    cout<<endl;
    delete []ht;
}

5、Sunday算法

再来说一个好算法,Sunday算法,它与上面说的Horspool很类似,唯一不同点在于,Horspool利用的是匹配串的最后一位字符,而Sunday用得事匹配窗口后面的一位字符,这样可以带来更大的平均移动距离。尽管它的移动距离要长一些,但是“展开”的Horspool具有更少的内存引用次数,因而一般来说比Horspool算法要更慢一些。

下面依旧用图示说明两种情况:

  1. 无论匹配是否成功,模式串P中能找到匹配窗口后一位的文本字符,则位移到对应位置

  2. 模式串P中不能找到匹配窗口后一位的文本字符,则位移到此字符之后

实现代码:

// 5.Sunday算法
int* SundayTable(const char *P)
{
    int n = strlen(P);
    int *st = new int[256];
    for (int i=0; i<256; i++)
        st[i] = n;
    for (int i=0; i<n; i++)
        st[(int)P[i]] = n - i - 1;
    return st;
}  

void Sunday(const char *T, const char *P)
{
    int m = strlen(T);
    int n = strlen(P);
    int *st = SundayTable(P);  

    int i=0, k=0;
    while (i<=m-n)
    {
        for (k=0; k<n && T[i+k]==P[k]; k++) ;  

        if (k==n)
        {
            cout<<i<<" ";
            if ((i+n) > m)
                break;
        }  

        i = i + st[(int)T[i+n]] + 1;
    }
    cout<<endl;  

    delete []st;
}

6、总结

以上介绍的其实都是单字符串匹配算法,除了介绍的几种之外此类算法还有Shift-And/Shift-Or算法、BNDM算法、BOM算法等;除了这类,还有像Multiple Shift-And算法、Aho-Corasick算法、Set Horspool算法等等多字符串匹配的算法。这个领域水很深那,对于我来说还没那个必要去研究那么多,因为暂时用不上,现阶段知道几个简单高效的性价比高的单字符串匹配算法就ok了,比如Horspool和Sunday算法,因为在实际应用中编码效率和程序执行效率才是王道。

时间: 2024-12-16 14:33:10

典型字符串匹配算法实现 - 单字符串匹配算法的相关文章

图解字符串的朴素模式匹配算法

复习串的朴素模式匹配算法 模式匹配 : 子串定位运算,在主串中找出子串出现的位置. 在串匹配中,将主串 S 称为目标(串),子串 T 称为模式(串).如果在主串 S 中能够找到子串 T, 则称匹配成功,返回 第一个 和 子串 T 中 第一个字符 相等 的 字符 在主串 S 中的 序号,否则,称匹配失败,返回 0. 算法思想: 从主串 S 的第 pos 个字符起和模式 T 的第一个字符比较之,若相同,则两者顺次的去比较后续的每一个字符,否则从主串 S 的下一个字符起再重新和模式 T 的字符比较之.

字符串的朴素模式匹配算法

#include <stdio.h> #include <string.h> //返回第一个子串在主串的位置,找不到返回-1 int StrMatch(char *source, char *match){ int slen=strlen(source); int mlen=strlen(match); int i=0,j=0; while(i<slen && j<mlen){//当主串或者子串全部匹配完,就退出循环 if(source[i] == ma

字符串匹配算法KMP算法

数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多多余的不符合的匹配做了冗余的比较.假设源字符串长n,字串长m 该算法最差时间复杂度为 m*(n-m+1),记为O(n*m);这里不做过多解释朴素匹配算法. KMP算法: kmp算法不是在源字符串中下手,他是从字串下手,比如我要在源字符串(acabaabaabcacaabc)中匹配一个字符串字串(ab

Data Structure 之 KMC字符串匹配算法

有关模式函数值next[i]确实有很多版本啊,在另外一些面向对象的算法描述书中也有失效函数 f(j)的说法,其实是一个意思,即next[j]=f(j-1)+1,不过还是next[j]这种表示法好理解啊: KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法.简单匹配算法的时间复杂度为O(m*n);   KMP匹配算法的时间复杂度为O(m+n).. 一.简单匹配算法 先来看一个简单匹配算法的函数:(C代码) int Index_BF ( char S[], char T[],

4-4-串的KMP匹配算法-串-第4章-《数据结构》课本源码-严蔚敏吴伟民版

课本源码部分 第4章  串 - KMP匹配算法 ——<数据结构>-严蔚敏.吴伟民版        源码使用说明  链接??? <数据结构-C语言版>(严蔚敏,吴伟民版)课本源码+习题集解析使用说明        课本源码合辑  链接??? <数据结构>课本源码合辑        习题集全解析  链接??? <数据结构题集>习题解析合辑        本源码引入的文件  链接? Status.h.SequenceString.c        相关测试数据下载

[考研系列之数据结构]线性表之字符串

基本概念 串(字符串)  由0个或多个字符组成的有限序列,例如s="hello world" 串名  上例中的s 子串  某串任意连续字符组成的子序列,称为此字符串的子串 空串  0个字符的串,s="" 空格串  由一个或多个字符组成的串 模式匹配算法 作用 定位某子串T在字符串S中的位置 主串 S 模式串  T 针对模式匹配算法从简到难我们需要了解两种算法: [1] 朴素的模式匹配算法 [2] KMP匹配算法 朴素的模式匹配算法: 所谓朴素就是简单,这是一种简单的

字符串模式匹配的几种算法

1.KMP算法 KMP算法程序看起来比较简单,但是求next数组的过程还是比较难理解,next数组实质就是求最大的前后缀,该算法的复杂度是O(m+n),算法流程如下: 假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符: 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j].此举意味着失配时,模式串P相对于文本串S向右移动了

KMP字符串模式匹配详解

KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法.简单匹配算法的时间复杂度为O(m*n);KMP匹配算法.可以证明它的时间复杂度为O(m+n).. 一.简单匹配算法 先来看一个简单匹配算法的函数: int Index_BF ( char S [ ], char T [ ], int pos ) { /* 若串 S 中从第pos(S 的下标0≤pos个字符 起存在和串 T 相同的子串,则称匹配成功,返回第一个 这样的子串在串 S 中的下标,否则返回 -1    */ int

(转)KMP字符串模式匹配详解

(转)KMP字符串模式匹配详解 个人觉得这篇文章是网上的介绍有关KMP算法更让人容易理解的文章了,确实说得很“详细”,耐心地把它看完肯定会有所收获的--,另外有关模式函数值next[i]确实有很多版本啊,在另外一些面向对象的算法描述书中也有失效函数 f(j)的说法,其实是一个意思,即next[j]=f(j-1)+1,不过还是next[j]这种表示法好理解啊: KMP字符串模式匹配详解 KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法.简单匹配算法的时间复杂度为O(m*n)