前阵子在学习KMP相关的内容,其他部分都挺好理解的,最后在next数组和k=next[k]这个递推公式上迷糊了好久,看了不少人写的博客,有的写着写着最后的结论又跳跃了,有的是写清楚了,但是感觉写的有过于细节了。

不过总算是弄懂了,所以决定自己也来写一写这个KMP算法

单模匹配

字符串匹配是一个经典的算法问题,扩展开来讲个几天也讲不完,本篇博客仅讨论最简单的单模匹配问题。
假设已知两个字符串S和P,问P是否是S的子串,并且要求输出P在S中匹配的位置(即P的第一个字母在S中的位置)。我们称P为模式串,S为主串

比方说,S=“An apple a day, keeps doctor away” , P=“apple” 那么P是S的子串,其在S的位置为3(下标从0开始)

暴力破解法

解决这个问题最简单的思路是使用暴力破解法

将P[0]和S[i]对齐(初始化i=0),然后逐个比较,如果中间遇到P[j]!=S[i+j] 说明不匹配(我们也称之为"失配")。
则将P向右移动一位,让P[0]和S[i+1]对齐,再逐个比较,失配则P右移一位 , 直到P的尾部和S的尾部对齐,如果都没有找到匹配的位置就说明P不是S的子串,否则就返回匹配的位置

再画个图简单说明下吧,其中蓝色的字母表示是相等的,红色的表示不相等也就是失配

S: abadadcb
P: adca

第一轮匹配:

第二轮匹配:

第三轮匹配:

第四轮匹配:

第五轮匹配:

理解了上面的说明,我们就可以写出暴力破解法的代码

   /*** @param s 主串* @param p 模式串*/public static int violentMatch(String s, String p) {int sLen = s.length();int pLen = p.length();int i = 0;int j = 0;while (i < sLen && j < pLen) {// 当两个字符相同,就比较下一个if (Objects.equals(s.charAt(i), p.charAt(j))) {i++;j++;} else {//一旦不匹配,i后退 , j归0j = 0;i = i - j + 1;}}if (j == pLen) {return i - j;} else {return -1;}}

用上面的S和P代入计算结果为:-1

暴力法的优点就在于代码清晰,非常易于理解,其缺点也很明显,时间复杂度太高(O(m*n)) 。那么有没有办法提高匹配的效率呢?

在暴力法中,匹配失败后模式串都需要向右移动1位,然后从头与主串进行匹配,要将前面已经匹配过的字符重新再匹配一遍,效率自然就差了,那么是不是有办法让模式串可以在失配时无需全部重新匹配呢?

比方说上面的第三轮匹配,从上帝视角来看,在S[4]与P[2]失配后,我们可以直接移动2位继续匹配
这是因为P[0~1]==S[2~3],而P[1]!=P[0],所以移动一位后一定有P[0]!=S[3] (如下图),这样我们就可以根据模式串P的一些性质来提高匹配的效率

失配后模式串P直接移动2位

KMP算法

KMP算法由D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人于1977年共同提出,全称为 Knuth-Morria-Pratt 算法。该算法相对于暴力破解算法有了比较大的优化,主要是消除了主串指针的回溯,从而提高了算法效率。

先说明几个概念:

前缀:空字符串或包含字符串第一个字符的任意连续的子串(不包含自身)。
如对于 name这个字符串, 空字符串,n,na,nam都是前缀。

后缀:空字符串或包含字符串最后一个字符的任意连续的子串(不包含自身)。
如对于 name这个字符串, 空字符串,e,me,ame都是后缀。

公共前后缀:对于某一个字符串,它的前缀和后缀中如果存在相同的,那么该字符串就是公共前后缀。其中长度最长的那个就是最大公共前后缀


假设两个模式串在红色部分(设为S[m])发生失配,此时我们需要向后移动模式串P, 如果在移动过程中S串的蓝色部分和P串的蓝色部分还能找到匹配的子串,那么就会出现下图的情形

那么在发生失配时,我们只需要将模式串移动到如下位置即可继续比较模式串和主串,从失配位S[m]开始进行比较而不需要对主串进行回溯。这里的深蓝色部分就是前面蓝色部分字符串的最大公共前后缀。

为什么一定是最大公共前后缀的位置一定是最优的而不是其中的任一位置呢? (能理解的可以不用看这段叙述)

假设我们可以将模式串移动到中间某一位置,如上图所示,此时两个深蓝色块和深绿色块是相等的,如果P[i]!=S[j], 那么就说明在这个位置会发生失配,这就不符合我们一开始想要的最优的位置。如果P[i]==S[j],那么根据前后缀的定义,灰色框部分是红色框部分的最大公共前后缀,这样一来我们继续找S[j+1]和P[i+1], 如果不相等则不满足我们想要的最优位置,如果相等红色框的部分就可以继续向后扩大,直到等于一开始匹配的蓝色部分。

故而,假设模式串和主串在S[j]和P[i]的位置发生失配,那么我们只需要知道P[0…i-1]的最大公共前后缀是什么,就可以知道需要把模式串移动到什么位置就可以在避免主串回溯的情形下继续匹配

那么接下来需要的就是求模式串P的各个前缀的最大公共前后缀了

next数组

next数组的含义
假设 next[j] = k; 那么就是表示长度为j的数组的前缀和后缀相等的最大长度为k(k<j)


按照next数组的定义,有next[0]=0,next[1]=0,为了计算方便,我们规定next[0]=-1

那么已知next[0]…next[j], next[j+1]怎么计算?
假设next[j]=k
分两种情形考虑
1)P[j]==P[k], 按照next[j]的定义,我们可以知道P[0…k-1]==P[j-k…j-1]
这样就有P[0…k]==P[j-k…j],也就是next[j+1]=k+1

2)P[j]!=P[k],那么next[j+1]的最大公共前缀一定小于k,假设为字符串M,长度为m(m<k)

那么M一定是字符串A的前缀,也是字符串S的后缀,又由于S=A+P[j], 所以M一定是字符串S的公共前后缀,假设next[k]=x, 那么next[j+1]=m<=x+1,即next[j+1]<=next[k]+1 注意:两个蓝色块的字符串是相等的,也就是他们各自的最大公共前后缀也是一样的

接下来让我们来证明下这个结论(使用反证法)
如果next[j+1]>next[k]+1,也就是说next[j+1]>=next[k]+2
而又因为next[j+1]的最大公共前后缀是字符串S的公共前后缀,也就是说字符串S存在一个公共前后缀,其长度不小于next[k]+2 ,如下图所示:
根据公共前后缀的定义我们可以知道字符串A有一个长度不小于next[k]+1的公共前后缀,这与next[k]的定义矛盾,故而假设不成立,也就是说在P[j]!=P[k]的时候,next[j+1]<=next[k]+1

那么前j+1个字符串的最大公共前后缀最大可能就是next[k]+1,要想使next[j+1]=next[k]+1, 按照下图就需要有
P[j-k+next[k]]==P[j],根据next[j]=k可知P[j-k+next[k]]==P[next[k]], 故而当P[next[k]]==P[j]时,next[j+1]=next[k]+1

那万一P[next[k]]!=P[j]怎么办呢? 那么字符串S的最大公共前后缀一定小于next[k],和上面的推理一样,此时字符串S的最大公共前后缀一定是红色框字符串的公共前后缀,其长度一定<=next[next[k]]+1

我们将几次推论的图放在一起,你就可以看到其中类似的地方


我们可以得出一个结论,想要计算next[j+1],那么我们就需要比较P[j]和P[k]是否相等,如果相等,那么next[j+1]=k+1,否则继续按照k=next[k]进行递归,直到next…[next[k]…] == P[j] 或者next…[next[k]…] == -1

关于next数组的求解以及其推导过程就分析到这了,接下来就可以开始写代码了

感谢v_JULY_v大佬的图,让我终于get到为什么要做k=next[k]的递归 (下图来自于v_JULY_v 从头到尾彻底理解KMP(2014年8月22日版)一文)

KMP算法实现

   //计算next数组public static void calNextArray(String str, int next[]) {int k = -1;//k表示当前字符串的最大公共前后缀,初始为-1即next[0]=-1int j = 0;//j代表字符串长度,从0(空字符串)开始递推next[0] = k;//next[0]=-1//递推过程 需要计算next[0],next[1],....next[str.length()-1]while (j < str.length() - 1) {//k == -1代表特殊边界条件,str[k] == str[j]表示新增字符和str[k]的字符相同if (k == -1 || str.charAt(k) == str.charAt(j)) {k++;j++;next[j] = k;} else {//此处代表str[k] != str[j],去找最大公共前后缀加上新字符后的最大公共前后缀k = next[k];}}}

如果觉得递归和next[0]=-1还有点迷糊,可以找个字符串然后计算下其next数组

 public static int kmp(String s, String p) {int i = 0;int j = 0;int sLen = s.length();int pLen = p.length();int[] next = new int[pLen];calNextArray(p, next);while (i < sLen && j < pLen) {//如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++if (j == -1 || Objects.equals(s.charAt(i), p.charAt(j))) {i++;j++;} else {//如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]//next[j]即为j所对应的next值j = next[j];}}if (j == pLen)return i - j;elsereturn -1;}

参考资料:
https://blog.csdn.net/Iseno_V/article/details/100114480
https://blog.csdn.net/v_july_v/article/details/7041827

题外话:

抛开前面讲的kmp算法,如果光从主串S和模式串P进行字符串匹配的角度来看的话,那么其效率最高的办法就是每一轮比较完之后,主串完全不回溯,而模式串P的直接移动到S[m]的位置开始第二轮匹配(n为主串的长度,m为子串的长度),其最优的时间复杂度为O(n/m)
但是这样比较的话如果在S[0-m-1]有部分字符串可以和模式串P匹配,那么这个单模匹配返回的结果就会失去精准性,为了可以"发现"这种可能匹配的情形,所以模式串不能移动m位置,而需要移动到如上图所示的位置, 这就需要利用到模式串P的对称性,也是KMP引入最大公共前后缀(next数组)的原因

KMP算法的时间复杂度

KMP算法流程:
1.计算出模式串P的next数组(对应的最大公共前后缀)
2.比较模式串P[j]和主串S[i]对应的字符是否相等
如果j == -1或者P[j] == S[i],则i和j都递增,匹配下一个字符;
如果j != -1且P[j] != S[i],则i不变,j = next[j],也就是在发生失配的时候,模式串向右移动j - next [j] 位。

那么其时间复杂度就是计算next数组的时间复杂度加上匹配的时间复杂度 : O(next[])+O(匹配)

计算next数组的时间复杂度为O(m) (m为模式串的长度);匹配的次数为 n (因为主串匹配的时候不会回溯) + 失配后移动模式串之后失配位S[i]会再重复比较一次,也就是说O(2*n)

所以整个KMP算法的时间复杂度就是O(m+n)

终于完全弄懂了KMP(个人理解篇)相关推荐

  1. 终于弄懂KMP算法了

    1.简例弄懂KMP-点此链接查看 看了上面的文章,你肯定大概明白了KMP的运作原理,但是你可能对于文章提到的"部分匹配值"的又来还存在疑惑,那么请继续往下看: 我们先抛出两个问题, ...

  2. 计算机考研英语一和英语二的区别,考研英语一和英语二的区别 今天终于弄懂了!...

    原标题:考研英语一和英语二的区别 今天终于弄懂了! 大家在最后三个月冲刺需要注意: 1.建议留几套真题,做考前模拟,精读真题可以用 <考研圣经>(英语二用)98-07 年的真题,都是逐词逐 ...

  3. 这一次,终于弄懂了协变和逆变

    一.前言 刘大胖决定向他的师傅灯笼法师请教什么是协变和逆变. 刘大胖:师傅,最近我在学习泛型接口的时候看到了协变和逆变,翻了很多资料,可还是不能完全弄懂. 灯笼法师:阿胖,你不要被这些概念弄混,编译器 ...

  4. 快速傅里叶变换(研二的我终于弄懂了)

    研二的我仍然对快速傅里叶变换一知半解,于是乎,本着待在家里,能耗时间就多耗点,不知道何年马月我才可以在外面快乐的奔跑~~ 快速傅里叶变换的实现(c++版本) 在做项目的时候,需要用到matlab里的f ...

  5. 淘宝特价版拉新赚钱的页面怎么做?我终于弄懂了

    淘宝的同胞兄弟特价版,虽然长的朴实无华以至于经常被人问起淘宝特价版靠谱吗?2021年淘宝特价版可谓大火了一把,阿里巴巴不计成本的大力推广淘宝特价版,目的也非常明确要把拼多多占领的市场掠夺回来.最近还传 ...

  6. NND今天终于把KD树弄懂了,花了劳资两个小时的有效时间

    kd树 kd树的作用: 数据量大时快速找到临近的k点 避免每次重新计算距离,算法会把距离信息保存在一棵树里,需要时直接调用 kd树原理: kd树核心: 1.树的建⽴: 建立kd树的核心原理:⼩于等于就 ...

  7. 一文让你完全弄懂逻辑回归和分类问题实战《繁凡的深度学习笔记》第 3 章 分类问题与信息论基础(上)(DL笔记整理系列)

    好吧,只好拆分为上下两篇发布了>_< 终于肝出来了,今天就是除夕夜了,祝大家新快乐!^q^ <繁凡的深度学习笔记>第 3 章 分类问题与信息论基础 (上)(逻辑回归.Softm ...

  8. 这一次,彻底弄懂 JavaScript 执行机制

    本文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我. 不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定 ...

  9. 硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!

    " 摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/jar/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1. 概述 2. MANIFEST.MF 3. J ...

最新文章

  1. 8W+文本数据,全景式展现中国教育发展情况
  2. https协议和Http协议的区别
  3. Ubuntu 20.04 搜索引擎环境搭建 (PostgreSQL 12.3, Redis 6, ELK[Elasticsearch 7.8, Logstash 7.8, Kibana 7.8])
  4. Leedcode9-linked-list-cycle-i
  5. mysql show 翻页_mysql show操作
  6. MATLAB安装工具箱
  7. Nagios 监控windows性能计数器
  8. 游戏开发之C++继承与派生(包含访问控制)(C++基础)
  9. [iOS_Dev] 官方Mac OS X.dmg 下载,dmg 转 iso,Mac 镜像。
  10. Photoshop插件-黑白(一)-脚本开发-PS插件
  11. Qt QDir用法及实战案例
  12. (十一)【数电】(组合逻辑电路)数据分配器和数据选择器
  13. 一篇文章带你搞定 create connection SQLException, url: jdbc:mysql://10.15.16.63:3306/restful, errorCode 1130
  14. 腾达无线路由器怎么设置能让自己的网速快
  15. ABAQUS软件实训(四):Mesh模块之六面体网格划分技巧
  16. c语言计算利息答案是0.0,ACCP北大青鸟4.0 程序逻辑和C语言实现课本后的习题和上机题目,怎么做?...
  17. Java 姓名脱敏的一点点改进 针对大于三个字 或叠字
  18. 手动删除7千万个Reids的Key是什么体验响!
  19. Java程序员需要了解的几个开源协议开源协议
  20. html文本标签练习

热门文章

  1. 斐波那契数列python递归 0、1、1、2、3_python实现斐波那契数列的多种方式
  2. TensorFlow机器学习实战指南之第一章
  3. Data Lake Analytics: 使用DataWorks来调度DLA任务
  4. Istio流量管理实践之(3): 基于Istio实现流量对比分析
  5. iOS开发Drag and Drop简介
  6. 大量执行OSS PutObject时卡住的问题排查
  7. 通过OWA修改密码,提示输入的密码不符合最低安全要求
  8. 勒索软件指向Flash与Silverlight漏洞
  9. 内存泄漏的常见应用领域
  10. IIS6.0官方技术必读