Loading [MathJax]/jax/input/TeX/jax.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >串匹配算法

串匹配算法

作者头像
努力努力再努力F
发布于 2019-10-10 09:37:27
发布于 2019-10-10 09:37:27
1.6K00
代码可运行
举报
文章被收录于专栏:fangyangcoderfangyangcoder
运行总次数:0
代码可运行

前言

串匹配问题是解决许多应用(文本编辑器,数据库检索,C++模板匹配,模式识别等等)的重要技术。

这个问题有两个输入,第一个是文本(Text),第二个是模式(Pattern),目的是要在文本中寻找模式。通常而言文本要远大于模式。

T : now is the time for all good people to come (长度为n)

P :people (长度为m)

串匹配问题可分为四种类型:

  • detection : P是否出现?
  • location : P首次出现在哪里?
  • counting : P出现了多少次?
  • enumeration : 各出现在哪里?

显然,解决location是最重要的,如果监测到了,就表明出现了(detection),出现多少次,只要将未比较的字符串根据同样的方法求得下一次首次出现的位置,直到整个文本结束,出现在哪里只要记录位置做标记即可。

下面开始介绍串匹配算法。

暴力匹配

思想是自左而右,以字符为单位,依次移动模式串,直到某个位置发生匹配。

这个算法最好的情况是第一次就比对成功,最好情况的上边界则是每次比对时,第一个字符都不匹配,这样就移动一格,最好情况的复杂度就等于

(Omega(n))

, n为文本的长度。最坏的情况是每次比较模式最后一个字符的时候才发现不匹配,这样就会导致最坏情况,时间复杂度为

(mathcalO(ncdotm))

.

C++实现版本1:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int match(string P, string T) {
    size_t n = T.size(), i = 0;
    size_t m = P.size(), j = 0;
    while (i < n - m + 1 && j < m)     //自左向右逐次比较
        if ( T[i] == P[j]) { i++; j++;}  // 若匹配,则转到下一对字符
        else               { i -= j - 1; j = 0;}  // 否则,T回退,P复位
    return i - j;
}

C++实现版本2:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int match(string P, string T) {
    size_t n = T.size(), i;
    size_t m = P.size(), j;
    for ( i = 0; i < n - m + 1; i++) {  //T[i]与P[0]对齐
        for ( j = 0; j < m; j++)        //逐次匹配
            if ( T[i+j] != P[j]) break; //失配则转到下一位置
        if ( m <= j) break;             //匹配成功,退出,返回i
    }
    return i;
}

两个实现版本的返回值都是位置信息,当i等于n - m + 1的时候说明未找到模式,否则就是找到了。

KMP :模式记忆

暴力匹配算法存在着冗余的问题,当最坏情况时,最后一个字符匹配失败,模式串和文本串的指针都要发生回退。

KMP算法的原理是利用Pattern构建一个查询表,根据查询表进行来指导移动位数,并且文本的索引不需要回退。理解这种算法我推荐阮一峰老师的KMP博客(真心推荐看看),讲得非常清晰,非常直观。

假设你看过阮老师的博客知道原理了,现在来看next表的构建代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
vector<int> buildNext(string P) { //构造模式串P的next表
    size_t m = P.size(), j = 0;   //“主”串指针
    vector<int> N(m, 0);          //next表
    int t = N[0] = -1;            //模式串指针(通配符*)
    while ( j  < m - 1 )          //j是不会减小的,j会在循环内变为m-1,此时退出
        if ( 0 > t || P[j] == P[t] ) { //当出现通配符也就是0>t, 当前j自加1,next表对应j为0。
                                       //当不是通配符时,比较是否相等,相等则next表对应j自加1
            j++; t++;
            N[j] = t;
        }
        else
            t = N[t];  //失配,根据前面得到的next,来看应该从那里开始比较,比如下面的匹配等于4的时候,e不等于c,查表知e所在的位置为0,也就是没有相同的前后缀,所以从0开始继续匹配,如果大于0,说明有共同前后缀,此时应该不从0开始,因为有共同前后缀,可以避开节省时间。
    return N;
}

这里需要注意的一点是,阮一峰老师的博客中当前next表是代表当前j的公共最大前后缀的长度,而这个实现中当前next表是代表j-1的公共最大前后缀的长度。

关于t = N[t]可以见下图,当X不匹配Y的时候,此时我们根据next表,由当前next表的值知,P[0, t)和P[j - t, j)是相同的,此时应该移动j-t,也就是从第t位开始比较,也就是N(t)的长度。有一种特殊情况需要考虑,当N(t)等于0时,此时从0开始比较,如果第0位也不等于当前j,根据性质,t此时就等于-1了,此时就进入0>t的条件,自增j,自增t,当前j没有共同前后缀。这里开始设N[0]等于-1以及t等于-1,有两层作用,第一层是为了首轮比较时,需要隔开一位比较。第二层作用是为了防止后面与第一位不相等时,可以根据-1这个条件进入if条件,防止卡死。很是巧妙。

下面有一个事例:

有了next表的构造方法,接下来就是根据next表进行匹配了。匹配代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int match(string P, string T) {
    vector<int> next = buildNext(P);
    size_t n = T.size(), i = 0;
    size_t m = P.size(), j = 0;
    while (j < m && i < n)
        if (0 > j || T[i] == P[j]) { i++; j++;}
        else j = next[j];
    return i - j;
}

理解了next表的构造原理,其实就理解了匹配过程,next构造过程就是模式串的自我匹配。当失配时,如果next表的值大于0,说明有公共的前后缀,那么就不需要从0开始比较,直接从公共前后缀的后一个字符与当前文本的第j个字符开始比较。

KMP再改进

考虑下面这个情况,明知T[4]不等于P[4]且P[1] = P[2] = P[3] = P[4],还要比对剩余的P[1], P[2], P[3], 这是没有必要的,这是需要改进next表。

改进只需要把next中的N[j] = t换成N[j] = ( P[++j] != P[++t] ? t : N[t] )即可。如下所示:

因为相同,所以可以直接跳过他们,更快。

KMP算法的时间复杂度是

(O(m+n))

, 空间复杂度是

(O(m+n))

. 匹配过程令k = 2i- j,k每次循环至少加1,判断为真则i加1,判断为假,j至少减1,所以k <= 2n - 1; 同理next过程也是如此。

KMP小结:

  • 以判断公共前后缀来进行模式串的移动,有公共前后缀,移动到前缀的下一位即可,没有公共前后缀则移动到头部。
  • 通过通配符来有效构造next表,表的第一位为-1,当第一位对齐不相等的时候,这时通配符匹配,使文本串(也包括模式串的自我匹配)可以移动起来,不至于卡死。
  • 当发生重复串的时候,跳过他们,不进行比较。

BM算法

对于BM算法的介绍,我同样推荐看阮一峰老师的BM博客(真心推荐看看),讲的十分清楚。同样假设你看过博客知道原理了,就知道BM算法有两个next表,一个是坏字符(bad character)bc表,另一个是好后缀(good suffix)gs表,现在来看看如何构造这两个表。

bc表

对于坏字符表,构造起来很简单,它是记录模式串中每种字符最后出现的位置,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
vector<int> buildBC(string P){
    vector<int> bc(256, -1);
    for(size_t m = P.size(), j = 0; j < m; j++)
        bc[ P[j] ] = j;
    return bc;
}

坏字符移动规则: 后移位数 = 坏字符的位置- 搜索词中的上一次出现位置

基于BM-DC的算法最好情况就是

(O(n/m))

, 最坏情况是

(O(mn))

最好情况:

最坏情况:

gs表

相比于bc表,gs表就很不好构造了。首先来看看一个概念,最大匹配后缀长度表,通过它来构建ss(suffix size)表,然后通过ss表来构造gs表。

最大匹配后缀长度的意思是在P[0,j)的所有缀中,与P的某一后缀匹配最长者。例如下面的P[0, 3) = ICE, 与末尾的ICE最长匹配,则P[0, 3)的末尾就为最长匹配长度3,RICE同理。(ss表的值就等于最大匹配长度)

ss表末尾的值就是整个模式串的长度,简单的想法是遍历每一个字符向后递减,与后缀开始一一比较(暴力搜索),这样做的复杂度为

(O(m2))

, 很好的做法是下面的代码(从后往前遍历),时间复杂度只有

(O(m))

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
vector<int> buildSS ( string P ) { //构造最大匹配后缀长度表:O(m)
    int m = P.size(); 
    vector<int> ss(m, 0); //Suffix Size表
    ss[m - 1]  =  m; //对最后一个字符而言,与之匹配的最长后缀就是整个P串
// 以下,从倒数第二个字符起自右向左扫描P,依次计算出ss[]其余各项
    for ( int lo = m - 1, hi = m - 1, j = lo - 1; j >= 0; j -- )
        if ( ( lo < j ) && ( ss[m - hi + j - 1] <= j - lo ) ) //情况一:该情况处于最大匹配后缀后的字符,例如,RICE中的R,I,C.
            ss[j] =  ss[m - hi + j - 1]; //直接利用此前已计算出的ss[]
        else { //情况二: 遇到匹配项,依次递减进行匹配
            hi = j; lo = min ( lo, hi );
            while ( ( 0 <= lo ) && ( P[lo] == P[m - hi + lo - 1] ) ) 
                lo--; //逐个对比处于(lo, hi]前端的字符
            ss[j] = hi - lo; // 高位减去递减后的低位,得到最长匹配长度
        }
    return ss;
}

知道ss表后,gs表可有ss表推导出,有两种情况:

对应的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
vector<int> buildGS ( string P ) { //构造好后缀位移量表:O(m)
   vector<int> ss = buildSS ( P ); //Suffix Size表
   size_t m = P.size(); 
   vector<int> gs(m, m); //Good Suffix shift table
   for ( size_t i = 0, j = m - 1; j < UINT_MAX; j -- ) //逆向逐一扫描各字符P[j]
      if ( j + 1 == ss[j] ) //若P[0, j] = P[m - j - 1, m),则
         while ( i < m - j - 1 ) //对于P[m - j - 1]左侧的每个字符P[i]而言
            gs[i++] = m - j - 1; //m - j - 1都是gs[i]的一种选择
   for ( size_t j = 0; j < m - 1; j ++ ) //正向扫描P[]各字符,gs[j]不断递减,直至最小
      gs[m - ss[j] - 1] = m - j - 1; //m - j - 1必是其gs[m - ss[j] - 1]值的一种选择
   return gs;
}

BM_BC+GS

知道了bc表和gs表,接下来就是匹配过程了,与阮老师的博客上说的一致,取两个表的最大值。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int match ( string P, string T ) { //Boyer-Morre算法(完全版,兼顾Bad Character与Good Suffix)
   vector<int> bc = buildBC ( P ), gs = buildGS ( P ); //构造BC表和GS表
   size_t i = 0; //模式串相对于文本串的起始位置(初始时与文本串左对齐)
   while ( T.size() >= i + P.size() ) { //不断右移(距离可能不止一个字符)模式串
      int j = P.size() - 1; //从模式串最末尾的字符开始
      while ( P[j] == T[i + j] ) //自右向左比对
         if ( 0 > --j ) break; 
      if ( 0 > j ) //若极大匹配后缀 == 整个模式串(说明已经完全匹配)
         break; //返回匹配位置
      else //否则,适当地移动模式串
         i += max ( gs[j], j - bc[ T[i + j] ] ); //位移量根据BC表和GS表选择大者
   }
   return i;
}

基于BM_BC+GS算法最好情况是

(O(n/m))

,最坏情况由于有了gs表,变为了

(O(m+n))

.

综合性能

各种模式匹配算法的时间复杂度如下所示:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-10-09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
c语言字符串匹配实现_c比较字符串
在查找操作中,我们用到很重要的概念就是字符串匹配,所谓字符串匹配就是在文本串中搜索模式串是否存在及其存在的位置。下面介绍几种字符串匹配的方法。
全栈程序员站长
2022/09/24
4K0
c语言字符串匹配实现_c比较字符串
字符串匹配,一文彻底搞懂
在主串A中查找模式串B的出现位置,其中如果A的长度是n,B的长度是m,则n > m。当我们暴力匹配时,在主串A中匹配起始位置分别是 0、1、2….n-m 且长度为 m 的 n-m+1 个子串。
bigsai
2021/10/08
1K0
字符串匹配,一文彻底搞懂
字符串匹配算法_字符串模式匹配算法
网络信息中充满大量的字符串,对信息的搜寻至关重要,因此子字符串查找(即字符串匹配)是使用频率非常高的操作:给定一段长度为N的文本和长度为M的模式字符串(N≥M),在文本中找到一个和模式串相匹配的子串。由这个问题可以延伸至统计模式串在文本中出现的次数、找出上下文(和该模式串相符的子字符串周围的文字)等更复杂的问题。
全栈程序员站长
2022/08/02
3.4K0
字符串匹配算法_字符串模式匹配算法
字符串匹配算法知多少?
不要被事物的表面现象所迷惑,这个算法全称:Brute Force,有个拉风的中文名:暴力匹配算法。
看、未来
2021/09/18
3520
常用字符串匹配算法简介
字符串模式匹配是常见的算法之一,在实际生活中有较高的使用频率,特别是在当下的互联网服务中,经常用于游戏角色名检查、论坛发帖、直播弹幕、分类打标签、入侵检测等场景。字符串模式匹配又分为单模匹配和多模匹配,区别在于单模匹配是搜索一个模式串,多模式匹配是搜索多个模式串。由于无数大佬前赴后继的投入到模式匹配算法的研究中,时至今日,又有大量成熟的匹配算法,这里姜维大家简要介绍一些,可以根据自身业务需要选用。
用户1257041
2019/09/01
3K0
字符串匹配算法(BM)
从好后缀的后缀子串中,找一个最长的且和模式串的前缀子串匹配的 {v},滑动至 {v} 对齐
Michael阿明
2021/02/20
1.5K0
字符串匹配算法(BM)
KMP、BM、Sunday等字符串匹配算法及实现
发现字符串的匹配完全要考虑全面,如果考虑的情况不足够全面,就很可能出现这个例子可以运行,下一个例子的就行不通,毕竟匹配可能遇到各种各样的情况。本着可以实现效果就可以的原则,编的代码也实在是不优美,BM参考了别人的代码,因为写的精炼,按照自己的思路来写,然后发现有的可以运行,有的就达不到相应的效果。主要实现了暴力字符串匹配、KMP、BM、Sunday四种,几天的时间学习完的,回头再看的时候发现自己都有点忘记了,赶紧记下来~
张凝可
2019/08/22
7000
字符串硬核讲解
在主串A中查找模式串B的出现位置,其中如果A的长度是n,B的长度是m,则n > m。当我们暴力匹配时,在主串A中匹配起始位置分别是 0、1、2….n-m 且长度为 m 的 n-m+1 个子串。
sowhat1412
2022/09/20
3980
字符串硬核讲解
字符串匹配算法(KMP)
如果 b[k] != b[j] , 则我要在前面部分里寻找能和包含 b[j] 的后缀匹配的最长前缀子串; b[k] 前面的最长匹配前缀长度就是 next[k],那么其后面一个字符就是 b[ next[k] ],如果它等于b[j],那么next[j+1] = next[k] + 1 参考文献
Michael阿明
2021/02/20
9460
字符串匹配算法(KMP)
【算法图文动画详解系列】KMP 字串匹配搜索算法
假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
一个会写诗的程序员
2021/04/28
1.2K0
【算法图文动画详解系列】KMP 字串匹配搜索算法
KMP算法分析
KMP 算法是一种改进的字符串匹配算法,KMP 算法是由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 三人提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称 KMP 算法)。KMP 算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个 next() 函数实现,函数本身包含了模式串的局部匹配信息。
evenleo
2020/10/14
5600
算法:字符串
在示例代码中,str是一个字符串的变量名称,hello world则是该字符串的值,字符串的长度为11,该字符串的表示如下图所示:
用户3578099
2022/03/15
3K0
算法:字符串
字符串匹配算法之KMP
需求一:项目结果文件中实验结论可能会存在未知类型、转换错误、空指针、超过索引长度等等。这里是类比需求,用日常开发中常出现的错误类型作为需求,如果要以上结论则判断这个项目检测失败;
沁溪源
2020/09/03
7370
字符串匹配(一) -- 朴素匹配与 KMP 算法
软件算法中,最基础的算法要数排序和查找了,而字符串模式匹配算法可谓是基础中的基础,而最有名又最具代表性的字符串匹配算法要数 KMP 算法了,本文我们就来详细介绍一下 KMP 算法
用户3147702
2022/06/27
1.5K0
字符串匹配(一) -- 朴素匹配与 KMP 算法
字符串匹配算法KMP, BM_BC/BM_GS如何理解? C++语言
字符串匹配是搜索算法的基础,也是数据结构中一个十分有用的算法分支,我在学习KMP和BMBC算法的时候就觉得听的云里雾里,但经过一些实操和分析不难发现,这几个算法都是很好理解,并且对算法有很务实启发的。
雪碧君
2023/02/15
8730
深入解析 Knuth-Morris-Pratt 算法:字符串匹配的高效解决方案
这篇文章主要是总结一下kmp算法。所以就不写暴力遍历的逻辑了。这个算法属实是让我看了挺长时间,各种讲解博客是一点也看不进去(不是写的不详细,而是总感觉写的乱七八糟很复杂),最长公共前缀一直没理解其作用,不过反反复复的刷对应的讲解视频,卒或有所闻。
f1sh
2024/07/23
2710
leetcode 28. 实现 strStr()----KMP算法,朴素模式匹配算法----超万字长文详解
3.KMP算法—这里借鉴宫水三叶大佬的讲解 具体详情可以看原文 KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。 上述的朴素解法,不考虑剪枝的话复杂度是 O(m * n) 的,而 KMP 算法的复杂度为 O(m + n)。 KMP 之所以能够在 O(m + n)O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。
大忽悠爱学习
2021/11/15
6970
从入门到精通之Boyer-Moore字符串搜索算法详解
本文讲述的是Boyer-Moore算法,Boyer-Moore算法作为字符串搜索算法,兴趣之下就想了解这个算法,发现这个算法一开始还挺难理解的,也许是我理解能力不是很好吧,花了小半天才看懂,看懂了过后就想分享下,因为觉得这个算法真的挺不错的,以前一直以为字符串搜索算法中KMP算很不错的了,没想到还有更好的,Boyer-Moore算法平均要比KMP快3-5倍。 下面是我对该算法的理解,参考了一些关于该算法的介绍,里面每一张图都画的很认真,希望能讲清楚问题,有什么错误、疑问或不懂的地方麻烦大家一定要提出来,共同
Angel_Kitty
2018/04/09
1.7K0
从入门到精通之Boyer-Moore字符串搜索算法详解
搜索中常见数据结构与算法探究(二)
Tech    导读    本文介绍了几个常见的匹配算法,通过算法过程和算法分析介绍了各个算法的优缺点和使用场景,并为后续的搜索文章做个铺垫;读者可以通过比较几种算法的差异,进一步了解匹配算法演进过程以及解决问题的场景;KMP算法和Double-Array TireTree是其中算法思想的集大成者,希望读者重点关注。 01 前言 上文探究了数据结构和算法的一些基础和部分线性数据结构和部分简单非线性数据结构,本文我们来一起探究图论,以及一些字符串模式匹配的高级数据结构和算法。《搜索
京东技术
2022/04/02
3970
搜索中常见数据结构与算法探究(二)
字符串匹配算法详解
愿你们都能考上自己心仪的学校,为你们的备考生涯划上一个完美的句号。做为你们的师兄有几句话想对你们说,希望这些话能对你们有一些帮助。
公众号袁厨的算法小屋
2021/01/08
1.7K0
相关推荐
c语言字符串匹配实现_c比较字符串
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档