KMP算法

  关于字符串匹配的算法,最知名的莫过于KMP算法了,尽管我们日常搬砖几乎不可能去亲手实现一个KMP算法,但作为一种算法学习的锻炼也是很好的,所以记录一下。

  KMP算法是根据三位作者(D.E.Knuth, J.H.Morris和V.R.Pratt)的名字来命名的,算法的全称是Knuth Morris Pratt算法,简称为KMP算法。

  关于字符串匹配,我们假设要在字符串A中查找字符串B,那么我们可以把字符串A叫做主串,把B叫做模式串。所以字符串匹配其实就是要在主串中找到与模式串相同的子串。假设主串长度是n,模式串长度为m,最简单直接的想法是,我们在主串中检查起始位置分别是0,1,2...n-m且长度为m的子串,看有没有跟模式串匹配的。这其实也是字符串匹配BF算法的思想,所谓BF就是Brute Force的缩写,中文叫做暴力匹配算法,也叫朴素匹配算法。

  在BF算法中,如果我们遇到了不匹配的子串,会将模式串向后移动一位并再次进行匹配。而KMP算法的核心思想是,如果遇到了不匹配的字符串的时候尝试寻找一些规律,将模式向后多移动几位,跳过那些肯定不会匹配的情况。

  

好前缀与坏字符

  先来看一个例子:

主串 a b a b a e a b a c
模式串 a b a b a c d      

  在模式串与主串的匹配过程中,我们把以及匹配好的那部分叫做好前缀(蓝色部分),把不能匹配的那个字符叫做坏字符(红色部分)。当遇到坏字符的时候说明这次匹配失败了,因此我们要向后移动模式串。KMP的核心思想是不匹配时利用规律向后多移动几位。观察一下好前缀本身,在它的后缀子串中,查找到最长的那个可以跟好前缀的前缀子串匹配的子串。上面的文字描述有一些绕口,我们基于上面的表格尝试将模式传向后移动两位就可以达到符合条件的那种效果,结合图来看一下。

  

主串 a b a b a e a b a c
模式串     a b a b a c  d   

  第一次匹配时我们获得的好前缀是‘ababa’,在它的后缀子串中,最长的可以跟它的前缀子串匹配的字符串是‘aba’(上图黄色部分)。假设好前缀的长度是L,最长的可匹配的哪部分前缀子串的长度是l,那我们就可以直接把模式传向后移动L-l位,然后再继续比较。结合上面的内容,好前缀的长度是5,最长的可以跟它的前缀子串匹配的后缀子串的长度是3,因此可以直接向后移动2位。

  在上面的过程中,其实并没有涉及到主串,只需要模式串本身就可以求解。因此可以提前构建一个数组,用来存储模式串中每个前缀(这些前缀都有可能是好前缀)的最长可匹配前缀子串的结尾字符下标。这个数组定义为next数组,在一些地方把这个数组称之为“失效函数”(failure function)。

  数组的下标是每个前缀尾部字符的下标,数组的值是这个前缀的最长可匹配前缀子串的结尾字符下标。还是用表格记录一下:

模式串 a b a b a c d
下标 0 1 2 3 4 5 6
模式串前缀 前缀结尾字符下标 最长可匹配前缀字符串结尾字符下标
a 0 -1(不存在)
ab 1 -1
aba 2 0
abab 3 1
ababa 4 2
ababac 5 -1

  上面表格中,存在最长可匹配前缀字符串的模式串前缀有'aba','abab','ababa'这样三个,注意这三个前缀的最长可匹配前缀字符串结尾字符下标的值,再加1其实就是我们在匹配过程中遇到坏字符后可以向后移动的长度。

  因此,如果我们在匹配之前就可以利用模式串得到一个类似与上面表格的内容,那么匹配过程就变成了这样:依次比较主串与模式串,直到遇到了坏字符或者整个模式传匹配完成。如果全部匹配上了就是我们找到了对应的结果。如果遇到了坏字符我们就利用预先求得的内容去数组中查询应该向后移动几位,并直接移动模式串,并继续进行匹配。

  

 public int kmp(char[] a, char[] b) //a为主串,b为模式串
{
int[] next = getNext(b); //利用模式传预先求得next数组的值。
int j = ; //检测模式串移动的下标 for(int i = ; i <= a.Length; i ++)
{
/*注意这里,使用while来判断而不是if,因为可能移动后的下一位,即最长可匹配前缀的下一位仍然与坏字符不匹配的情况,此时需要再次查表,直到找到了匹配的内容或是返回到模式串的首字符。*/
while(j > && a[i] != b[j])
{
j = next[j - ] + ;
} if(a[i] == b[j])
{
j++;
} if(j == b.Length) //找到匹配的字符串了
{
return i - b.Length + ;
}
} return -;
}

  经过上面的内容我们可以看出,KMP较暴力匹配方法高效的原因是可以利用事先求得失效函数的值,在遇到不匹配的字符时快速向后移动多位,因此如何预先求得失效函数的的值变成了问题的关键。

  为了保证KMP的高效,我们获取next数组的值的方法也应该尽量高效。这个计算方式其实有一些动态规划的思想,我们按照下标递增的方式依次计算next数组的值,当计算next[i]的时候,next[0],next[1].....next[i-1]应该已经计算出来了。这里重温一下,next数组的下标代表模式串的前缀结尾字符下标,值为对应的前缀最长可匹配的前缀子串的字符下标。

  先来看一种比较简单的情况。假设模式串数组为b,我们的目的是求得next[i],那么应该已经求得了next[i-1]的值。假设next[i-1] = k - 1。那么就说明b[0,k-1]也是b[0, i-1]的后缀。那么我们考察b[k]这个字符是否与b[i]这个字符相等,如果相等,那么b[0,k]也就是b[0,i]的后缀,也就求出了next[i] = k。(相等的两个字符串在末尾分别添加一个相等的字符,新的字符串仍然相等。)

  如果b[k] != b[i],那就不能这么计算了。下面的过程有些不好理解,笔者能力一般,水平有限,尽量解释吧。我们顺着刚才的思路,既然不能直接利用next[i-1]的最长可匹配前缀字符串了,我们就尝试去使用次长可匹配前缀字符串。举个不恰当的例子,比如我们的模式串b[0, i-1]='ababa',那么它的最长可匹配前缀字符串是’aba‘(最长可匹配后缀字符串也是'aba'),当这个最长的值不能使用时,我们就退而求其次,使用次长字符串a。注意这个次长的值,当它是前缀字符串时,它较最长前缀减少的是末尾,当它是后缀字符串时,它较最长后缀减少的是开头字符。这个时候我们再去考察这个次长子串的下一位,假设是b[x],如果b[x]与b[i]相等,说明我们找到了结果,那么next[i] = x。否则我们就需找再次的最长前缀。

KMP算法的复杂度

  KMP算法的空间复杂度是O(M),M是模式串的长度。

  KMP算法的时间复杂度,第一部分计算next数组的时间复杂度是O(M),M是模式串的长度,第二部分匹配的时间复杂度是O(n),n为主串的长度。所以综合来看,KMP算法的时间复杂度是O(m+n)。

LeetCode刷题--基础知识篇--KMP算法的更多相关文章

  1. LeetCode刷题总结-数组篇(上)

    数组是算法中最常用的一种数据结构,也是面试中最常考的考点.在LeetCode题库中,标记为数组类型的习题到目前为止,已累计到了202题.然而,这202道习题并不是每道题只标记为数组一个考点,大部分习题 ...

  2. LeetCode刷题总结-数组篇(下)

    本期讲O(n)类型问题,共14题.3道简单题,9道中等题,2道困难题.数组篇共归纳总结了50题,本篇是数组篇的最后一篇.其他三个篇章可参考: LeetCode刷题总结-数组篇(上),子数组问题(共17 ...

  3. LeetCode刷题总结-数组篇(中)

    本文接着上一篇文章<LeetCode刷题总结-数组篇(上)>,继续讲第二个常考问题:矩阵问题. 矩阵也可以称为二维数组.在LeetCode相关习题中,作者总结发现主要考点有:矩阵元素的遍历 ...

  4. LeetCode刷题总结-树篇(上)

          引子:刷题的过程可能是枯燥的,但程序员们的日常确不乏趣味.分享一则LeetCode上名为<打家劫舍 |||>题目的评论: 如有兴趣可以从此题为起点,去LeetCode开启刷题之 ...

  5. LeetCode刷题总结-树篇(下)

    本文讲解有关树的习题中子树问题和新概念定义问题,也是有关树习题的最后一篇总结.前两篇请参考: LeetCode刷题总结-树篇(上) LeetCode刷题总结-树篇(中) 本文共收录9道题,7道中等题, ...

  6. LeetCode刷题总结-树篇(中)

    本篇接着<LeetCode刷题总结-树篇(上)>,讲解有关树的类型相关考点的习题,本期共收录17道题,1道简单题,10道中等题,6道困难题. 在LeetCode题库中,考察到的不同种类的树 ...

  7. LeetCode刷题预备知识(二)

    Python四大数据结构的属性及方法 在LeetCode刷题预备知识一中我们掌握了常见的内置函数,和四大数据结构的基本概念: 但只掌握这些还远远不够,我们还需了解四大数据结构的属性及方法才能更高效快速 ...

  8. LeetCode刷题专栏第一篇--思维导图&时间安排

    昨天是元宵节,过完元宵节相当于这个年正式过完了.不知道大家有没有投入继续投入紧张的学习工作中.年前我想开一个Leetcode刷题专栏,于是发了一个投票想了解大家的需求征集意见.投票于2019年2月1日 ...

  9. LeetCode刷题总结-动态规划篇

    本文总结LeetCode上有动态规划的算法题,推荐刷题总数为54道.具体考点分析如下图: 1.中心扩展法 题号:132. 分割回文串 II,难度困难 2.背包问题 题号:140. 单词拆分 II,难度 ...

随机推荐

  1. PyQt5的菜单栏、工具栏和状态栏

    1.创建菜单栏import sys, mathfrom PyQt5.QtWidgets import *from PyQt5.QtGui import *from PyQt5.QtCore impor ...

  2. spark sql 访问mysql数据库

    pom.xml <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-ja ...

  3. 25 JavaScript对象原型&ES5新的对象方法

    JavaScript对象原型 所有JavaScript对象都从原型继承对象和方法 日期对象继承自Date.prototype,数组继承自Array.prototype,对象构造器新建的对象Person ...

  4. vs2017 vs2019配置sqlite3连接引擎(驱动)指南(二)vs2019续集

    在写完上一篇博客后,一觉醒来,又又又又不行了,介绍一个终极大招,如果你的fuck vs又提示无法打开sqlite3.h的问题 环境win10  vs2019 debug x86 实在没心情写文字了,直 ...

  5. 计划任务cron,date,时间同步ntp,chrony

    取出磁盘利用率最大值 ,写个脚本,做判断,只要快满了,就报警 计划周期性执行的任务提交给crond,到指定时间会自动运行 系统cron任务:系统维护作业  /etc/crontab 用户cron任务: ...

  6. 转专业后对于C语言补修的一些体会(1)

    在转入软件工程后,原来的C语言程序设计只有三学分,而信息学院的C语言程序设计有四学分.迫于无奈的我只能再补修一遍C语言,自我认为大一对于C语言的学习已经基本足够,但我发现信息学院用的是不一样的书后,对 ...

  7. vue下canvas绘制矩形

    起因:根据项目需求本人写了一个绘制矩形的组件.功能:在图片中绘制矩形,根据图片大小进行自适应展示,获取图片矩形坐标.思路:首先定义一个固定大小的DIV,DIV标签中有监测鼠标变化的四个事件moused ...

  8. Educational Codeforces Round 74 (Rated for Div. 2)E(状压DP,降低一个m复杂度做法含有集合思维)

    #define HAVE_STRUCT_TIMESPEC#include<bits/stdc++.h>using namespace std;char s[100005];int pos[ ...

  9. 【PAT甲级】1004 Counting Leaves (30 分)(BFS)

    题意:给出一棵树的点数N,输入M行,每行输入父亲节点An,儿子个数n,和a1,a2,...,an(儿子结点编号),从根节点层级向下依次输出当前层级叶子结点个数,用空格隔开.(0<N<100 ...

  10. 刚下载好的 vscode 不能运行,一片黑 以及终端不能输入 解决办法

    1.鼠标右键vscode快捷方式点击属性,选择兼容性,勾选以兼容模式运行,下拉列表调整为windows vista (service pack 1)即可解决. 2.如果打开终端不能输入命令,首先点击属 ...