LeetCode刷题--基础知识篇--KMP算法
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算法的更多相关文章
- LeetCode刷题总结-数组篇(上)
数组是算法中最常用的一种数据结构,也是面试中最常考的考点.在LeetCode题库中,标记为数组类型的习题到目前为止,已累计到了202题.然而,这202道习题并不是每道题只标记为数组一个考点,大部分习题 ...
- LeetCode刷题总结-数组篇(下)
本期讲O(n)类型问题,共14题.3道简单题,9道中等题,2道困难题.数组篇共归纳总结了50题,本篇是数组篇的最后一篇.其他三个篇章可参考: LeetCode刷题总结-数组篇(上),子数组问题(共17 ...
- LeetCode刷题总结-数组篇(中)
本文接着上一篇文章<LeetCode刷题总结-数组篇(上)>,继续讲第二个常考问题:矩阵问题. 矩阵也可以称为二维数组.在LeetCode相关习题中,作者总结发现主要考点有:矩阵元素的遍历 ...
- LeetCode刷题总结-树篇(上)
引子:刷题的过程可能是枯燥的,但程序员们的日常确不乏趣味.分享一则LeetCode上名为<打家劫舍 |||>题目的评论: 如有兴趣可以从此题为起点,去LeetCode开启刷题之 ...
- LeetCode刷题总结-树篇(下)
本文讲解有关树的习题中子树问题和新概念定义问题,也是有关树习题的最后一篇总结.前两篇请参考: LeetCode刷题总结-树篇(上) LeetCode刷题总结-树篇(中) 本文共收录9道题,7道中等题, ...
- LeetCode刷题总结-树篇(中)
本篇接着<LeetCode刷题总结-树篇(上)>,讲解有关树的类型相关考点的习题,本期共收录17道题,1道简单题,10道中等题,6道困难题. 在LeetCode题库中,考察到的不同种类的树 ...
- LeetCode刷题预备知识(二)
Python四大数据结构的属性及方法 在LeetCode刷题预备知识一中我们掌握了常见的内置函数,和四大数据结构的基本概念: 但只掌握这些还远远不够,我们还需了解四大数据结构的属性及方法才能更高效快速 ...
- LeetCode刷题专栏第一篇--思维导图&时间安排
昨天是元宵节,过完元宵节相当于这个年正式过完了.不知道大家有没有投入继续投入紧张的学习工作中.年前我想开一个Leetcode刷题专栏,于是发了一个投票想了解大家的需求征集意见.投票于2019年2月1日 ...
- LeetCode刷题总结-动态规划篇
本文总结LeetCode上有动态规划的算法题,推荐刷题总数为54道.具体考点分析如下图: 1.中心扩展法 题号:132. 分割回文串 II,难度困难 2.背包问题 题号:140. 单词拆分 II,难度 ...
随机推荐
- java 协程
协程是比线程更轻量级的程序处理单元,也可以说是运行在线程上的线程,由自己控制 1.适用于被阻塞的,且需要大量并发的场景. 2.不适用于,大量计算的多线程,遇到此种情况,更好实用线程去解决. 虽然Jav ...
- 常用es5和es6语法区别,以及三个点的用法
链接:https://www.jianshu.com/p/b4d48e9846e7 //三个点 链接:https://blog.csdn.net/qiladuo1207/article/details ...
- 吴裕雄--天生自然Numpy库学习笔记:NumPy 从数值范围创建数组
import numpy as np x = np.arange(5) print (x) import numpy as np # 设置了 dtype x = np.arange(5, dtype ...
- quartz定时任务cron表达式讲解及翻译成现实语言的插件的使用详解
cron表达式讲解 参见该网址: https://www.cnblogs.com/GarfieldTom/p/3746290.html cron表达式只有专业技术人员才看得懂,普通人不知道表达式是什么 ...
- android studio 导入主题设置,代码风格(附带eclipse 主题代码样式)
在这里我最想说的,android studio默认主题样式,太low.不适合长时间写代码,看代码颜色不好识别,相对于背景的代码样式,我都不想吐槽了.还是网上下载主题代码样式导入样式.在这里我推荐 Su ...
- 「CF1313C Skyscrapers」
题目大意 给出一个长度为 \(N\) 的序列 \(a\) 需要构造出一个长度为 \(N\) 的序列 \(h\) 使得 \(\forall i \in \{1,2,\ldots ,N\} h_i \le ...
- jemter-plugins-maven dependency -WIiki用法配置介绍
1.先介绍下jmeter 的maven中央仓库地址,有兴趣自己看下 https://mvnrepository.com/artifact/org.apache.jmeter 2.Wiki github ...
- linux动态监控dstat&&glances&&psutil&&bottle
安装dstat yum install dstat 安装glances yum install python-devel pip install glances 如果我们安装了 Bottle 这个 w ...
- 在一个不稳定的无效的ViewState净的应用问题。 Erratic Invalid Viewstate issue in a .NET application
这似乎是很多人都经历了同样的IE8的问题.似乎发生的是,不知何故,IE8(在IE8的渲染模式和IE7兼容模式)将失去4096个字节的HTML文档中该数据缺失导致此异常(通常你看到这一scriptres ...
- nginx 性能优化的概述及在CPU资源方面的处理
nginx的性能优化的概述 软件层面的提升硬件的使用率 增大CPU的利用率 增大内存的利用率 增大磁盘IO利用率 增大网络带宽利用率 提升硬件规格 网卡:万兆网卡.例如10G.25G.40G等 磁盘: ...