KMP算法和BM算法

KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同

前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右

后缀匹配是指:模式串和母串的的比较从右到左,模式串的移动从左到右。

通过上一章显而易见BF算法也是属于前缀的算法,不过就非常霸蛮的逐个匹配的效率自然不用提了O(mn),网上蛋疼的KMP是讲解很多,基本都是走的高大上路线看的你也是一头雾水,我试图用自己的理解用最接地气的方式描述


KMP

KMP也是一种优化版的前缀算法,之所以叫KMP就是Knuth、Morris、Pratt三个人名的缩写,对比下BF那么KMP的算法的优化点就在“每次往后移动的距离”它会动态的调整每次模式串的移动距离,BF是每次都+1,

KMP则不一定

如图BF与KMP前置算法的区别对比

我通过图对比我们发现:

在文本串T中搜索模式串P,在自然匹配第6个字母c的时候发现二等不一致了,那么BF的方法,就是把整个模式串P移动一位,KMP则是移动二位.

BF的匹配方法我们是知道的,但是KMP为什么会移动二位,而不是一位或者三位四位呢?

这就上一张图我们讲解下,模式串P在匹配了ababa的时候都是正确的,当到c的时候才是错误,那么KMP算法的想法是:ababa是正确的匹配完成的信息,我们能不能利用这个信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

那么问题来了, 我怎么知道要移动多少个位置?

这个偏移的算法KMP的作者们就给我们总结好了:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

偏移算法只跟子串有关系,没文本串没毛线关系,所以这里需要特别注意了

那么我们怎么理解子串中已匹配的字符数与对应的部分匹配值?


已匹配的字符:

T : abababaabab

p : ababacb

p中红色的标记就是已经匹配的字符,这个很好理解


部分匹配值:

这个就是核心的算法了,也是比较难于理解的

假如:

T:aaronaabbcc
P:aaronaac

我们可以观察这个文本如果我们在匹配c的时候出错,我们下一个移动的位置就上个的结构来讲,移动到那里最合理?

aaronaabbcc
aaronaac

那么就是说:在模式文本内部,某一段字符头尾都一样,那么自然过滤的时候可以跳过这一段内容了,这个思路也是合理的

知道了这个规律,那么给出来的部分匹配表算法如下:

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度”

我们看看aaronaac的如果是BF匹配的时候划分是这样的

BF的位移: a,aa,aar,aaro,aaron,aarona,aaronaa,aaronaac

那么KMP的划分呢?这里就要引入前缀与后缀了

我们先看看KMP部分匹配表的结果是这样的:

a   a  r  o  n  a  a  c
[, , , , , , , ]

肯定是一头雾水,不急我们分解下,前缀与后缀

匹配字符串 :“Aaron”
前缀:A,Aa, Aar ,Aaro
后缀:aron,ron,on,n

移动的位置:其实就是针对每一个已匹配的字符做前缀与后缀的对比是否相等,然后算出共有的长度


部分匹配表的分解

KMP中的匹配表的算法,其中p表示前缀,n表示后缀,r表示结果

a,         p=>, n=>  r = 

aa,        p=>[a],n=>[a] , r = a.length => 

aar,       p=>[a,aa], n=>[r,ar]  ,r = 

aaro,      p=>[a,aa,aar], n=>[o,ra,aro] ,r = 

aaron      p=>[a,aa,aar,aaro], n=>[n,on,ron,aron] ,r = 

aarona,    p=>[a,aa,aar,aaro,aaron], n=>[a,na,ona,rona,arona] ,r = a.lenght = 

aaronaa,   p=>[a,aa,aar,aaro,aaron,aarona], n=>[a,aa,naa,onaa,ronaa,aronaa] ,  r = Math.max(a.length,aa.length) = 

aaronaac   p=>[a,aa,aar,aaro,aaron,aarona], n=>[c,ac,aac,naac,onaac,ronaac]  r = 

类似BF算法一下,先分解每一次可能匹配的下标的位置先缓存起来,在匹配的时候通过这个《部分匹配表》来定位需要后移动的位数

所以最后aaronaac的匹配表的结果 0,1,0,0,0,1,2,0 就是这么来的

下面将会实现JS版的KMP,有2种

KMP实现(一):缓存匹配表的KMP

KMP实现(二):动态计算next的KMP


KMP实现(一)

匹配表

KMP算法中最重要的就是匹配表,如果不要匹配表那就是BF的实现,加上匹配表就是KMP了

匹配表决定了next下一个位移的计数

针对上面匹配表的规律,我们设计一个kmpGetStrPartMatchValue的方法

function kmpGetStrPartMatchValue(str) {
var prefix = [];
var suffix = [];
var partMatch = [];
for (var i = , j = str.length; i < j; i++) {
var newStr = str.substring(, i + );
if (newStr.length == ) {
partMatch[i] = ;
} else {
for (var k = ; k < i; k++) {
//前缀
prefix[k] = newStr.slice(, k + );
//后缀
suffix[k] = newStr.slice(-k - );
//如果相等就计算大小,并放入结果集中
if (prefix[k] == suffix[k]) {
partMatch[i] = prefix[k].length;
}
}
if (!partMatch[i]) {
partMatch[i] = ;
}
}
}
return partMatch;
}

完全按照KMP中的匹配表的算法的实现,通过str.substring(, i + ) 分解a->aa->aar->aaro->aaron->aarona->aaronaa-aaronaac

然后在每一个分解中通过前缀后缀算出共有元素的长度

回退算法

KMP也是前置算法,完全可以把BF那一套搬过来,唯一修改的地方就是BF回溯的时候直接是加1,KMP在回溯的时候我们就通过匹配表算出这个next值即可

//子循环
for (var j = ; j < searchLength; j++) {
//如果与主串匹配
if (searchStr.charAt(j) == sourceStr.charAt(i)) {
//如果是匹配完成
if (j == searchLength - ) {
result = i - j;
break;
} else {
//如果匹配到了,就继续循环,i++是用来增加主串的下标位
i++;
}
} else {
//在子串的匹配中i是被叠加了
if (j > && part[j - ] > ) {
i += (i - j - part[j - ]);
} else {
//移动一位
i = (i - j)
}
break;
}
}

红色标记的就是KMP的核心点 next的值  = 已匹配的字符数 - 对应的部分匹配值

完整的KMP算法

<!doctype html><div id="test2"><div><script type="text/javascript">

function kmpGetStrPartMatchValue(str) {
var prefix = [];
var suffix = [];
var partMatch = [];
for (var i = 0, j = str.length; i < j; i++) {
var newStr = str.substring(0, i + 1);
if (newStr.length == 1) {
partMatch[i] = 0;
} else {
for (var k = 0; k < i; k++) {
//取前缀
prefix[k] = newStr.slice(0, k + 1);
suffix[k] = newStr.slice(-k - 1);
if (prefix[k] == suffix[k]) {
partMatch[i] = prefix[k].length;
}
}
if (!partMatch[i]) {
partMatch[i] = 0;
}
}
}
return partMatch;
}

function KMP(sourceStr, searchStr) {
//生成匹配表
var part = kmpGetStrPartMatchValue(searchStr);
var sourceLength = sourceStr.length;
var searchLength = searchStr.length;
var result;
var i = 0;
var j = 0;

for (; i < sourceStr.length; i++) { //最外层循环,主串

//子循环
for (var j = 0; j < searchLength; j++) {
//如果与主串匹配
if (searchStr.charAt(j) == sourceStr.charAt(i)) {
//如果是匹配完成
if (j == searchLength - 1) {
result = i - j;
break;
} else {
//如果匹配到了,就继续循环,i++是用来增加主串的下标位
i++;
}
} else {
//在子串的匹配中i是被叠加了
if (j > 1 && part[j - 1] > 0) {
i += (i - j - part[j - 1]);
} else {
//移动一位
i = (i - j)
}
break;
}
}

if (result || result == 0) {
break;
}
}

if (result || result == 0) {
return result
} else {
return -1;
}
}

var s = "BBC ABCDAB ABCDABCDABDE";
var t = "ABCDABD";

show('indexOf',function() {
return s.indexOf(t)
})

show('KMP',function() {
return KMP(s,t)
})

function show(bf_name,fn) {
var myDate = +new Date()
var r = fn();
var div = document.createElement('div')
div.innerHTML = bf_name +'算法,搜索位置:' + r + ",耗时" + (+new Date() - myDate) + "ms";
document.getElementById("test2").appendChild(div);
}

</script></div></div>


KMP(二)

第一种kmp的算法很明显,是通过缓存查找匹配表也就是常见的空间换时间了。那么另一种就是时时查找的算法,通过传递一个具体的完成字符串,算出这个匹配值出来,原理都一样

生成缓存表的时候是整体全部算出来的,我们现在等于只要挑其中的一条就可以了,那么只要算法定位到当然的匹配即可

next算法

function next(str) {
var prefix = [];
var suffix = [];
var partMatch;
var i = str.length
var newStr = str.substring(, i + );
for (var k = ; k < i; k++) {
//取前缀
prefix[k] = newStr.slice(, k + );
suffix[k] = newStr.slice(-k - );
if (prefix[k] == suffix[k]) {
partMatch = prefix[k].length;
}
}
if (!partMatch) {
partMatch = ;
}
return partMatch;
}

其实跟匹配表是一样的,去掉了循环直接定位到当前已成功匹配的串了

完整的KMP.next算法

<!doctype html><div id="testnext"><div><script type="text/javascript">

function next(str) {
var prefix = [];
var suffix = [];
var partMatch;
var i = str.length
var newStr = str.substring(0, i + 1);
for (var k = 0; k < i; k++) {
//取前缀
prefix[k] = newStr.slice(0, k + 1);
suffix[k] = newStr.slice(-k - 1);
if (prefix[k] == suffix[k]) {
partMatch = prefix[k].length;
}
}
if (!partMatch) {
partMatch = 0;
}
return partMatch;
}

function KMP(sourceStr, searchStr) {
var sourceLength = sourceStr.length;
var searchLength = searchStr.length;
var result;
var i = 0;
var j = 0;

for (; i < sourceStr.length; i++) { //最外层循环,主串

//子循环
for (var j = 0; j < searchLength; j++) {
//如果与主串匹配
if (searchStr.charAt(j) == sourceStr.charAt(i)) {
//如果是匹配完成
if (j == searchLength - 1) {
result = i - j;
break;
} else {
//如果匹配到了,就继续循环,i++是用来增加主串的下标位
i++;
}
} else {
if (j > 1) {
i += i - next(searchStr.slice(0,j));
} else {
//移动一位
i = (i - j)
}
break;
}
}

if (result || result == 0) {
break;
}
}

if (result || result == 0) {
return result
} else {
return -1;
}
}

var s = "BBC ABCDAB ABCDABCDABDE";
var t = "ABCDAB";

show('indexOf',function() {
return s.indexOf(t)
})

show('KMP.next',function() {
return KMP(s,t)
})

function show(bf_name,fn) {
var myDate = +new Date()
var r = fn();
var div = document.createElement('div')
div.innerHTML = bf_name +'算法,搜索位置:' + r + ",耗时" + (+new Date() - myDate) + "ms";
document.getElementById("testnext").appendChild(div);
}

</script></div></div>

git代码下载: https://github.com/JsAaron/data_structure

数据结构与算法JavaScript (五) 串(经典KMP算法)的更多相关文章

  1. hdu 3336:Count the string(数据结构,串,KMP算法)

    Count the string Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) ...

  2. 数据结构与算法JavaScript (四) 串(BF)

    串是由零个或多个字符组成的有限序列,又叫做字符串 串的逻辑结构和线性表很相似的,不同的是串针对是是字符集,所以在操作上与线性表还是有很大区别的.线性表更关注的是单个元素的操作CURD,串则是关注查找子 ...

  3. 经典KMP算法C++与Java实现代码

    前言: KMP算法是一种字符串匹配算法,由Knuth,Morris和Pratt同时发现(简称KMP算法).KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的.比 ...

  4. 大话数据结构(十二)java程序——KMP算法及改进的KMP算法实现

    1.朴素的模式匹配算法 朴素的模式匹配算法:就是对主串的每个字符作为子串开头,与要连接的字符串进行匹配.对主串做大循环,每个字符开头做T的长度的小循环,直到成功匹配或全部遍历完成为止. 又称BF算法 ...

  5. 第4章学习小结_串(BF&KMP算法)、数组(三元组)

    这一章学习之后,我想对串这个部分写一下我的总结体会. 串也有顺序和链式两种存储结构,但大多采用顺序存储结构比较方便.字符串定义可以用字符数组比如:char c[10];也可以用C++中定义一个字符串s ...

  6. 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串

    1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...

  7. poj 3461 - Oulipo 经典kmp算法问题

    2017-08-13 19:31:47 writer:pprp 对kmp算法有了大概的了解以后,虽然还不够深入,但是已经可以写出来代码,(可以说是背会了) 所以这道题就作为一个模板,为大家使用吧. 题 ...

  8. 串和KMP算法

    一.串 串是由零个或多个字符串组成的有限序列 (一).串的定义 定长顺序存储 特点:每个串变量分配一个固定长度的存储区,即定长数组 定义: #define MAXLEN 255 typedef str ...

  9. KMP算法详解 --- 彻头彻尾理解KMP算法

    前言 之前对kmp算法虽然了解它的原理,即求出P0···Pi的最大相同前后缀长度k. 但是问题在于如何求出这个最大前后缀长度呢? 我觉得网上很多帖子都说的不是很清楚,总感觉没有把那层纸戳破, 后来翻看 ...

随机推荐

  1. 16-01-25---Servlet复习笔记(01)

    Servlet ServletAPI中有4个java包    javax.servlet 包含Servlet与Servlet容器之间契约的类和接口    javax.servlet.http 包含HT ...

  2. Hive 笔记

    DESCRIBE EXTENDED mydb.employees  DESCRIBE EXTENDED mydb.employees DESCRIBE EXTENDED mydb.employees ...

  3. unity3D脚本中,update ,fixupdate 和lateupdate的区别

    1.MonoBehaviour.Update 更新 当MonoBehaviour启用时,其Update在每一帧被调用. 2.MonoBehaviour.FixedUpdate 固定更新 当MonoBe ...

  4. Struts2

    为什么要用Struts2? 这里列举一些Servlet的缺点: 1.每写一个servlet在web.xml中都要做相应的配置.如果有多很servlet,会导致web.xml内容过于繁多. 2.这样的结 ...

  5. 多文档上传(upload multiple documents)功能不能使用怎么办?

    问题描述: 在SharePoint 2010的文档库里选择documents标签,然后选择upload document下拉菜单,你会发现upload multiple documents那个按钮是灰 ...

  6. The Myths about Transactions (ACID) and NoSQL

    There has been widespread characterization of one of the major distinctions between NoSQL and tradit ...

  7. Microsoft dotnetConf 2015 一些整理

    dotnetConf 2015 直播计划: 第一天 (10 个主讲) - .NET Product Teams 主讲人为 .NET 产品开发团队,.NET 产品主要包含 .NET Core.ASP.N ...

  8. Centos7下安装mono3.10.0

    mono 3.10.0 正式发布:性能进一步改进,以前已经写过一篇  Centos 7.0 安装Mono 3.4 和Jexus 5.6.下面我们在CentOS 7上通过源码安装Mono 3.10, 需 ...

  9. 轻量级ORM框架初探-Dapper与PetaPoco的基本使用

    一.EntityFramework EF是传统的ORM框架,也是一个比较重量级的ORM框架.这里仍然使用EF的原因在于为了突出轻量级ORM框架的性能,所谓有对比才有更优的选择. 1.1 准备一张数据库 ...

  10. [.net 面向对象程序设计进阶] (23) 团队开发利器(二)优秀的版本控制工具SVN(上)

    [.net 面向对象程序设计进阶] (23) 团队开发利器(二)优秀的版本控制工具SVN(上) 本篇导读: 上篇介绍了常用的代码管理工具VSS,看了一下评论,很多同学深恶痛绝,有的甚至因为公司使用VS ...