什么是kmp算法呢?这是一个处理字符串的算法,用来判断给出的模式串p是否存在于文本串t中(p的长度小于t)。

在本文中,字符串储存在字符数组中,并且第一个字符放在下标为1的元素中。

那么如何理解kmp算法呢?首先要从最朴素的匹配算法说起。

我们判断p是否存在于t中,最原始的方法就是从头到尾一直遍历。定义变量i为文本串t中的下标,定义变量j为模式串p中的下标,然后i表示看文本串的前i个字符,j表示判断这前i个字符组成的子串中,长度为j的前后缀是否相等。

如果t[i] = p[j],则i与j同时后移一位,比较下一位是否相同,如果t[i] != p[j],则表示串t在i位置处“失配”,需要重新进行匹配,i保持不动,并且j必须返回到模式串p的开头,也就是相当于回退到1,然后再次进行循环。

如果t的长度为m,p的长度为n时,这样做的时间复杂度为O(m*n)

kmp就是在这种最原始匹配算法的基础之上的改进算法。

Kmp的改进之处在哪里呢? 上面这种复杂度最大的朴素方法中,有一个步骤,当“失配”时,我们的i不移动,但是j需要回到串p的开头,这样每一次失配,我们都需要再从模式串的开头重新开始匹配,相当于将j直接回退到1,然后再从1开始去试满足的最大的相同前后缀长度,多了好多次循环,聪明的科学家们想到的办法是:假设现在有这样一种情况:

在遍历到文本串t的的前i-1个字符组成的子串之后,我们已经确定了该子串中的长度为j-1的前后缀是相同的,那么现在在考虑下一个字符(第a个)时发生失配,也就是第a个字符不等于第b个,我们不想让b直接回退到1,而是回退到1和b之间的某个值,以减小复杂度。

我们先放下这个问题,思考另外一个问题:

如果要求一个字符串中相同的前缀和后缀的最长长度,怎么求呢?

和上面的kmp其实特别像,还是分治的思想:假设我们现在已经看到了字符串的前i-1个元素,并且在这i-1个元素的子串中,长度为j-1的后缀和前缀是一样的,表示匹配到了j-1的长度,那么我们就可以考虑第字符串中第i个元素是否和第j个元素相同了,如果相同就继续匹配下去,如果不同,j仍要回退,但是不能把j直接回退到1然后递增地去判断,这样复杂度太大。

因为我们现在知道的条件是前i-1个字符组成的子串的长度为j-1的前缀和后缀是相同的,那么如果这个长度为j-1的前缀的某个长度(假设为b)的前缀恰好等于其后缀的后缀,那么就表示我们可以对于长度i-1的子串,去考虑其长度为b+1的前后缀是否相等了,换言之,j不回退到1了,而是回退到b+1,因为在这种情况下我们是知道a-1的子串中,长度为c的前后缀也是相同的。

觉得上面这段话难理解的话,我来举一个实际一点的例子帮助理解。

上图中,假设现在是i=16,j=6,表明前面我们已经确定了前15个字符的子串中,相同的前缀和后缀的长度为5,现在新增了第16个字符,我们来考虑第6个和第16个字符是否相同.

如图,明显不同,所以我们这个时候就没办法说长度为16的子串中相同的前后缀长度为6了,那么这个前后缀的长度是多少呢?要靠j的移动来判断。

前面也说了,不能用朴素算法将j先回退到1,然后再自增j来判断。

我们发现在这个例子中,恰好长度为5的前缀abcab,在这个前缀内部,自己也是有前缀和后缀相等的情况的,前两个ab等于后两个ab,那么这样就是说长度为15的子串中,其实有“长度为5的前缀的长度为2的前缀“ = ”长度为5的后缀的长度为2 的后缀“ ,也就是说,长度为15的子串中,最长的相等的前后缀是5,但是我们根据长度为5的前缀自身也有相等的前后缀的情况(本质上是递归嘛)找到了长度为长度为15的子串中,次长的相等的前后缀长度是2,那么就将j回退到2的下一个位置,也就是3,再次判断,这时回退的时候是判断第三个字符和第16个字符,这个情况在前面没有判断过,说不定就相等了呢?要是不相等,就再次回退。用这种方法去回退,就不用将j回退到1,然后再自增,从而减小复杂度了。

这里的递归的思想其实很巧妙,我的j回退到哪呢?我已经知道了前i-1个字符中有长度为j-1 的前后缀是相同的,那么我去看是否有前缀的前缀等于后缀的后缀,也就是说,这i-1个字符中是否还有长度更小(小于j-1)的前后缀是相等的,方便我把j回退到那里去,因为前后缀这个性质,所以我要实现上面那个目的,其实只需要去看长度为j-1的子串中,是否有前后缀相等就可以了。

那我们怎么”看“呢?总不能每次都去求你这个前缀里面是否还有前后缀相等吧。我们可以用打表的方式,提前做出一个next数组,它的下标从1 开始,next[t] 里面放的就是 “前t-1个字符组成的子串中的相等的前缀和后缀的长度再+1 ”。加一是为了方便在j回退的时候,直接赋值给j。

简而言之,利用的就是”前缀有可能有某个长度的前缀等于后缀的相同长度的后缀“这个性质,来减少j的回退步数,从而减少复杂度。

那怎么求这个next数组呢?

直接上代码吧。

next数组只跟模式串p有关,并且它的第一个元素下标从1开始。而且第i个需要表示与前i-1个字符组成的字串相关的内容,所以它的长度应该比模式串p的长度plen多1。

next[i]中元素的含义:前i-1个字符组成的子串中的最长的相同前缀和后缀的长度再加一。

//这个next的各个元素的填充也是从头到尾的,即从next[1]到next[ plen+1 ]。
//ch数组就是我们的字符串
void getnext(char * ch, int plen, int *next){
next[1] = 0; //首先前0个元素是没有的,所以next[1]是0
int i=1,j=0; //i是子串长度,也就是指向这个子串中的后缀的最后一个元素,j表示相同的前缀和后缀的长度,即指向子串中的前缀的最后一个元素。
while(i<=plen){
if(j==0 || ch[i] == ch[j])
next[++i] = ++j;
else j = next[j];
}
}

上面主要控制while循环的是i,当i为p的长度再加一,即plen+1时则说明已经把next从1填到了plen+1。就可以退出while循环。那么在每一次的循环中,有两个分支:

第一个分支,如果当前ch[i] == ch[j] (也就是模式串p中的长度为i的子串前缀的最后一个元素==后缀的最后一个元素)时那么就表示当前我们可以进入下一个循环,即考虑p的长度为i+1的字串的长度为j+1的前缀与后缀了,而且正好我们上文分析到的,next[i] 里面放的是“前i-1个字符组成的子串中的相等的前缀和后缀的长度再加一 ”。在这里面数组下标和数组的值都其实隐含着“+1”,所以有了next[++i] = ++j;

另外一个分支是ch[i] != ch[j],表示模式串p中的长度为i的子串的长度为j的前缀和后缀是不匹配的(到了这最后一位不匹配了)。那么我们怎么办呢?回退咯,并且因为我们对于next[i]数组的巧妙定义,直接j = next[j]就完成了我们上面所说的回退。

那么还有一种特殊情况,是一开始的j=0,一开始next[2] 就将其置为1,因为第一个字符它只有一个,不构成子串,所以它的相等的前后缀长度为0,加上1就是1。所以我们在判断条件中加了一个“或j==0” ,就是为了在第一次循环中,将next[2]置为1;

其实这个生成next数组的过程,就是求字符串中相同的前后缀的长度的过程,两者其实本质上是一个东西,next数组生成完了,求字符串中相同的最长前后缀问题也就解决了,属于”你中有我,我中有你“。

解决了这个问题之后我们再来看kmp算法。

如果将模式串直接拼到文本串的前端,问题 ”判断给出的模式串p是否存在于文本串t中“ 也就转换成了 “判断拼接后的字符串中是否有一个子串的最大前后缀的长度与模式串的长度相等” 啦。

(如果你跟着我的引导,把前面的内容都看懂了,然后在看到这句话时有种醍醐灌顶的感觉,那么我会很开心)

kmp函数的具体代码如下,不用做真正的拼接操作,我们只需要将j指向模式串p,然后i指向文本串t就行了。

并且,生成的next数组,也只需要生成模式串p的长度就行了,生成多了没用啊。

//文本串text string,模式串pattern string
//i指向文本串,j指向模式串
int kmp(int *t,int tlen,int *p,int plen){
// int cnt = 0;
int *next = new int [plen+2]; //多预留了一个空间,然后我顺手再给它开了一位的空间
getnext(p,plen,next); //生成next数组
int j = 1;
for(int i=1;i<=tlen;i++){
while(j>1 && t[i] != p[j]){
j = next[j];
}
if(t[i] == p[j]) j++;
if(j == plen+1) return i-plen+1;
}
return -1;
}

i指向文本串t,然后i从1开始向后推,就像本文一开始讲的那样,t[i]与p[j]开始匹配,然后一旦失配,j就“回溯”,若一次回溯后仍失配,则继续回溯直到j变成1。那若是t[i]与p[j]相等,就j后移并且进入下一次循环,也就是i也后移,然后当j到了plen+1的时候,就表示模式串p已找到(为什么是plen+1呢?因为第当plen个匹配完成后,j会再加一,所以当模式串p匹配完成时j的值应该是plen+1的) 然后返回的值也就是模式串p在文本串t中出现的位置的首个字符的下标,所以就是i减去模式串p的长度plen再加一。

其实kmp的整个过程可以模仿next来写,像这样: (但是下面这个还没验证对不对)

int kmp(int *t,int tlen,int *p,int plen){
int *next = new int [plen+2];
getnext(p,plen,next);
int j = 0,i=1;
while(i<=tlen){
if(j == 0 || t[i] == p[j]){
i++;
j++;
}
else j = next[j];
if(j == plen+1) return i-plen;
}
return -1;
}

KMP算法的详细解释的更多相关文章

  1. KMP算法的详细解释及实现

    这是我自己学习算法时有关KMP的学习笔记,代码注释的十分的详细,分享给大家,希望对大家有所帮助 在介绍KMP算法之前, 先来介绍一下朴素模式匹配算法: 朴素模式匹配算法: 假设要从主串S=”goodg ...

  2. C语言实现数组快速排序(含对算法的详细解释)

    /* 说明: 代码参考过网上代码,但分析为个人原创,本贴重在说明快速排序算法的思想和运行过程. */ 代码部分: #include<stdio.h> #include<stdlib. ...

  3. KMP算法原理

    前几天在看数据结构与算法,里面提到过kmp算法,一个超级经典的字符串匹配算法.虽然网上有一大堆关于kmp算法的介绍文章,但是我看过之后还是“不明觉厉”.所以打算自己写写,大家一起学习吧. 一.关于KM ...

  4. 字符串模式匹配之KMP算法的next数组详解与C++实现

    相信来看next数组如何求解的童鞋已经对KMP算法是怎么回事有了一定的了解,这里就不再赘述,附上一个链接吧:https://www.cnblogs.com/c-cloud/p/3224788.html ...

  5. 【数据结构&算法】10-串基础&KMP算法源码

    目录 前言 串的定义 串的比较 串的抽象类型数据 串与线性表的比较 串的数据 串的存储结构 串的顺序存储结构 串的链式存储结构 朴素的模式匹配算法 模式匹配的定义 朴素的匹配方法(BRUTE FORC ...

  6. 22.1.22 并查集和KMP算法

    22.1.22 并查集和KMP算法 1.并查集结构 1)实现: 并查集有多种实现方式,例如向上指的图的方式,数组的方式等等.其根本思想就在于准确记录某个节点的根节点,这个这种记录就能够很快的实现并查集 ...

  7. KMP算法具体解释(转)

    作者:July. 出处:http://blog.csdn.net/v_JULY_v/. 引记 此前一天,一位MS的朋友邀我一起去与他讨论高速排序,红黑树,字典树,B树.后缀树,包含KMP算法,只有在解 ...

  8. KMP算法详细分解

    1. 引言 给定一个主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出现的位置,此即串的模式匹配问题. Knuth-Morris-Pratt 算法(简称 KMP)是解决这一问题的 ...

  9. (收藏)KMP算法的前缀next数组最通俗的解释

    我们在一个母字符串中查找一个子字符串有很多方法.KMP是一种最常见的改进算法,它可以在匹配过程中失配的情况下,有效地多往后面跳几个字符,加快匹配速度. 当然我们可以看到这个算法针对的是子串有对称属性, ...

随机推荐

  1. 04 jumpserver资产管理

    4.资产管理: (1)管理用户: 管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户, JumpServer 使用该用户来 `推送系统用户`.`获取资 ...

  2. 关于asp.net中Repeater控件的一些应用

    在Asp.net中,我是比较喜欢用Repeater这个控件,刚刚遇到的一个问题,怎么实现单击 <asp:LinkButton>,通过后台的单击事件获取同一行数据中的其他数据(对象). 1, ...

  3. Java核心反射机制

    Java核心反射机制: 基本反射: 反射是一种动态类的处理机制,通过Class类来实现反射机制: Class类的基本信息: Module java.base Package java.lang Cla ...

  4. COURSES 赤裸裸的二分匹配大水题

    COURSES 1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include ...

  5. Error in render: "TypeError: Cannot read property '' of undefined"

    描述 在用Vue的时候出现了一个令人窒息的错误 报错显示 "avatar" 未定义,但在postman中测试返回的数据确实有"avatar",可是为什么未找到? ...

  6. sonarqube 8.9版本配置项目访问权限

    soanrqube设置项目权限 admin->项目->要设置的项目 进行项目权限配置 选择权限 权限配置(公开,私有)如果是公司项目建议选择私有 根据项目团队成员的角色需求,进行勾选配置 ...

  7. Redis:银河麒麟arm服务器安装redis5.0.3,配置开机自启

    百度网盘下载地址 链接:https://pan.baidu.com/s/1f2ghL2-0brPt0IodjfqOqQ提取码:9al1    解压tar包 #解压tar包 tar -xvf arm-r ...

  8. Redis 高级面试题

    Redis 持久化机制 Redis 是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来 保证数据持久化.当 Redis 重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目 ...

  9. 使用 Cron4j 表达式 在 Solon 里开发定时任务

    cron4j 是一个轻量级的Java任务调度工具.cron4j-solon-plugin 是 solon 对 cron4j 的适配插件 添加 maven 引用 <dependency> & ...

  10. layim即时通讯实例各功能整合

    一.系统演示1.1 聊天窗体主界面演示 1.2 模拟两人在线聊天(点击图片查看演示视频) 1.3 在线演示> 在线演示,点击进入系统到这里,若是您想要的,接下来听我娓娓道来二.开发工具开发软件: ...