
字符串匹配的KMP算法
阮一峰【字符串匹配的KMP算法】html
-
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html -
原文地址:https://github.com/Aaaaaaaty/blog/issues/42
字符串匹配是计算机的基本任务之一。c++
举例来讲,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另外一个字符串"ABCDABD"?git
许多算法能够完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最经常使用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。github

这种算法不太容易理解,网上有不少解释,但读起来都很费劲。直到读到Jake Boxer
的文章,我才真正理解这种算法。下面,我用本身的语言,试图写一篇比较好懂的KMP算法解释。web
字符串匹配
字符串匹配是计算机科学中最古老、研究最普遍的问题之一。一个字符串是一个定义在有限字母表∑上的字符序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一个字符串。字符串匹配问题就是在一个大的字符串T中搜索某个字符串P的全部出现位置。算法
kmp算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth
,J.H.Morris
和V.R.Pratt
同时发现,所以人们称它为克努特——莫里斯——普拉特操做(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽可能减小模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数自己包含了模式串的局部匹配信息。时间复杂度O(m+n)
。api
在js中字符串匹配咱们一般使用的是原生api,indexOf;其自己是c++实现的不在此次的讨论范围中。本次主要经过动画演示的方式展示朴素算法与kmp算法对比过程的异同从而试图理解kmp的基本思路。微信
PS:在以后的叙述中BBC ABCDAB ABCDABCDABDE为主串;ABCDABD为模式串编辑器
效果预览

【演示地址】(https://aaaaaaaty.github.io/blog/Algorithm/kmp/kmp.html)函数
上方为朴素算法即按位比较,下方为kmp算法实现的字符串比较方式。kmp能够经过较少的比较次数完成匹配。
基本思路
从上图的效果预览中能够看出使用朴素算法依次比较模式串须要移位13次,而使用kmp须要8次,故能够说kmp的思路是经过避免无效的移位,来快速移动到指定的地点。接下来咱们关注一下kmp是如何“跳着”移动的:

与朴素算法一致,在以前对于主串“BBC ”的匹配中模式串ABCBABD的第一个字符均与之不一样故向后移位到如今上图所示的位置。主串经过依次与模式串中的字符比较咱们能够看出,模式串的前6个字符与主串相同即ABCDAB;而这也就是kmp算法的关键。
根据已知信息计算下一次移位位置
咱们先从下图来看朴素算法与kmp中下一次移位的过程:

朴素算法雨打不动得向后移了一位。而kmp跳过了主串的BCD三个字符。从而进行了一次避免无心义的移位比较。那么它是怎么知道我此次要跳过三个而不是两个或者不跳呢?关键在于上一次已经匹配的部分ABCDAB
从已匹配部分发掘信息
咱们已知此时主串与模式串均有此相同的部分ABCDAB。那么如何从这共同部分中得到有用的信息?或者换个角度想一下:咱们能跳过部分位置的依据是什么?
第一次匹配失败时的情形以下:
BBC ABCDAB ABCDABCDABDE
ABCDABD
D != 空格 故失败
为了从已匹配部分提取信息。如今将主串作一下变形:
ABCDABXXXXXX... X多是任何字符
咱们如今只知道已匹配的部分,由于匹配已经失败了不会再去读取后面的字符,故用X代替。
那么咱们能跳过多少位置的问题就能够由下面的解得知答案:
//ABCDAB向后移动几位可能能匹配上?
ABCDABXXXXXX...
ABCDABD
答案天然是以下移动:
ABCDABXXXXXX...
ABCDABD
由于咱们不知道X表明什么,只能从已匹配的串来分析。
故咱们能跳过部分位置的依据是什么?
答:已匹配的模式串的前n位可否等于匹配部分的主串的后n位。而且n尽量大。
举个例子:
//第一次匹配失败时匹配到ABCDDDABC为共同部分
XXXABCDDDABCFXXX
ABCDDDABCE
//寻找模式串的最大前几位与主串匹配到的部分后几位相同,
//能够发现最可能是ABC部分相同,故能够略过DDD的匹配由于确定对不上
XXXABCDDDABCFXXX
ABCDDDABCE
如今kmp的基本思路已经很明显了,其就是经过经失败后得知的已匹配字段,来寻找主串尾部与模式串头部的相同最大匹配,若是有则能够跨过中间的部分,由于所谓“中间”的部分,也是有可能进入主串尾与模式串头的,没进去的缘由便是相对位置字符不一样,故最终在模式串移位时能够跳过。
部分匹配值
上面是用通俗的话来述说咱们如何根据已匹配的部分来决定下一次模式串移位的位置,你们应该已经大致知道kmp的思路了。如今来引出官方的说法。
以前叙述的在已匹配部分中查找主串头部与模式串尾部相同的部分的结果咱们能够用部分匹配值的说法来形容:
-
其中定义"前缀"和"后缀"。"前缀"指除了最后一个字符之外,一个字符串的所有头部组合;"后缀"指除了第一个字符之外,一个字符串的所有尾部组合。 -
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。
例如ABCDAB
-
前缀分别为A、AB、ABC、ABCD、ABCDA
-
后缀分别为B、AB、DAB、CDAB、BCDAB
很容易发现部分匹配值为2即AB的长度。从而结合以前的思路能够知道将模式串直接移位到主串AB对应的地方便可,中间的部分必定是不匹配的。移动几位呢?
移动位数 = 已匹配的字符数 - 对应的部分匹配值
答:匹配串长度 - 部分匹配值;本次例子中为6-2=4,模式串向右移动四位
代码实现
计算部分匹配表
function pmtArr(target) {
var pmtArr = []
target = target.split('')
for(var j = 0; j < target.length; j++) {
//获取模式串不一样长度下的部分匹配值
var pmt = target
var pmtNum = 0
for (var k = 0; k < j; k++) {
var head = pmt.slice(0, k + 1) //前缀
var foot = pmt.slice(j - k, j + 1) //后缀
if (head.join('') === foot.join('')) {
var num = head.length
if (num > pmtNum) pmtNum = num
}
}
pmtArr.push(j + 1 - pmtNum)
}
return pmtArr
}
kmp算法
function mapKMPStr(base, target) {
var isMatch = []
var pmt = pmtArr(target)
console.time('kmp')
var times = 0
for(var i = 0; i < base.length; i++) {
times++
var tempIndex = 0
for(var j = 0; j < target.length; j++) {
if(i + target.length <= base.length) {
if (target.charAt(j) === base.charAt(i + j)) {
isMatch.push(target.charAt(j))
} else {
if(!j) break //第一个就不匹配直接跳到下一个
var skip = pmt[j - 1]
tempIndex = i + skip - 1
break
}
}
}
var data = {
index: i,
matchArr: isMatch
}
callerKmp.push(data)
if(tempIndex) i = tempIndex
if(isMatch.length === target.length) {
console.timeEnd('kmp')
console.log('移位次数:', times)
return i
}
isMatch = []
}
console.timeEnd('kmp')
return -1
}
有了思路后总体实现并不复杂,只须要先经过模式串计算各长度的部分匹配值,在以后的与主串的匹配过程当中,每失败一次后若是有部分匹配值存在,咱们就能够经过部分匹配值查找到下一次应该移位的位置,省去没必要要的步骤。
因此在某些极端状况下,好比须要搜索的词若是内部彻底没有重复,算法就会退化成遍历,性能可能还不如传统算法,里面还涉及了比较的开销。
完整地址:
function pmtArr(target) {
var pmtArr = []
target = target.split('')
for (var j = 0; j < target.length; j++) {
//获取模式串不一样长度下的部分匹配值
var pmt = target
var pmtNum = 0
for (var k = 0; k < j; k++) {
var head = pmt.slice(0, k + 1) //前缀
var foot = pmt.slice(j - k, j + 1) //后缀
if (head.join('') === foot.join('')) {
var num = head.length
if (num > pmtNum) pmtNum = num
}
}
pmtArr.push(j + 1 - pmtNum)
}
return pmtArr
}
function mapKMPStr(base, target) {
var isMatch = []
var pmt = pmtArr(target)
console.time('kmp')
var times = 0
for (var i = 0; i < base.length; i++) {
times++
var tempIndex = 0
for (var j = 0; j < target.length; j++) {
if (i + target.length <= base.length) {
if (target.charAt(j) === base.charAt(i + j)) {
isMatch.push(target.charAt(j))
} else {
if (!j) break //第一个就不匹配直接跳到下一个
var skip = pmt[j - 1]
tempIndex = i + skip - 1
break
}
}
}
var data = {
index: i,
matchArr: isMatch
}
// callerKmp.push(data)
if (tempIndex) i = tempIndex
if (isMatch.length === target.length) {
console.timeEnd('kmp')
console.log('移位次数:', times)
return i
}
isMatch = []
}
console.timeEnd('kmp')
return -1;
}
let source = 'BBC ABCDAB ABCDABCDABDE',
match = 'ABCDABD';
let res = mapKMPStr(source, match);
console.log(res);
console.log(source.indexOf(match));

本文分享自微信公众号 - JavaScript忍者秘籍(js-obok)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。