数据结构与算法JavaScript (五) 串(经典KMP算法)
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算法)的更多相关文章
- hdu 3336:Count the string(数据结构,串,KMP算法)
Count the string Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) ...
- 数据结构与算法JavaScript (四) 串(BF)
串是由零个或多个字符组成的有限序列,又叫做字符串 串的逻辑结构和线性表很相似的,不同的是串针对是是字符集,所以在操作上与线性表还是有很大区别的.线性表更关注的是单个元素的操作CURD,串则是关注查找子 ...
- 经典KMP算法C++与Java实现代码
前言: KMP算法是一种字符串匹配算法,由Knuth,Morris和Pratt同时发现(简称KMP算法).KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的.比 ...
- 大话数据结构(十二)java程序——KMP算法及改进的KMP算法实现
1.朴素的模式匹配算法 朴素的模式匹配算法:就是对主串的每个字符作为子串开头,与要连接的字符串进行匹配.对主串做大循环,每个字符开头做T的长度的小循环,直到成功匹配或全部遍历完成为止. 又称BF算法 ...
- 第4章学习小结_串(BF&KMP算法)、数组(三元组)
这一章学习之后,我想对串这个部分写一下我的总结体会. 串也有顺序和链式两种存储结构,但大多采用顺序存储结构比较方便.字符串定义可以用字符数组比如:char c[10];也可以用C++中定义一个字符串s ...
- 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串
1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...
- poj 3461 - Oulipo 经典kmp算法问题
2017-08-13 19:31:47 writer:pprp 对kmp算法有了大概的了解以后,虽然还不够深入,但是已经可以写出来代码,(可以说是背会了) 所以这道题就作为一个模板,为大家使用吧. 题 ...
- 串和KMP算法
一.串 串是由零个或多个字符串组成的有限序列 (一).串的定义 定长顺序存储 特点:每个串变量分配一个固定长度的存储区,即定长数组 定义: #define MAXLEN 255 typedef str ...
- KMP算法详解 --- 彻头彻尾理解KMP算法
前言 之前对kmp算法虽然了解它的原理,即求出P0···Pi的最大相同前后缀长度k. 但是问题在于如何求出这个最大前后缀长度呢? 我觉得网上很多帖子都说的不是很清楚,总感觉没有把那层纸戳破, 后来翻看 ...
随机推荐
- sqlplus运行sql文件
当sql文件的数据比较多的时候,pl/sql运行比较慢,可以通过oracle的sqlplus进行导入: sqlplus user/password@tnsname@sqlfile.sql; 注意如果文 ...
- codeforces 360 C
C - NP-Hard Problem Description Recently, Pari and Arya did some research about NP-Hard problems and ...
- linux一句话轻松提权
linux命令: [b@fuckks~]$ printf "install uprobes /bin/sh" > exploit.conf; MODPROBE_OPTI** ...
- unison+inotify
hostnamectl --static set-hostname tCentos 1.下载文件到/usr/local/srcocamlunisoninotify-tools 2.安装inotify- ...
- js实现div居中
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...
- backbone新手填坑教程资源
backbone 入门第二版 http://www.kancloud.cn/kancloud/backbonejs-learning-note/49379 backbone 入门讲解 http://w ...
- linux时间同步ntp服务的安装与配置
1.首先安装NTP [root@localhost /]# yum install ntp -y 2.修改NTP配置文件,添加NTP服务器的网络位置 /etc/ntp.conf # For mo ...
- ASP.NET关于对excel数据导入到数据库
//*******************************C#(UI代码)********************************/ Web界面上放一个FileUpload 名称为:F ...
- 为什么要重写hashcode() 方法
Java中的集合(Collection)有两类,一类是List,再有一类是Set. 前者集合内的元素是有序的,元素可以重复:后者元素无序,但元素不可重复. 那么我们怎么判断两个元素是否重复呢? 这就是 ...
- thrift ssl 证书整理
一.生成证书,所需机器数必须 >= 2(一台生成服务端证书,一台生成客户端证书),以下服务器以A表示服务端.B表示客户端来举例,thrift版本为0.7.01.自签名的证书的生成和测试 1)生成 ...