之前说到,朴素的匹配,每趟比较,都要回溯主串的指针,费事。则 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回溯)

 图1   2

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

 3  4

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

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

  5  6

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

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

换一个角度思考;

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

 7   8

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

 9   10

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

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 数组原理和实现方案的更多相关文章

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

    相信来看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. KMP 算法中的 next 数组

    KMP 算法中对 next 数组的理解 next 数组的意义 此处 next[j] = k:则有 k 前面的浅蓝色区域和 j 前面的浅蓝色区域相同: next[j] 表示当位置 j 的字符串与主串不匹 ...

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. HUD--递增数

    递增数 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submiss ...

  2. Spring4 实例

    结构目录如下: 其中: dao层和entity层都属于hibernate的的管辖.entity层里面装的是每张表对应的持久化类.dao层里面装的是"底层操作数据库的行为",仅仅只是 ...

  3. 【转】POJ题目分类推荐 (很好很有层次感)

    OJ上的一些水题(可用来练手和增加自信) (poj3299,poj2159,poj2739,poj1083,poj2262,poj1503,poj3006,poj2255,poj3094)初期: 一. ...

  4. STL之priority_queue

    下面以 long long 型队列介绍: Q.empty() // 判断队列是否为空 返回ture表示空 返回false表示空 bool Q.top() // 返回顶端元素的值 元素还在队列里 lon ...

  5. SQL多表合并查询结果

    两表合并查询,并同时展示及分页SELECT a.* FROM ( ( SELECT punycode, `domain`, 'Success' AS state, add_time, AS refun ...

  6. 代码片段:处理HTTP请求的接口

    以下程序用来获取用户传递过来的信息. 1.头文件 /* * 这个是处理HTTP请求的接口头文件 */ #define KEY_VALUE_MAX 1024 /* 键值的最大长度 */ typedef ...

  7. canvas绘制简易时钟

    时钟绘制的非常简易,但该有的都有了. 效果图如下, <!DOCTYPE html> <html> <head lang="en"> <me ...

  8. 日常办公工具利器 notepad++

    日常办公工具利器 notepad++ 文本内容比较 Notepad++ https://notepad-plus-plus.org/ http://jingyan.baidu.com/article/ ...

  9. 《3D Math Primer for Graphics and Game Development》读书笔记2

    <3D Math Primer for Graphics and Game Development>读书笔记2 上一篇得到了"矩阵等价于变换后的基向量"这一结论. 本篇 ...

  10. <canvas>设置宽高遇到的问题

    在使用<canvas>元素时必须设置宽度和高度,指定可以绘画的区域大小.但是这里设置宽度和高度的时候有一个小问题. 样例代码: <!DOCTYPE html> <html ...