如果你想了解KMP算法,请静下心读完这篇文章,一定不会辜负你的时间

暴力匹配(BF)

字符串匹配是我们在编程中常见的问题,其中从一个字符串(主串)中检测出另一个字符串(模式串)是一个非常经典的问题,当提及到这个问题时我们首先想到的算法可能就是暴力匹配,下面的动图就展示了暴力匹配的流程。

上图中箭头指向的字符都为蓝色时代表二者匹配,都为黑色时代表二者不匹配,红色则代表在主串中找到模式串。

这种算法大致思路就是每当模式串和主串中有字符不匹配,模式串与主串对应的位置整体向后移动一位,再次从模式串第一位开始比较,重复上述做法直至在主串中匹配到模式串或者匹配到主串最后一位结束。

如果主串与模式串都比较短时,用暴力匹配还是不错的选择,编码也相对容易;但是如果主串与模式串过长时,我们只是简单想想就知道这个过程是非常耗时的,那么会不会有对应的优化算法呢?

下面就介绍本文的主角——KMP算法,不扯没用的概念,直接讲算法的应用过程及利用Python实现该算法的代码,最后会通过二者时间复杂度的分析,总结出为何KMP算法会优于暴力匹配算法。

KMP算法

构建前缀表

我们首先要确定一下引例的主串和模式串:

  • 主 串 S = "abacaababc"
  • 模式串P="ababc"

在模式串与主串匹配时,我们暂时只看第4步,明显主串S中的c和模式串P中的b是不匹配的:

如果用暴力匹配算法,那么就是后移模式串P,在从P的第一个字符开始比较。但是现在通过匹配我们可以知道的是当第4位不匹配时,前三个字符为"aba"是确定的,这个已知信息是十分有用的。

而KMP算法的核心就是利用匹配失败后获取的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的,比如对于这个不匹配现象我们是不是可以直接这样移动模式串呢?

那么信息从何而来呢?在KMP算法中,对于一个模式串都可以先计算出其内部的匹配信息,这样在匹配失败时可以最有效的移动模式串,从而减少匹配次数。在此之前,需要先理解一下前缀和后缀。

  • 前缀:abcde的前缀可以是a、ab、abc、abcd
  • 后缀:abcde的后缀可以是e、de、cde、bcde

这里需要引出一个新的概念——前缀表,可以用profix表示、且下标从0开始,profix[i]存储的信息就是前i+1个字符的最长公共前后缀,并且这个最长公共前后缀长度一定是小于字符串长度的。

可以看到"ababc"不是前后缀,但也被列到了表中。如果你曾经了解过KMP算法,那你可能听过next数组,当前缀表转化为next数组时,最后一位的值会被覆盖掉,对过程是没有什么影响的。由于本文仅是靠着前缀表profix完成KMP算法,所以不再过多讲述next数组,不同的方法只是表示形式不一样,但归根结底原理还是相同的。

上面的前缀表是我们通过肉眼比对得出的,程序毕竟不是人嘛,所以需要通过一种程序能够识别的方法构建前缀表,依据下图进行讲述流程。

通过这个动图可以将构建前缀表规划成下面五步:

  1. 首先创建两个指针,指针j指向模式串第一位(下标为0)、指针i指向模式串第二位(下标为1)。
  2. 由于模式串最开始是单一字符,没有前缀和后缀,所以对应前缀表第一位总为0。
  3. 当j=0时,比较j和i指向的字符,如果字符不匹配,i对应的前缀表位置填入0,且将i向后移动移位,j原地不变。
  4. 当j和i指向的字符匹配时,i对应的前缀表位置填入(j+1),且将j和i都后移一位。
  5. 如果j和i指向的字符不匹配,并且此时,j需要回溯到profix[j-1]的位置,再次与i指向的字符比较,重复此步骤直至j和i指向的字符匹配或者j=0。

当结合动图读完这五个步骤时,我猜你会不理解第五步,如果你都理解了,我也只能感叹一句NiuBi,利用下面这个例子更能凸显出步骤五的回溯机制。

依据上面步骤我写出了前缀表的前五位,而此时j和i指向的字符不匹配且j≠0,这里j的下标是3,所以需要在前缀表中找到下标为j-1的值,即profix[2],然后将j回溯到对应的位置。

这样回溯是因为可以在模式串头部找到和j和i之间的字符串相匹配的前缀,也就是这个例子中的a,如果此时j和i指向的字符相匹配,那么最长公共前后缀的长度就是已匹配的前缀的长度(a)再加1。由此可见如果j和i之间字符串很长时,这个操作可以节省很多时间。

而此时j和i指向的字符仍然不匹配,那么需要继续回溯j,方法和上述一致,回溯的位置就是profix[0]。

此时j和i指向的字符还是不匹配,但这里需要做的就不是回溯了,因为j=0已经满足回溯结束条件,只需将i对应前缀表的位置(profix[5])中填入0即可,用肉眼匹配也会发现此时的确没有公共前后缀。

在理解上述步骤之后,可以将其当成伪代码,依据伪代码很容易编写出构建前缀表函数。

def PrefixTable(Pattern):    i = 1;j = 0    prefix = [0]*len(Pattern)    while(i

可以输入一个模式串,测试一下该代码是否能够得出对应前缀表。

优化前缀表

经过上文解释你可能会发现一个基本事实,即前缀表最后一位没有任何作用,这么说的理由是什么呢?因为当j和i指向的字符不匹配时,这里的解决办法是回溯j,而回溯依据一直都是prefix[j-1],j是永远不可能超越i的,所以前缀表最后一位永远也不会用到。

那么最后一位就可以去掉,将所有元素整体后移一位,并向前缀表第一位填入-1,如下图:

填入-1这个操作的原理等下结合图片一起讲述会更易懂,目前我们只需知道这个操作并且了解其对应代码即可。

-def MoveTable(prefix):    for i in range(len(prefix)-1,0,-1):        prefix[i] = prefix[i-1]    prefix[0] = -1    return prefix复制代码

KMP匹配机制

主串和模式串还是利用上文所举例子,这里省略了一些简单的匹配过程,直接看关键点。

可以看到主串和模式串的第4位是不匹配的,现在需要做的是将Pattern[prefix[4]]对应主串中需要匹配的元素,也就是模式串下标为1的元素后移至与主串第4位对应的位置,看图可懂。

对应位置仍然不匹配,需要继续后移模式串,该位置对应前缀表的值为0,所以将Pattern[prefix[0]]对应主串中需要匹配的元素,即模式串下标为0的元素与主串该位置对应。

此时两串对应位置还是不匹配,但是a已经是模式串的第一位元素了,如果按照上面方法需要继续后移模式串,让主串那个位置与模式串下标为-1的元素匹配,可是前缀表中并不存在下标为-1的元素。

所以比较时如果模式串和主串对应位置不匹配,且模式串的元素对应前缀表的值为-1,那么直接将模式串整体后移一位,并且将指向主串的指针后移一位即可,这也是为什么在前缀表第一位插入-1的原因。

下面动图是利用KMP算法在主串中查找模式串的全过程。

KMP算法的代码如下:

def KMP(TheString,Pattern):    m = len(TheString);n = len(Pattern)    prefix = PrefixTable(Pattern)    prefix = MoveTable(prefix)    i = 0;j = 0#i为主串指针,j为模式串指针    while(i

这里只讲一下第一个if语句,当j指向了模式串最后一位,并且此时如果主串和模式串对应位置匹配,则代表在主串中找到了模式串,并打印出第一个字符出现的位置。而j= prefix[j]这个语句的作用是在找到模式串后继续匹配剩余的主串,因为可能会有主串中含有若干个模式串的现象出现。

最后整个程序运行截图如下:

BF与KMP比较

为什么KMP会优于BF,这里通过对比二者的时间复杂度给出原因,假设有这么两个比较极端的主串和模式串:

  • 主 串 S = "aaaaaaab"
  • 模式串P="aaab"

首先看一下BF算法解决该匹配问题的流程:

然后再看一下KMP算法解决该匹配问题的流程:

假设主串长度为m,模式串长度为n。对于BF算法,每当遇到不匹配字符时,都要从模式串开头再次匹配,所以对应时间复杂度O(m*n);对于KMP算法,每当遇到不匹配字符时,根据获得的信息它不会重复匹配的已知前缀,所以对应时间复杂度为O(m+n)。当字符串较长时,就时间复杂度而言KMP算法是完全优于BF算法的。

总结

个人认为KMP算法难度不低,讲这个算法的博客与视频很多,但都各有差异,虽然原理都是大致相同的,但不要同时看前缀表和next数组,由于这两个很像所以会容易混淆,可以先弄透前缀表然后再看next数组相关知识点,这样对于KMP的理解才算透彻。

作者:奶糖猫

链接:https://juejin.im/post/5eb8ac9d5188256d9147983e

来源:掘金

ac自动机 匹配最长前缀_别再暴力匹配字符串了,高效的KMP,才是真的香相关推荐

  1. ac自动机 匹配最长前缀_AC自动机算法

    AC自动机简介: 首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一.一个常见的例子就是给出n个单词,再给出一段包 ...

  2. ac自动机 匹配最长前缀_Aho Corasick自动机结合DoubleArrayTrie极速多模式匹配

    本文使用Double Array Trie实现了一个性能极高的Aho Corasick自动机,应用于分词可以取得1400万字每秒,约合27MB/s的分词速度.其中词典为150万词,构建耗时1801 m ...

  3. AC自动机——多个kmp匹配

    (并不能自动AC) 介绍: Aho-Corasick automaton,最经典的处理多个模式串的匹配问题. 是kmp和字典树的结合. 精髓与灵魂: ①利用trie处理多个模式串 ②引入fail指针. ...

  4. 字符串匹配算法 -- AC自动机 基于Trie树的高效的敏感词过滤算法

    文章目录 1. 算法背景 2. AC自动机实现原理 2.1 构建失败指针 2.2 依赖失败指针过滤敏感词 3. 复杂度及完整代码 1. 算法背景 之前介绍过单模式串匹配的高效算法:BM和KMP 以及 ...

  5. Keywords Search HDU - 2222(AC自动机模板)

    题意: 给定 n个长度不超过 50的由小写英文字母组成的单词准备查询,以及一篇文章,问:文中出现了多少个待查询的单词.多组数据. 题目: In the modern time, Search engi ...

  6. KMP算法、AC自动机算法的原理介绍以及Python实现

    KMP算法 要弄懂AC自动机算法,首先弄清楚KMP算法. 这篇文章讲的很好: http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E ...

  7. BZOJ 4327 【JSOI 2012】 玄武密码 AC自动机+dfs

    题目描述 在美丽的玄武湖畔,鸡鸣寺边,鸡笼山前,有一块富饶而秀美的土地,人们唤作进香河.相传一日,一缕紫气从天而至,只一瞬间便消失在了进香河中.老人们说,这是玄武神灵将天书藏匿在此.  很多年后,人们 ...

  8. AC自动机原理及代码实现

    目录 一 定义 二 构建字典树 三 构建 AC 自动机 四 模式匹配 五 性能分析 六.代码实现 一 定义 AC 自动机是 KMP 算法和 Trie 树的结合,是经典的多模匹配算法. 首先将多个模式串 ...

  9. AC自动机算法详解以及Java代码实现

    详细介绍了AC自动机算法详解以及Java代码实现. 文章目录 1 概念和原理 2 节点定义 3 构建Trie前缀树 3.1 案例演示 4 构建fail失配指针 4.1 案例演示 5 匹配文本 5.1 ...

最新文章

  1. 论网络营销在我国的发展
  2. 面试官问: 如何保证 MQ消息是有序的?
  3. [Swift]LeetCode388. 文件的最长绝对路径 | Longest Absolute File Path
  4. 一个通过引用返回局部变量的列子
  5. mysql float的缺点 (不适用于高精度数据)
  6. 网络协议:TCP/IP、SOCKET、HTTP
  7. usaco题思考记录
  8. Go语言基础练习题系列2
  9. 字符串匹配算法总结(转)
  10. 使用Excel公式,获取 当前 Excel 的Sheet页 的 名字
  11. pycharm自定义代码片段
  12. 在libevent中服务模型
  13. 使用HTML5中的Canves标签制作时钟特效
  14. html grid插件,grid.html
  15. 面试官:谈谈你对geohash的理解和如何实现附近人功能呢?
  16. 频谱分析仪是什么东西 怎么去选择----TFN FMT350(3.1gHz)/FMT450(4.4gHz)/FMT650(6gHz) 系列频谱仪
  17. java 导出txt_【Java】导入导出TXT文件
  18. C语言编程>第十四周 ⑦ 请编写一个函数fun,它的功能是:计算n门课程的平均分,计算结果作为函数值返回。
  19. 百度地图、高德地图等商用5w/年怎么搞
  20. 2d与2.5d坐标转换_视觉SLAM:搞定坐标系、三角测量、PnP

热门文章

  1. 安静的飞鸽传书2011绿色版地方
  2. 有两个程序员得了肺癌 都是30多岁的男人
  3. 下班啦!做那么多老板不会心疼你的
  4. 程序员“苦逼值”测试,分数越高越苦逼
  5. 微信JS SDK开放,前端开发者“鸡冻”了!
  6. 华为申请注册华为鸿蒙商标,华为申请注册“华为鸿蒙”商标,自研操作系统或将实现?-控制器/处理器-与非网...
  7. vgh电压高了有什么_一文告诉你电压互感器的作用是什么?
  8. java while语句_Java while循环
  9. Python | threading04 - 使用信号量,实现线程间同步
  10. java接收rowtype类型_Java PhysType.getJavaRowType方法代码示例