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,难度 ...
随机推荐
- VS中使用C的一些函数报错的问题
VS建议采用带_s的函数,如scanf_s.strcpy_s,但这些并不是标准C函数. 要想继续使用此函数,需要在源文件中添加以下指令就可以避免这个错误提示: #define _CRT_SECURE_ ...
- Python的 REPL 模式
REPL Read Eval Print Loop读取,执行,输出,循环 在REPL环境中,你输入一句话,他就读取,执行,输出一个结果,所以也称为 交互式提示模式这是python代码最简单的方式,也揭 ...
- left join 、right join 和inner join之间的区别
SQL的left join .right join 和inner join之间的区别 left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录 right join(右联接) ...
- 吴裕雄 python 神经网络——TensorFlow 数据集基本使用方法
import tempfile import tensorflow as tf input_data = [1, 2, 3, 5, 8] dataset = tf.data.Dataset.from_ ...
- wx小程序笔记
目录 p18 事件绑定1 p19 事件绑定2 btn p20+ view 相关,wxss,less,css 零基础玩转微信小程序[黑马程序员] https://www.bilibili.com/vid ...
- LPWAN
典型LPWA技术: 1 Sigfox技术由同名的法国Sigfox公司设计研发,成立于2010年,因为Sigfox网络由Sigfox公司为主导进行全球部署,这样能最大程度保证网络服务质量的统一性和稳定性 ...
- 浅谈Java三大特性之继承
前言 简单用伪代码去介绍一些 父类与子类的构造函数问题 others 前言 类的继承性是面向对象语言的基本特性,多态性前提是继承性.Java 支持继承性和多态性.——Java从小白到大牛 继承:三大特 ...
- office 格式定义
在做项目中,碰到如题的问题.比如要将居民的信息导出到excel中,居民的身份证号码因为长度过长(大于10位),excel会自动的将过长的数字串转换成 科学计数法.现在网上找到解决方案之一: (在数字串 ...
- 【网摘】监控 div 的内容变化
数据是动态加载而来,而当无数据时,提示一下暂无数据.而数据是可以动态在当前页面即时添加的,故在无数据时所做提示,需要隐藏,所以找了这个方法.成功在动态添加数据后,暂无数据的提示没有了. if($(&q ...
- 重新理解业务里程碑----HHR计划----以太一堂第二课
---- 理解业务背后的逻辑,抓住创业重点. 第一课:开始学习 1,FA : financial advisor.财务顾问. 2,本节课的目的:抓住创业的重点. 3,预热思考题: (1) 如果把你的整 ...