本文已在本人微信公众号“码农小阿飞”上发布,打开微信搜索“码农小阿飞”,或者扫描文章结尾的二维码,进入公众号并关注,就可以在第一时间收到我的推文!

前言

不管是什么编程语言,字符串可能不是基本类型之一,但一定都是最常用的数据类型之一,对于字符串的操作是程序设计中最常见的行为。

在所有对字符串的操作中,字符串的查找匹配似乎又是日常编程中最司空见惯的操作,无论是后端程序根据用户所提交的搜索关键字来匹配,并返回搜索候选内容。还是前端程序根据用户输入的关键字,高亮显示匹配的字符串。

所谓的字符串匹配,就是在一段字符主串中,去匹配和模式串在每个位置上的字符都相同的子串,而模式串就是那串需要判断是否在主串中出现的字符串。高效率的字符串匹配算法,能在运行过程中给处理器较小的负担,能减少用户在程序执行算法匹配过程中的等待时间,给用户一种行云流水般的体验。因此,在编程过程中选择合适的字符串匹配算法显得格外重要

朴素模式匹配算法

可能日常开发过程涉及字符串查找匹配的操作,已经有很多的工具类帮助实现,只要简单的调用就行。授人以鱼不如授人以渔,如果让你手写一个简单的字符串匹配算法,你又会怎么做呢?

说到字符串的匹配,我想大家自然而然最先能够想到的算法应该就是朴素模式匹配算法。所谓的朴素模式匹配算法,就是刚开始的时候将主串的第1个字符和模式串的第1个字符对齐,从模式串的第1个字符开始,从左往右逐个对比主串和模式串的字符。如果字符相同,则对比双方的下1个字符。如果其中出现某个位置,主串的字符和模式串对应位置的字符不相同,则将模式串整体往右移动1个字符的位置,继续上述操作,直到在主串中匹配到与模式串完全相同的子串,或者主串剩余还没有进行对比的字符数量少于模式串的字符数量。

/****** Java代码实现朴素模式匹配* * @param stringS 主串S * @param stringT 模式串T */
public boolean match(String stringS, String stringT) {char[] charsS = stringS.toCharArray();char[] charsT = stringT.toCharArray();for (int i = 0, sizeI = charsS.length - charsT.length; i <= sizeI; i ++) {for (int j = 0, sizeJ = charsT.length; j < sizeJ; j ++) {if (charsS[i + j] != charsT[j]) {break;}if (sizeJ - 1 == j) {return true;}}}return false;
}

朴素模式匹配算法比较简单,比较容易理解和接受,下面就是朴素模式匹配算法的运行过程:

从上述运行过程的动态图可以看出,朴素模式匹配算法在字符匹配的过程中,不断的往回移动主串的指针来和模式串进行对比,这种行为称为回溯。在算法运行过程中,大量的指针回溯往往会增加大量的运行时间,效率往往都不高。

既然这么说,那有没有一种算法能避免这种通过主串指针不断回溯来进行字符匹配导致效率降低的问题呢?毫无疑问,有!那就是今天要讲的KMP模式匹配算法。

KMP模式匹配算法

先看上面这张图,主串直到匹配到第6个字符才发现和模式串第6个字符不相同,说明什么?说明主串的前5个字符组成的子串是和模式串的前5个字符组成的子串是相同的,这会是一个很重要的信息。而从模式串可以看出,模式串的前3个字符互不相同。那就没必要像朴素模式匹配算法一样往回移动主串的指针,拿主串的第2和第3个字符去和模式串的第1个字符去做无谓的比较,因为那一定不相同。

既然如此,先将模式串往右移动,跳过模式串第1个字符和主串第2和第3个字符的比较,这时候模式串第1个字符和主串第4个字符相对齐,模式串第2个字符和主串第5个字符相对齐。

再回过来看一下模式串,模式串第1个和第2个字符组成的子串“ab”和模式串第4个和第5个字符组成的子串“ab”相同。主串前5个字符组成的子串又是和模式串前5个字符组成的子串是相同的,那么主串的第4个和第5个字符组成的子串就必然和模式串第1个和第2个字符组成的子串相同,在上一次模式串位置滑动后他们刚好一一对齐,那么主串的指针就没必要再回来对比这两个位置的字符了。

这样就可以不往回移动主串的指针,又得以继续进行字符比较。这就是KMP算法的核心思想,通过模式串前面的字符和主串对应位置字符的对比结果,再结合模式串自身的前后位置信息,做出适当的滑动,避免主串指针无谓的回溯,从而达到匹配效率上的提升。

你可能会疑问了,我们当然可以非常直观的通过图示对模式串进行解读,可计算机呢?对计算机来说,只能通过循环和判断等方式来解决问题,怎么用代码来让计算机来解读未知模式串的信息呢?

KMP算法-next[]数组推导

KMP算法就是根据模式串的字符前后位置信息生成一个与模式串等长的整形数组next[],用来存储当模式串上每个位置上的字符如果和主串所对应字符不相同的时候,根据next[]中得到的整型数值作为指引,做一个最优的滑动,什么叫最优滑动呢?那就是模式串往右滑动的尽可能少,这样就能说明左侧仍然对齐的部分尽可能的多,也就不会错过可能的子串,同时又能做较少的比较。

如上图所示就是next[m]=n的例子,上边是模式串移动之前的位置,下边是模式串移动之后的位置。意思是当模式串下标为m处的字符和主串中的字符对比出现不同时,根据next[m]=n指示,往右滑动模式串,使模式串下标为n处的字符和原本模式串下标为m处的字符对应的那个主串中的字符对齐并进行比较,那么,模式串移动前后的位置关系就会如上图所示,模式串下标为m处的字符和模式串下标为n处的字符对齐。

注意此处用的是下标,因为Java语言和很多程序语言一样,数组的下标是从0开始的,这和上文多处提到的第几个字符要注意区分,很多人都因为这两者的关系理解错乱,而导致对后续理解next[]数组的推导过程造成不小的影响。

模式串未移动前m往左所有位置的字符都已经和主串中对应位置的字符一一相同,移动就是为了避免主串指针回溯进行无谓的比较,所以所有有效的next[m]=n都应该满足性质:如果next[m]=n成立,则n位置往左直到字符串的尽头所有字符和m往左所对应的字符都应该一一相同,除非n之前不存在任何字符。满足这个条件的n可能会有多个,但是这个n一定是最大的那种可能。这就是人们常说的最长的公共前缀和最长的公共后缀,是整个next[]数组推导的核心思想,也是后续推导next[]数组的理论基础,这一点必须得搞清楚,因为如果不满足这个性质,所有的滑动都是徒劳,是没有意义的,只会造成误判产生错误。

结合上述得出的理论基础,再来看一下这张图。如果next[m]=n成立,一定满足上面的性质。那么如果此时m处的字符和n处的字符正好又相同的话,next[m+1]=n+1就一定成立,既然m和n往左部分已经生成最长的公共前缀和公共后缀,m处的字符又和n处的字符相同,那next[m+1]=n+1就一定满足上述性质。

可是,如果m处的字符和n处的字符不相同的话,我们就不能草率的令next[m+1]=n+1,因为这样就不满足上述性质。怎么做?那我们就像是模式串n处的字符对比失败那样,令n=next[n]做一次最优的滑动,m和n依旧可以满足上述关系,和上面一样,我们继续比较m处和n处的字符是否相同,不相同就继续移动,直到m处和n处的字符相同,又或者n处不存在字符。可以这么说:在已知所有的next[m],其中m<n,那么我们就可以根据这些信息推导出next[n]

如上图所示,如果在对比过程中,模式串的第1个字符和主串所对应的字符不相同,那么就跳过主串中这个字符的对比,所以next[0]=-1,而且这一点在任何模式串中都成立。那么,结合上面的论述,已知next[0]我们就可以推导出next[1],已知next[0]和next[1]我们就可以推导出next[2],以此类推,整个next[]数组我们就可以推导出来。

/*** Java代码实现KMP模式匹配算法next[]推导过程** @param stringT 模式串* */
private int[] getNext(String stringT) {char[] chars = stringT.toCharArray();int[] next = new int[chars.length];next[0] = -1;  // 这是一个必然的结果,不管是对什么模式串,以此为突破点往后推导for (int i = 1; i < chars.length; i ++) {int j = next[i - 1];while (true) {if (-1 == j || chars[i - 1] == chars[j]) {next[i] = j + 1;break;} else {j = next[j];}}}return next;
}


验证一下结果,和我们从图示中得到的信息是一样的,再将推导出的next[]数组结合到实际字符串匹配的代码中。

/****** Java代码实现KMP模式匹配** @param stringS 主串S * @param stringT 模式串T */
public boolean match(String stringS, String stringT) {char[] charsS = stringS.toCharArray();char[] charsT = stringT.toCharArray();int[] next = getNext(stringT);int j = 0;for (int i = 0, sizeI = charsS.length; i < sizeI; i ++) {// 此处0 > j其实就是针对next[j]=-1的情况,跳过主串当前字符的对比if (0 > j || charsT[j] == charsS[i]) {j ++;} else {j = next[j];i --; // 实现继续对比主串的同一个位置的字符}if (charsT.length == j) {return true;}}return false;
}

看看效果如何。

KMP算法-next[]数组推导优化

是不是主串的指针没在往回移动?也少去了很多没必要的比较过程?可是我想告诉你的是,还可以再优化优化,如果上述的模式串改为“abcabc”呢?

根据之前next[]数组推导的结果,结果和我们之前说的一样,结合到实际匹配当中去看看:

可以看出,在主串第6个字符和模式串第6个字符不相同时,我们根据next[5]=2,滑动模式串,也就是将模式串的第3个字符和主串的第6个字符进行对齐并进行比较。可是我们从模式串中可以知道,模式串的第3个和第6个字符是一样的,也就是说既然主串的第6个字符和模式串的第6个字符不相同,那么也就和模式串第3个字符不相同了。我们只在next[]数组的推导中next[m]=n需要满足最长公共前缀和公共后缀,却忽略了next[]推荐滑动位置的字符是否就是和当前字符相同,如果相同那么也就不需要再做无谓的比较了。

我们对getNext()的代码进行一下优化:

/*** Java代码实现KMP模式匹配算法next[]推导过程* 针对next[]推荐对比字符相同问题优化** @param stringT 模式串* */
private int[] getNextPlus(String stringT) {char[] chars = stringT.toCharArray();int[] next = new int[chars.length];next[0] = -1; // 这是一个必然的结果,不管是对什么模式串,以此为突破点往后推导for (int i = 1; i < chars.length; i ++) {int j = next[i - 1];while (true) {if (-1 == j || chars[i - 1] == chars[j]) {next[i] = j + 1;break;} else {j = next[j];}}}// 优化版增添代码for (int i = 0; i < next.length; i ++) {if (0 <= next[i] && chars[next[i]] == chars[i]) {next[i] = next[next[i]];}}return next;
}


再验证一下,发现变化还是有点多的,在结合图示发现确实没有问题,优化后的代码再结合到实际匹配当中去:

/****** Java代码实现KMP模式匹配** @param stringS 主串S * @param stringT 模式串T */
public boolean match(String stringS, String stringT) {char[] charsS = stringS.toCharArray();char[] charsT = stringT.toCharArray();int[] next = getNextPlus(stringT);int j = 0;for (int i = 0, sizeI = charsS.length; i < sizeI; i ++) {// 此处0 > j其实就是针对next[j]=-1的情况,跳过主串当前字符的对比if (0 > j || charsT[j] == charsS[i]) {j ++;} else {j = next[j];i --; // 实现继续对比主串的同一个位置的字符}if (charsT.length == j) {return true;}}return false;
}

现在,再看一下效果:

简直不要比朴素模式匹配算法快太多,我素材都可以少画不少,哈哈哈 …

结束语

在编程世界中,解决一个问题的方法往往不止一种,而这些方法之间没有绝对的优劣之分,只是可能当时所处的环境不同罢了。上述例子让我们看到KMP算法比朴素模式匹配算法高效不少,那如果主串是“abcdeabcde”,模式串是“abcde”呢?朴素模式匹配算法不需要指针回溯就能匹配成功,KMP模式匹配算法却多出了一个next[]数组推导的过程。KMP算法的优势是:next[]数组一次推导,到处匹配的特性。

到这里KMP模式匹配算法也就算讲完啦!吃水不忘挖井人,感谢算法创始人D.E.Knuth、J.H.Morris和V.R.Pratt,KMP正是以他们三者名字的首字母组合而来。

如果觉得这篇文章对你了解KMP模式匹配算法和其中next[]数组的推导过程有帮助的话,不胜荣幸。当然,如果对文章有什么疑问,又或者是文章中有什么地方有误或者解释不当,请在评论区留言,一起探讨。

关注我,带你去看编程世界中那些有趣的发现。

也许,你可以像我这样来理解KMP模式匹配算法相关推荐

  1. 深入理解建造者模式 ——组装复杂的实例

    历史文章回顾: 设计模式专栏 深入理解单例模式 深入理解工厂模式 历史优质文章推荐: 分布式系统的经典基础理论 可能是最漂亮的Spring事务管理详解 面试中关于Java虚拟机(jvm)的问题看这篇就 ...

  2. 2020-11-23(彻底理解KMP)

    文章目录 声明 暴力破解 第一步 第二步 第三步 第四步 第五步 第六步 第七步 KMP算法 定义 步骤 寻找前缀后缀最长公共元素长度 求next数组 解释 寻找最长前缀后缀 基于<最大长度表& ...

  3. 转:[置顶] 从头到尾彻底理解KMP(2014年8月22日版)

    [置顶] 从头到尾彻底理解KMP(2014年8月22日版) 转载于:https://www.cnblogs.com/kira2will/p/4111564.html

  4. KMP字符串匹配算法理解(转)

    一.引言 主串(被扫描的串):S='s0s1...sn-1',i 为主串下标指针,指示每回合匹配过程中主串的当前被比较字符: 模式串(需要在主串中寻找的串):P='p0p1...pm-1',j 为模式 ...

  5. 从理解Future模式到仿写JUC的Future模式

    1.Future模式 过生日,在线上定蛋糕的例子可以形象的解释Future模式. 两个步骤: 1.下单 - 委托制作蛋糕:你在线定下单订蛋糕,将制作蛋糕的工作委托给了蛋糕店. 2.取蛋糕:凭着在线支付 ...

  6. 理解用户模式(User Mode)和内核模式(Kernel Mode)

    理解用户模式(UserMode)和内核模式(KernelMode)

  7. 深入理解原型模式 ——通过复制生成实例

    Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):github.com/Snailclimb/- 系列文章回顾: 设计模式专栏 深入理解单例模式 深入理解工厂模式 ...

  8. 设计模式之浅浅的理解桥接模式

    网上有不少博文都深入浅出的论述了桥接模式的作用和设计方法.不过我总觉得自己理解得不到位. 下边是我的理解和实验. 桥接模式理解: 在理解桥接模式中,网上的博客中都谈到抽象与实现的分离,这里的实现与继承 ...

  9. 【数据结构】字符串 模式匹配算法的理解与实现 Brute Force算法(BF算法)与KMP算法 (C与C++分别实现)

    #笔记整理 若不了解串的定义,可至: 串(string)的定义与表示 查看 串的模式匹配算法 求子串位置的定位函数 Index(S, P, pos) 求子串的定位操作通常称作串的模式匹配(其中子串P称 ...

最新文章

  1. 时间序列(三)滑动窗口
  2. Cydia for Android2
  3. bootstrap的php写在哪里,bootstrap用什么编辑器写
  4. 无线基站侧的信令风暴根因——频繁的释放和连接RRC产生大量信令、设备移动导致小区重选信令增加、寻呼信令多...
  5. 【JAVA学习笔记】个人设定
  6. Python判断变量的数据类型的两种方法
  7. 如果程序跑着跑着就崩溃了,查看内存
  8. 刀剑无双服务器显示404,刀剑无双如何开启GM命令 刀剑无双GM指令修改
  9. java 时间回退_java.time DateTimeFormatter使用灵活的回退值进行解析
  10. python 动态链接库_Python调用dll动态链接库(下)
  11. java.net cidr接口_CIDR - xiaohuazi - 博客园
  12. 阿里云免费SSL证书到期了怎么办?(阿里云虚拟主机安装HTTPS)
  13. 入侵提权过程中猜解linux路径与windows路径,网站路径暴力
  14. NewStarCTF 公开赛赛道 WEEK2 pwn 砍一刀
  15. 计算机领域区块链是什么是意思,为什么区块链瑞普顿RXP是不可篡改的
  16. 计算机科学在职研究生排名,计算机在职研究生院校
  17. 备份你的Sina博客
  18. 车道检测(传统方法)
  19. 初学stm32建议的---实用开发板推荐
  20. 证书工具+网络插件介绍

热门文章

  1. NCMMSC 2021丨希尔贝壳参加第十六届全国人机语音通讯学术会议
  2. 华为从服务器获取安装包信息失败,华为系统恢复获取安装包信息失败
  3. JAVA 类的继承(私有属性、自动转型)(入门级小白一看就懂)
  4. iOS开发笔记--超全!iOS 面试题汇总
  5. MSDN不能使用,提示“无法打开文档资源管理器”
  6. 社区发现之标签传播算法(LPA)
  7. 小提琴和钢琴一起学行吗_选学钢琴、小提琴的5大误区,家长一定要知道!
  8. Android开发38岁被裁,本以为稳进Top3,今天已经是失业第42天
  9. 理清JS中的深拷贝与浅拷贝
  10. 开启虫洞的频率_仅用几段音频、扬声器和灯就能打开“虫洞”,这是真的?内附视频...