背景

KMP 是经典的字符串匹配算法,在大学课本里面都是讲到过的,不过我感觉课本都讲的太生硬了,很容易忘记。大多数博客也是讲的云里雾里的,一开始就来个公式+部分匹配表,反正我感觉对于没有计算机基础的童鞋来说还是有点不容易理解,经过前两天在开发者头条上看到的一篇关于kmp算法的讲解,我看完了于是有了整理下来的冲动。
说明:

  1. 其中i为主串的起始位置的下标
  2. 其中j为模式串匹配失败位置的下标
  3. 比如要在a=‘abbcf’,b=‘sjldfsdabbcfdsfs’,要在b中找a的,则a为模式串,b为主串

首先要明确的一点是,KMP 算法本身包含两个步骤:

  1. KMP 算法本身的思想与策略;
  2. 利用动态规划的思想来求一个字符串所有前缀子串的最大共同前后缀子串的长度(也就是一直在说的 next 数组);

两个部分其实关系不大,但很多文章都很喜欢将其混在一起讲。我们首先先讲第一步。

暴力匹配(也就是一个一个的匹配)

例如:实现 substr, 在字符串 haystack 中,找字符串 needle 的位置,不存在则返回-1.

最符合直觉的方法就是穷举法,遍历 haystack 中每个字符,对于每个字符,都尝试以该字符作为开始于 needle 进行逐个字符串的比较,匹配上了则返回当前的 i。整个过程可以看做是模式串(needle)从对齐 haystack 第一个字符开始逐步一个一个往后移动的过程。

假设 haystakc=”Hello Hella”, need=”Hella”, 过程如下:
i=0:
Hello Hella
Hellai=1:
Hello HellaHellai=6:
Hello HellaHella(matched)整个过程一共执行了 6 次检查。
代码如下:
实例一:
def match_str(haystakc, need):temp = 0main_str_len, son_str_len = len(haystakc), len(need)while temp <= main_str_len - son_str_len:if haystakc[temp:temp + son_str_len] == need:return tempelse:temp += 1else:return -1
实例二:
def naive_matching(t, p):m, n = len(p), len(t)i, j = 0, 0while i < m and j < n:  # i==m说明找到匹配if p[i] == t[j]:  # 字符相同,开始匹配下一对i, j = i + 1, j + 1else:  # 字符不同,开始t中的下一个位置i, j = 0, j - i + 1if i == m:  # 找到匹配,返回下标return j - ireturn -1haystakc = 'Hello Hella'
need = 'Hella'
print(match_str(haystakc, need))
print(naive_matching(haystakc, need))

尝试优化

分析一下刚才 i=0 的执行过程。H、e、l、l 都匹配成功,仅最后的 o 和 a 匹配失败。此时移动模式串 Hella,我们很容易发现接下来的几步(i=1到 i=5)都是徒劳无功,我们是否可以利用先验知识(比如 Hell 之前已经匹配成功)来避免这几次比较的浪费呢?

首先做一个大胆的假设,当基于主串的第 i 个字符,一旦模式串的第 j 位匹配失败。当 j>0时,则主串的 [i, i+j)区间直接跳过,从 i+j位置开始搜索,当 j 小于=0 时,则直接从 i+1的位置开始搜索,简单的来说就是比如有:

i=0
Hello Hella
Hella(此时 j = 4, 代表模式串第五位匹配失败)则下一次直接从 i+j = 4位置开始搜索i=4
Hello HellaHella(j=0,失配,下一次 i=i+1 = 5)i=5
Hello HellaHella(j=0,失配,下一次 i=i+1 = 6)i=6
Hello HellaHella(matched)

特殊情况

上面的是一个假设,有没有可能存在不满足该假设的 bad case 呢。比如看以下的例子, 主串=”HelloHelloHead” 模式串=”HelloHead”

i=0
HelloHelloHead
HelloHea (a 和 l 不匹配,此时 j=7)按照刚才的公式,直接跳过已经匹配好的部分,跳到 i+j = 7处开始下一次匹配i=7
HelloHelloHeadHelloHead (匹配失败,i=i+1)i=8
HelloHelloHeadHelloHead (匹配失败)

我们知道这个 case 应该是能够匹配成功才对,但是按照我们的假设流程却失败了。通过分析过程能够发现,本质上是我们跳过得太多了。那到底要跳过多少比较合适呢?先回头看看例子

i=0
HelloHelloHead
HelloHea (a 和 l 不匹配,此时 j=7)j=7 的位置不匹配,但能看到前面两个位置是 H 和 e并且是匹配成功的,加上模式串的开头,也是 H 和 e,看起来可以**直接把开头的 He,对齐到后面的 He**,也就是i=5 的位置i=5
HelloHelloHeadHelloHead(matched)那更新一下我们的假设,当匹配失败时,直接跳到 i+j的位置开始新的匹配,但对于某些case,需要少跳几步

kmp算法

KMP 算法的本质就是定义了什么样的情况需要少跳,以及具体少跳几步。

  1. 其实从刚才的 case 不难发现,需要少跳的步数就是模式串已经成功匹配的部分的共同前后缀的长度, 比如刚才的 HelloHead,已经成功匹配的部分是 HelloHe,这个字符串具备共同的前后缀 He,长度为2.
  2. 共同的前后缀简单的理解就是前缀和后缀相同的字符串,比如 ABBA,共同前后缀是 A; ABBABB,共同前后缀是 ABB;ABBABBAAC, 无共同前后缀。
  3. 基于此,我们进一步更新我们的假设,当匹配失败时,如果已经匹配的模式子串无共同前后缀,则直接跳到 i+j的位置开始新的匹配,若存在共同子串,则跳转到 i+j-共同子串长度的位置开始新的匹配
  4. 因为模式串每一位都可能发生失配,所以我们需要求出模式串所有前缀子串分别的最大相同前后缀子串长度。比如模式串是 ABCDABC,我们需要分别求出 A、AB、ABC、ABCD、ABCDA、ABCDAB、ABCDABC 分别的最大相同前后缀的长度,比如 ABCDAB 是 2(AB),ABCDABC是 3(ABC),而 ABCDA 是 1(A)。这部分最后的结果存放在一个数组,普遍称之为 next 数组,其中next数组得出来了后,需要讲next数组每一位向后移动一位,这是next数组的第一个位置为-1,模式串最长的字串的最长公共前后缀不需要计入next数组。
  5. 例如字符串“ABCDABD”,它的next数组如下

    失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(可以自行验证,这些都是前人已经总结好的规律)

动态规划的思想求 next 数组

def matching_KMP(t, p, pnext):j, i = 0, 0n, m = len(t), len(p)while j < n and i < m:if i == -1 or t[j] == p[i]:j, i = j + 1, i + 1else:i = pnext[i]if i == m:return j - ireturn -1def gen_pnext(p):i, k, m = 0, -1, len(p)pnext = [-1] * mwhile i < m - 1:if k == -1 or p[i] == p[k]:i, k = i + 1, k + 1pnext[i] = kelse:k = pnext[k]return pnexthaystakc = 'Hello Hella'
need = 'Hella'
pnext=gen_pnext(need)
print(matching_KMP(haystakc, need, pnext))

No.5终于搞懂了kmp算法(精髓为next数组的求解过程,此文next数组未经过优化)相关推荐

  1. IntelliJ IDEA 部署 Web 项目,终于搞懂了!

    IntelliJ IDEA 部署 Web 项目,终于搞懂了! 这篇牛逼:Java 程序员必备的 Intellij IDEA 插件 IDEA 中最重要的各种设置项,就是这个 Project Struct ...

  2. 电压和电流反馈判别及例子,绝对让你通透,其实也没有那么难,一次就看懂!从此终于搞懂了电压反馈和电流反馈!

    电压和电流反馈判别及例子,其实也没有那么难,绝对让你通透,一次就看懂!从此终于搞懂了电压反馈和电流反馈! 一个简单粗暴的判断方法: 先看反馈是否直接连到Uo输出端(若不是直接从输出端引出,则为电流反馈 ...

  3. 一文搞懂BPE分词算法

    大家好,我是Xueliang,又和大家见面了. 我最近在打机器翻译的一个比赛,主要使用基于BERT的模型.在这其中,一个小的知识点引起了我的好奇,就是在将英语训练语料输入到BERT模型之前,需要对其进 ...

  4. HTTPS 终于搞懂了 !

    点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | ...

  5. 字符串:你看的懂的KMP算法(带验证)

    前言 KMP算法可以说说许多学习算法的同学的第一道坎,要么是领会不到KMP算法的思想,要么是知道思想写不出代码,网上各种查找.关于算法的书籍上也都有KMP算法的实现,可为啥自己写不出来呢?博主看得大话 ...

  6. 最短路算法的证明_彻底搞懂最短路算法

    只想说:温故而知新,可以为师矣.我大二的<数据结构>是由申老师讲的,那时候不怎么明白,估计太理论化了(ps:或许是因为我睡觉了):今天把老王的2011年课件又看了一遍,给大二的孩子们又讲了 ...

  7. 目标检测扩(六)一篇文章彻底搞懂目标检测算法中的评估指标计算方法(IoU(交并比)、Precision(精确度)、Recall(召回率)、AP(平均正确率)、mAP(平均类别AP) )

    ​ 基本在目标检测算法中会碰到一些评估指标.常见的指标参数有:IoU(交并比).Precision(精确度).Recall(召回率).AP(平均正确率).mAP(平均类别AP)等.这些评估指标是在评估 ...

  8. 【JVM】一文搞懂常见GC算法

    文章内容 1.概述 2.如何确定垃圾对象? 3.GC算法 4.GC算法总结 5.常见的垃圾收集器 1.概述 GC目的:程序运行过程中可能会产生许多垃圾对象,持续占用内存会造成内存泄漏,最终可能导致内存 ...

  9. 关于子网掩码怎么计算!!!!我终于搞懂了!!!!

    今天终于搞明白了子网掩码啥的是啥意思了!!!我写几个就我自己看懂的! 1.首先ip呢都是XXX.XXX.XXX.XXX这样组成的然后一般来说就是255.255.255.255,对应的二进制文件就是11 ...

最新文章

  1. UDP收/发广播包原理及步骤
  2. 企业中常用的几种文件传输方法介绍
  3. 程序员必练六大项目:从数据结构到操作系统,计算机教授为你画重点
  4. vs html自动对齐,vscode esLint 保存时 自动对齐
  5. 直击阿里新一代数据库技术:如何实现极致弹性能力?
  6. mysql默认join是什么类型_MySQL:join语句类型
  7. 走进统信软件,读懂国产操作系统新生态建设
  8. java定时器 并发_【java多线程与并发库】— 定时器的应用 | 学步园
  9. 7002.ubuntu18.04将软件图标固定到工具栏
  10. 北交大实验室爆炸事故后续
  11. 使用IDEA创建Maven项目教程
  12. Linux下打开Android调试器DDMS的方法
  13. 打造适合 Ruby on Rails 开发的 Sublime Text
  14. 同城交友小程序项目功能方案介绍
  15. python批量解压rar和zip的压缩包
  16. flutter 后台管理框架
  17. 小米手机隐私相册在哪?将你的照片锁进“保险柜”
  18. 关于安装ANACONDA遇到的无法定位动态链接库问题
  19. python白平衡-树莓派摄像头Camera的使用
  20. Oracle之用户、特权和角色

热门文章

  1. 【翻译】Sencha Touch 2入门:创建一个实用的天气应用程序之三
  2. 数字孪生医院的智能化运营平台建设内容
  3. 深度linux 挂载硬盘,Deepin 深度磁盘挂载
  4. MATLAB2018a 64安装
  5. 预警信息发布程序设计
  6. windows-sys5:升级win11——此版本Windws不支持该处理器、该电脑必须支持TPM2.0等问题解决
  7. 如何入门UI设计?学ui设计要看什么书籍
  8. 如何将QQLive和QQ2010和虚拟光驱运行在ubuntu上
  9. Opengl入门基础-shader着色器画方形并且填颜色
  10. Linux系统代理上网