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,难度 ...
 
随机推荐
- linux--网络管理-ifconfig,route,netstat,ip,ss,dns,主机名网卡名修改bond
			
cat /etc/services 查看常见端口对应的服务 查一查某个端口号,是哪个进程在用 lsof -i :6010 49152-65535:动态端口或私有端口,客户端程序随机使用的端口 其 ...
 - 02使用GitHub远程仓库
			
一.远程库配置 由于本地的GIT仓库和GitHub仓库之间的传输是通过SSH加密的,所以需要以下配置: 1.创建SSH key 为什么GitHub需要SSHKey:根据key来授权,有哪些key可以往 ...
 - 实现纸牌游戏的随机抽牌洗牌过程(item系列几个内置方法的实例)
			
实现纸牌游戏的随机抽牌洗牌过程(item系列几个内置方法的实例) 1.namedtuple:命名元组,可以创建一个没有方法只有属性的类 from collections import namedtup ...
 - MySQL - 设置UTF-8编码
			
1. 在Windows上,安装时请选择UTF-8编码,以便正确地处理中文. 在Mac或Linux上,需要编辑MySQL的配置文件,把数据库默认的编码全部改为UTF-8.MySQL的配置文件默认存放在/ ...
 - 设计模式01 创建型模式 - 单例模式(Singleton Pattern)
			
参考 [1] 设计模式之:创建型设计模式(6种) | 博客园 [2] 单例模式的八种写法比较 | 博客园 单例模式(Singleton Pattern) 确保一个类有且仅有一个实例,并且为客户提供一 ...
 - 区间树Splay——[NOI2005]维护数列
			
无指针Splay超详细讲解 区间树这玩意真TM玄学. 学这东西你必须要拥有的 1.通过[模板]文艺平衡树(Splay),[模板]普通平衡树,GSS3 - Can you answer these qu ...
 - SSH框架整合,启动Tomcat报错:Unable to load configuration
			
报错信息: 严重: Dispatcher initialization failed Unable to load configuration. - bean - file:/E:/MIKEY/mik ...
 - windows安装ActiveMQ以及点对点以及发布订阅
			
一.MQ产品的分类 1.RabbitMQ 是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级 ...
 - spring boot 中容器 Jetty、Tomcat、Undertow
			
spring boot 中依赖tomcat <dependency> <groupId>org.springframework.boot</groupId> < ...
 - Java面向对象编程 -1
			
面向对象简介 C语言是面向过程开发的代表 C++ 或者Java 是面向对象的编程语言 所谓的面向过程指的是面对于一个问题的解决方案,更多的情况下是不会做出重用的设计思考的. 而面向对象的主要设计形式是 ...