字符串模式匹配之KMP算法图解与 next 数组原理和实现方案

时间:2022-09-02 10:17:05

之前说到,朴素的匹配,每趟比较,都要回溯主串的指针,费事。则 KMP 就是对朴素匹配的一种改进。正好复习一下。

KMP 算法其改进思想在于:

每当一趟匹配过程中出现字符比较不相等时,不需要回溯主串的 i指针,而是利用已经得到的“部分匹配”的结果将模式子串向右“滑动”尽可能远的一段距离后,继续进行比较。如果 ok,那么主串的指示指针不回溯!算法的时间复杂度只和子串有关!很好。

KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的,很自然的,需要一个函数来存储匹配失败的信息。

先理解一个概念:前后缀字符串

比如"ababa"

前缀:a,ab,aba,abab,除了最后一个字符

后缀:a,ba,aba,baba,除了第一个 字符

比如"abcd"

前缀:a,ab,abc

后缀:d,cd,bcd

图解kmp 算法对朴素匹配改进的过程;

同样如图1,发生不匹配,朴素的做法是 j 到开头1出,i 到上次开始比较的位置的下一位2处(i回溯)

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案 图1   字符串模式匹配之KMP算法图解与 next 数组原理和实现方案2

但是发现一个问题,那就是在 图1的3处,不匹配的时候,前面的字符已知是匹配的,ab 是模式串里临时匹配的串,如果 i 回溯,那么等于是白白去比较,因为要把"搜索位置"移到已经比较过的位置,重比一遍。无用功,如果此时 i 不动,直接就可以减少无用的比较次数(所谓无用是说以最少的比较次数,找出完全的匹配串,尽量少做不匹配比较,通过之前的信息来计算和判断),如上图2,i 不动,j 回溯到1,匹配,ij继续走……一直都是匹配的,直到图4

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案 3  字符串模式匹配之KMP算法图解与 next 数组原理和实现方案4

那么不匹配了,临时的匹配串是 abca,如果 j 还是回到1,i 回溯到4(朴素的),我们发现1和4比较后不匹配,那么 i 继续右移,j 还是1,直到 i 到了6,才和 j=1处的 a 匹配,是不是之前的比较都是无用功?为什么不可以直接就和6比较呢?怎么解决呢?

发现一个规律:如果临时匹配串里,前后缀有重复,那么其实模式串的j,没必要每次都回到1,仔细思考是这样的。有一定规律可寻。

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案  5  字符串模式匹配之KMP算法图解与 next 数组原理和实现方案6

整个过程结束,最后结果和朴素一样,但减少了比较次数,改进了时间复杂度,让 o(t) 只和模式串t有关,因为主串s是给定的,且 i 不回溯,一直往前走!

到这里,就要思考,如何找出 j 回溯的逻辑。

换一个角度思考;

之前我们的做法是都观察 i j 的回溯!如图,现在我们移动模式串来观察。

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案 7   字符串模式匹配之KMP算法图解与 next 数组原理和实现方案8

图7不匹配, i 到2,j 到1,其实这相当于 T 右移,看图8。T 继续右移直到发生匹配,顺次比较,直到图9,红色标出,发现了不匹配,那么,按照之前的朴素做法,i 到4,j 还是1,相当于 T还是 右移一位!如下下图10。

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案 9   字符串模式匹配之KMP算法图解与 next 数组原理和实现方案10

比较之后,不匹配,T 继续友谊,直到T 1处移动到S6处,才发生匹配,之后继续顺次比较,ok!找出了匹配串。

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案11

那么通过观察来思考:每当临时匹配串(已知的)前后有重复的时候,那么只需 把模式串 T 直接移动到后缀刚刚开始有重复的位置(设移动距离为 d),i 不回溯。也就是j 直接反向的回溯距离 d,亮着等价的。为什么这样?

因为,在字符串搜索和匹配的时候,经常有前后重复的时候,前缀和后缀重复!如倒数第3图的已知的临时匹配串abca,前后缀 a 重复!此时没必要再用模式串的首位a去和S 的 b 比较了,直接和前后缀子串第一次有重复的位置比较(设子串右移距离 d),也就是 a 处。如果还那么顺次比较,是做无用功!此时思考如何实现这个逻辑,找到对应的j 回溯的距离 d。这个距离 d 就是前后重复的字符串的距离。

设置一个数组next来返回模式串的 j 应该回溯的位置

若令数组next[j]=k,则next[j]表明当模式中第j个字符与主串中相应字符失配时,在模式中需要重新和主串中该字符进行比较的字符的位置。

得到 KMP 模式匹配算法的实现思路(区别就是 next 函数)

那么问题来了,如何实现 next数组 ,生成对于的 j回溯的位置,从前面的讨论可知,next数组值仅取决于模式串本身,而与主串无关。

j 的回溯距离d 等于模式串中临时匹配串长(也就是j) 减去 相同的前后缀子串中的最大子串长度S(两个最大子串的距离),next 的值就是 j - j + S =S因此要计算next函数的返回值,就要找出前缀和后缀相同的最大子串的长度。

这个查找过程实际上仍然是模式匹配,只是匹配的模式与目标在这里是同一个串S。(这里遵守 c 的规定,数组都是默认下标0开始存储)

//计算 next 数组:根据待匹配的字符串,求出对应每一位的最大相同前后缀的长度
void computeNext(char *str, int next[])
{
} int strKMPCompare(char *strMain, char *strSub, int index, int next[])
{
int iMain = index;
int jSub = ;
int lenMain = getLength(strMain);
int lenSub = getLength(strSub); while ((iMain >= && iMain <= lenMain - ) && ((jSub >= && jSub <= lenSub - ))){
if (strMain[iMain] == strSub[jSub]) {
iMain++;
jSub++;
}else{
//主串的 i 不回溯!
//计算 next 数组
computeNext(strSub, &next[]);
jSub = next[jSub];
}
}
//如果匹配 ok,肯定子串先比完。
if (jSub > lenSub - ) {
return iMain - lenSub;//得到的就是匹配 ok 后,主串里第一个和模式串第一个字符匹配的字符的位置
}else{
return ;//匹配失败
}
}

那么最大的问题来了,如何实现 next数组?

next 数组递推的图解如下,已知,模式串的长度为 L ,j=0的时候,也就是第一个就不匹配, 规定next[0]=-1成立!其实在匹配过程中,若发生不匹配的情况,如果next[j]>=0,则目标串的指针i不变,将模式串的指针j移动到next[j]的位置继续进行匹配;若next[j]=-1,则将i右移1位,并将j置0,继续进行比较。

假设执行到某步,求出此时的 j (也就是第 j+1项发生不匹配)的 next[j] = k,k 是程序执行到这 步时,最长前后缀子串的长度。

如下是最长前缀子串:

P(0)  P(1)  ……  P(K-1)

如下是模式串:

P(0)  P(1)  ……  P(K-1)  P(K)  P(K+1)  ……  P(J-1)  P(J)  ……  P(L - 1) 

因为前后缀子串长度相等为 k,那么得到:

P(0)  P(1)  ……  P(K-1)   =    P(J - K)  ……  P(J-1)

其中P(J - K)  ……  P(J-1)是最长后缀子串,长度是 k,注意满足j-1-j+k=k-1,直观的看就是:

P(0)  P(1)  ……  P(K-1)

||    ||  ||    ||

P(J - K)  ……     P(J-1)

如果,对于模式串,继续求 j+1的 next[J+1],如下:

P(0)  P(1)  ……  P(K-1)  P(K)  P(K+1)  ……   P(J - K)     ……    P(J-1)  P(J)

如果 p(k)=p(j),那么有:

P(0)  P(1)  ……  P(K-1)      P(K)

||    ||  ||    ||     ||

P(J - K)  ……     P(J-1)   P(J)

此时,next[j+1]=k+1=next[j]+1

如果,p(k)!=p(j),那么需要从新检查,不过不论怎样计算,最后还是能得到一个正确的结果,即:最大重复前后缀字符串的前缀子字符串开头一定是 p(0),后缀字符串结尾一定是 p(j)。 但是在得到这个正确结果之前,我们总会经历相思的步骤,因为是找前后缀相等的子字符串,那么一般情况下总会经历这样的过程:前缀子字符串开头一定是 p(0),而已经知道后缀字符串的倒数第二项等于 p(k-1),那么前缀字符串的倒数第二项也应该等于 p(k-1),现在设为 p(m-1)来表示,又我们假设的是求出了next[j]=k,

P(0)  P(1)  ……     p(m-1)   ……  P(K-1)  P(K)  P(K+1)  ……   P(J - K)     ……    P(J-1)

那么 j 之前的 每一位对应的 next 也都求出了,自然得到next[k-1]=m,此时前缀的结尾 p(m)要么满足和 p(j)相等,要么不相等,如相等还是m++处理,不等还是如上的过程,这样递归下去直到成功找到。

本质上则可以把其看做模式匹配的问题,即匹配失败的时候,k值如何移动

next[k-1]=m,next[m-1]=n,……,next[0]=0  =》

next[next[next[k-1]-1]-1……]=next[j+1] 且 next[j]=k

k++

实现代码

 //计算 next 数组:根据待匹配的字符串,求出对应每一位的最大相同前后缀的长度
void computeNext(char *str, int next[])
{
int k = -;//记录最长前后缀字符串的长
int i = ;//
//next【0】=-1,肯定要遍历模式串
next[] = -;
//模式串长度
int len = getLength(str);
//第一岑循环控制计算到模式串的每一位
while (i < len) {
//第二层循环,控制每次计算到某位置时,递归求解 k 的过程
//next[next[next[k-1]-1]-1……]=next[j+1] 且 next[j]=k
while (k > && str[i] != str[k]) {
k = next[k - ];//递归,逐层深入,调用
}
//i 变化,如果 stri=strk,退出递归循环,直接+1求解,否则一直递归到为k<=0退出
if (k == - || str[i] == str[k]) {
k++;
}
//所有情况都处理完毕,存储结果
next[i] = k;
i++;
}
}

需要注意几个点,因为规定了,next[0]=-1,那么 k 最小应该为-1,且若 next 返回-1的话,说明第一个不匹配,那么这里注意下,把 i++,j=0设置!在 kmp 函数里注意。

            jSub = next[jSub];
if (jSub == - ) {
jSub = ;
iMain++;
}

在next 函数的 if 语句中,当 strk==stri, k++,但是还要注意,k==-1的情况!也要k++,否则紧跟 下面的 赋值,会把-1付给next【i】

完整代码

 //计算 next 数组:根据待匹配的字符串,求出对应每一位的最大相同前后缀的长度
void computeNext(char *str, int next[])
{
int k = -;//记录最长前后缀字符串的长
int i = ;//
//next【0】=-1,肯定要遍历模式串
next[] = -;
//模式串长度
int len = getLength(str);
//第一岑循环控制计算到模式串的每一位
while (i < len) {
//第二层循环,控制每次计算到某位置时,递归求解 k 的过程
//next[next[next[k-1]-1]-1……]=next[j+1] 且 next[j]=k
while (k > && str[i] != str[k]) {
k = next[k - ];//递归,逐层深入,调用
}
//i 变化,如果 stri=strk,退出递归循环,直接+1求解,否则一直递归到为k<=0退出
if (k == - || str[i] == str[k]) {
k++;
}
//所有情况都处理完毕,存储结果
next[i] = k;
i++;
}
} int strKMPCompare(char *strMain, char *strSub, int index, int next[])
{
int iMain = index;
int jSub = ;
int lenMain = getLength(strMain);
int lenSub = getLength(strSub); while ((iMain >= && iMain <= lenMain - ) && ((jSub >= && jSub <= lenSub - ))){
if (strMain[iMain] == strSub[jSub]) {
iMain++;
jSub++;
}else{
//主串的 i 不回溯!
//计算 next 数组
computeNext(strSub, &next[]);
jSub = next[jSub];
if (jSub == - ) {
jSub = ;
iMain++;
}
}
}
//如果匹配 ok,肯定子串先比完。
if (jSub > lenSub - ) {
return iMain - lenSub;//得到的就是匹配 ok 后,主串里第一个和模式串第一个字符匹配的字符的位置
}else{
return ;//匹配失败
}
} int main(int argc, const char * argv[]) {
char *str1 = "avcbababcc";
char *str2 = "bab";
int next[] = {}; int i = strKMPCompare(str1, str2, , &next[]); for (int i = ; i < ; i++) {
printf("%d \n", next[i]);
} printf("%d\n", i); return ;
}

-1 

3

Program ended with exit code: 0

补充:next数组的直接求法,上面说的是递归思想,递推关系。其实也可以直接求解。

思考:关键是如何比较,肯定需要知道模式串长 len,还需要头部一个标记,尾部一个标记。直接想到循环去遍历两个头,头++,尾部++,这是求的模式串的某个位置的 next 数组值。故还需要一个外循环控制依次遍历模式串。在这个外循环里,每趟循环,调用一次比较函数,求出 next[x]=?,通过 break 退出内部循环,实现一次调用。

 //i 是前缀长,后缀长=i,故后缀第一个下标是 j-i ,通过比较函数内部的约束来调整参数
bool equal(char *str, int i, int j)
{
int head = ;//假设是 0 1 …… i-1 和 j-i …… j-1,依靠,前后缀相等的特征!
int tailHead = j - i;//参数 i 是前缀字符串的尾部下标,而前缀字符串长度就是 i,等于后缀字符串长度,故用末位 j-i 就是后缀字符串的开头 for (; head <= i - && tailHead <= j -; head++, tailHead++) {
if (str[head] == str[tailHead]) {
return true;
}
} return false;
} void getNext(char *str, int next[])
{
int nextI = ;
int head = ; for (; nextI < getLength(str) ; nextI++) {
//规定0是-1
if ( == nextI) {
next[nextI] = -;
}else if (nextI == ){
next[nextI] = ;
}
else{
//进行比较,需要一个循环控制 尾部字符串的处理
for (head = nextI - ; head > ; head--) {
//head 是头字符串的终点,nextI 是临时模式串的终点
if (equal(str, head, nextI)) {
next[nextI] = head;
break;
}
}
//别忘了要习惯性的思考边界的特例,如果 head--为0了,自动退出内循环,要处理下,其实是说明字符串没有任何重复字符出现的情况。
if ( == head) {
next[nextI] = ;
}
}
}
}

改进的 next数组 算法

在看一个例子:

主串:’aaabaaab’;

子串: aaaa3b

当子串中的第四个字符’a’与主串中的第四个字符’b’失配后,按照之前的 next 数组求法,的 next[3]=2如果用子串中的第3个字符’a’继续与主串中的第四个字符’b’比较,将是做无用功。以此类推。说明:kmp 算法里的关键next函数仍有改进的地方。改进next函数:当子串中的第j个字符与主串中的第i个字符失配后,如果有next[j]=k;且在子串中有pj=pk;那么pk肯定也与主串中的第i个字符不等,所以,直接让

next[j]=next[k];

直到他们不等或next[j] = -1为止。这个很好理解。当子串中的第四个字符’a’与主串中的第四个字符’b’失配后,next[3]=next[2]=1,则next[3]=next[1] = next[0] = -1。这样效率又高了些。

        //所有情况都处理完毕,存储结果
if(str[i] == str[k])
{
//本质还是把一个 k 赋值给了 next【i】,然后回到 while 处从新循环
next[i] = next[k];
}else{
next[i] = k;
}

KMP算法只有在主串和模式串"部分匹配"时才会才会体现出他的优势,否则两者差异不大 ,KMP算法应用(可借鉴和参考的思想) 首先next数组代表每个字符在匹配失败时,回溯的位置,我们可以通过next数组找到每个字符的前缀和后缀

欢迎关注

dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案

字符串模式匹配之KMP算法图解与 next 数组原理和实现方案的更多相关文章

  1. 字符串模式匹配之KMP算法的next数组详解与C&plus;&plus;实现

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

  2. 串的模式匹配和KMP算法

    在对字符串的操作中,我们经常要用到子串的查找功能,我们称子串为模式串,模式串在主串中的查找过程我们成为模式匹配,KMP算法就是一个高效的模式匹配算法.KMP算法是蛮力算法的一种改进,下面我们先来介绍蛮 ...

  3. 字符串匹配算法之 kmp算法 (python版)

    字符串匹配算法之 kmp算法 (python版) 1.什么是KMP算法 KMP是三位大牛:D.E.Knuth.J.H.MorriT和V.R.Pratt同时发现的.其中第一位就是<计算机程序设计艺 ...

  4. Java数据结构之字符串模式匹配算法---KMP算法2

    直接接上篇上代码: //KMP算法 public class KMP { // 获取next数组的方法,根据给定的字符串求 public static int[] getNext(String sub ...

  5. Java数据结构之字符串模式匹配算法---KMP算法

    本文主要的思路都是参考http://kb.cnblogs.com/page/176818/ 如有冒犯请告知,多谢. 一.KMP算法 KMP算法可以在O(n+m)的时间数量级上完成串的模式匹配操作,其基 ...

  6. 【模式匹配】KMP算法的来龙去脉

    1. 引言 字符串匹配是极为常见的一种模式匹配.简单地说,就是判断主串\(T\)中是否出现该模式串\(P\),即\(P\)为\(T\)的子串.特别地,定义主串为\(T[0 \dots n-1]\),模 ...

  7. 串的模式匹配,KMP算法

    串的模式匹配 现考虑一个常用操作,在字符串s(我们称为主串)中的第pos开始处往后查找,看在主串s中有没有和子串p相匹配的的,如果有,则返回字串p第一次出现的位置. 暴力求解 int Index(ch ...

  8. 字符串匹配算法之————KMP算法

    上一篇中讲到暴力法字符串匹配算法,但是暴力法明显存在这样一个问题:一次只移动一个字符.但实际上,针对不同的匹配情况,每次移动的间隔可以更大,没有必要每次只是移动一位: 关于KMP算法的描述,推荐一篇博 ...

  9. 扩展的KMP算法图解

    扩展的KMP算法,可以在Ο(n + m)的时间复杂度内计算出模板串与文本串的每一个后缀的最长公共前缀,即LCP(T[i:n],P). KMP算法所解决的单模板字符串匹配问题,求得的匹配点是LCP = ...

随机推荐

  1. SQL Server 批量主分区备份(Multiple Jobs)

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 案例分析(Case) 方案一(Solution One) 方案二(Solution Two) ...

  2. iOS8定位问题解决方案

    原文  http://blog.csdn.net/nextstudio/article/details/40050095 1.修改info 新增Key: NSLocationAlwaysUsageDe ...

  3. jQuery中 end&lpar;&rpar;&semi; 的用法

    jQuery中的end()方法的意思 选取某个元素,查找选取其子元素,然后再回过来选取这个元素.用例子说明了一下: 比如HTML代码: <p><span>Hello</s ...

  4. 开始学习IOS

    最好的学习方式就是动手. 对于有编程经验的程序员来说,学习另外一门技术最好的方式就是coding,虽然基础知识和IDE都不熟悉,但是在写代码的过程中,不断的解决问题,不断查找资料,不断感悟代码,一切都 ...

  5. Common Converters in WPF&sol;Silverlight

    using System; using System.ComponentModel; using System.Globalization; using System.Windows.Data; na ...

  6. c&num; 事件和EventManager

    事件 基本用法 关键字event,声明格式为: public event <委托类型> <事件对象> 事件的处理方法:适用于该委托的方法 数据的触发: 绑定同类事件,绑定时,可 ...

  7. dbca建库--linux上使用vnc图形化安装oracle10g版本

    选择创建数据库 配不配置em,自己决定,我们选择配置 配置下面账户密码,在项目中,下面账户肯定是不相同的,我们这里输入相同的,密码为oracle10g 选择文件系统存放(asm和字符设备暂时不能存放) ...

  8. XCache 一种快速可靠的PHP操作码缓存

    1,错误报告开启 错误报告是在PHP中一个非常有用的功能,应同时在开发阶段启用. 这可以帮助我们确定我们的代码中的问题. 最常用的功能是“E_ALL”,这有助于我们发现所有的警告和严重错误. 必须指出 ...

  9. Arduino I2C &plus; DS1307实时时钟

    主要特性 DS1307是Maxim的串行.I2C实时时钟芯片.主要特性有: 工作电压:主电源电压4.5~5.5V,电池电压2.0~3.5V 功耗:电池供电.备份模式时<500nA 接口:I2C, ...

  10. 2754&colon; C&plus;&plus;习题-快速排序

    2754: C++习题-快速排序 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 921  Solved: 406[Submit][Status][Web ...