什么是KMP算法?

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,简称KMP算法。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。

KMP算法与BF(暴力破解法)的区别

KMP算法是建立在BF算法基础上的,KMP算法与BF算法的最大区别在于,BF算法只能一位一位匹配,而KMP算法能够省去不必要的匹配步骤,从而实现跳位的匹配(具体如何跳位,看下图演示)。

  • 主串S“ACDACBACDDC”,模式串T“ACBACD”,匹配相比之下,BF算法匹配成功需匹配4次,而KMP算法只需匹配2次就可以,那么为什么KMP算法的步子可以跨这么大?是因为KMP算法中的next数组存放了模式串T出现重复字符的信息,利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置,而避免模式串移动到无效位置(BF算法中2-3步均为无效位置)。

  • 通过算法比较我们发现KMP算法与BF算法结构类似,最大的区别在于else里的j=next[j];也就是 i 不回溯,j 回溯到 next[ j ] 的位置,而不是每次都从零开始。
  • 那么,看到这里,我们也就知道了KMP算法原理就是利用 j=next[j] 来回溯,来减少比较次数,或者是说跳过一些不必要的比较,也就是迈大步子。

next是啥?

  • next顾名思义,是下一个的意思,那么其作用是在回溯时指引指针j回溯的下一位置,也就是j=next[j];那么next[j]也就是j下一个要回溯到的位置。那么next数组就是用于存储每一位当发生不匹配时所对应的下一个要移动到的位置。言下之意next数组存的是位置(下标),通过带入j的值就可以获取到j的下一个回溯位置next[j]。
  • 举个例子:主串S“ACDACBACDDC”,模式串T“ACBACD”(如下图),当前j=5时,模式串为“ACBACD”此时我们j位next[j]=next[5]=2,也就是说“D”前面("D"的左边)最大匹配长度为2,我们观察模式串“ACBAC”也不难发现j指向的“D”的前面的“AC”(后缀)与模式串最前端的“AC”一致,那么在匹配的过程中(如下图所示),此时的指针j=5,所指的元素是“D”,然而“D”与主串中的"B"并不相等,那么,此时应该让j回溯,那么,回溯到什么位置呢?我们观察发现,“D”前面的AC先与主串匹配成功了,那么我们是不是只需要将,前缀的“AC”对齐,平移过来对齐,就可以了。

如何求next数组的值?

  • 在模式匹配的过程中,模式串T中的每一位都可能发生失配的现象,失配就要进行回溯,那next[j]是如何知道模式串T的每一位要发生回溯时的下一位置?
  • 这个问题还得从 “最大长度”Length数组说起。“最大长度”Length数组是一个用来存放一个字符串的最长前缀和最长后缀相同的长度。按每一位对应的下标将对应长度值存于数组Length中,如模式串T为“ababaca”,长度是7,所以Length[0],Length[1],Length[2],Length[3],Length[4],Length[5],Length[6]分别计算的是 a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。所以Length[]={0,0,1,2,3,0,1};
  • 那我们的到了这个长度之后有什么用呢?Length数组的下标与模式串T的下标是一致的(也就是下标 j ),也就是模式串T中每一位都可以对应其下标( j )在Length数组中找到该位当前前缀与后缀的最长匹配长度,那么也就知道了,在当前位置下(也就是后缀)是否与模式串的开头(也就是前缀)有相同的部分、相同的部分是几位。那么我们举个例子当j=4时,此时模式串为“ababa”,那么此时显然有 前缀:“aba”与 后缀:“aba”相匹配,长度是3。那么我们将j=4带入Length[ j ];结果是Length[4]=3。那么此时,当j前走一位,j=5时,如果需要回溯,我们就可以知道回溯位置是Length[4],也就是第四个位置。
  • 那么也就是说,有了Length数组,我们只需要知道下标就可以知道,当前的前后缀的最长匹配长度。而当j位发生不匹配,需要回溯时,我们要看前一位也就是j-1位的前后缀匹配情况也就是Length[j-1],可以知道要从j位回溯到Length[j-1]位。
  • 那么现在问题来了,当我们发现j所指向的“D”此时发生失配的情况时,我们得去前一位 j-1(也就是Length[j-1])找回溯位置,而并不能直接在Length[j]中找到,那么我们此时就引入next数组来代替Length数组来完成查找回溯位置这一工作。
  • 所以next数组只需要在原有Length的基础上,整体向后移一位,即可实现通过next[j]直接找到j当前所需要回溯到的位置。

为什么要让next[0]=-1

  • 未完待续

next数组有啥作用?

  • 这就好比后缀中的“AC”是探路人,告诉后方是否能匹配,下一次回溯时,前缀中的"AC"就可以直接上前来,事实上不只是后缀“AC”,任何一个后缀都可以是探路人这也就印证了为什么KMP算法的步子可以迈得大,因为有探路人探清楚了前面的情况,那后面的人就不需要小心翼翼的一步一步走,可以跨着走,跳着走。
  • 那么现在我们就知道了,后缀这个探路人,在发现前方能匹配的情况下,通过next数组找到后人(与后缀相匹配的前缀),让其上前来。如果next[ j ]=0的话,那说明没有后人(不存在与后缀相匹配的前缀),那只能重头开始。
  • next数组也正是KMP算法的精髓所在,充分记录并利用探路人(后缀)获取的信息(已匹配信息),让算法能绕过很多不必要的匹配项,少走很多弯路。

如何让程序求出next数组?

  • 从代码比较来看我们会发现求next数组的函数和KMP算法的结构惊人的相似,那么其实求next数组就是让模式串T的前缀和后缀相匹配,那么也就是一个自我匹配的过程,用前缀去匹配后缀。
  • 那么不同之处在于当匹配成功时,我们不但要把指针(下标)向后推一位,还同时需要对应当前下标记录下当前的最佳匹配长度,也就是next[j]=k;k表示的是模式串T的前k项后k项相匹配(如下图),那么k也正是当前的匹配长度,那么我们将这个长度记录在next数组的第j位也即是next[j]=k;那么我们也就完成了当第j位时,将前缀与后缀的最佳匹配长度k存储于next[j]中。
  • 那么此处你可能会有疑问了,求next数组不是得先求出Length吗?我们知道next数组是在Length数组上下标+1,那么我们注意看if中的代码,j在判断完T[j]==T[k];之后j++;k++;那么这个j此时已经向后推了一位,k也想后推了一位,那么此时的k的值也就是前k位的长度值(长度为k,数组下标是0~k-1,所以坐标得加1才是长度值),然后在执行next[j]=k;将k记录于next[j];那么这个先后顺序,就成功的将匹配信息记录于next数组中。
f(k==-1||T[j]==T[k]){j++;//注意执行顺序,先j++,k++,再next[j]=k;k++;next[j]=k;}
  • 当匹配失败时,我们可以看到KMP算法和求next数组的算法,处理方式是一样的,都是将当前指针回溯也就是k=next[k];

  • 很多人理解不了next函数中的k=next[k];其实在此处(如下图)相当于是将模式串T分割成两半,让前缀去和后缀相匹配,那么我们此时可以确定的是前k项(前缀)与后k项(后缀)成功匹配,那么此时做的是看一下在这个基础上能不能找到更长的匹配长度也就是前k+1项(新的前缀),和后k+1项(新的后缀)相匹配,如果可以,记录k+1于next数值,接着看再k+1的基础上能否有k+2的前后缀最匹配长度…

  • 那如果在前k项与后k项相同的情况下,第k+1项失配,也就是下图所视情况,T[k]≠T[j] ,那么我们的前缀指针k是不是要回溯,在此处使用BF算法的回溯思路,从零开始重新找前后缀匹配长度,固然可以,但是我们如果用kmp算法的回溯思路,是不是可以更快找到呢?

  • 如图示,根据next数组的存放值的含义我们通过next[k]可以知道,T[k]前是否有已匹配项,根据图示,我们可以发现蓝色部分是已匹配项,那么此时我们是不是只用取比较T[ next[k] ] 与T[ j ] 的值是否相等即可?那么也就是k=next[k];

  • 那么看到这里,你可能又会有疑问了,这个GetNext函数不是在求next数组吗?那我的next数组都还没有求完,你又怎么可以用next数组去求下一回溯位置呢?

  • 那么其实,我们回去看看设定,会发现k是永远比j小的,此时模式串已经处理到第j位,对应的next数组也对应的填到了next[j]的位置,那么也就是说next[0]~next[j]都是可用的,而0<k<j,那么k=next[k];自然是没问题的。

  • 说到这里我们也就豁然开朗了,其实BF算法,KMP算法,求next数组的算法,这三者结构都是十分相似的,因为本质上都是模式匹配,只不过后两者在回溯上有更聪明的回溯方法罢了

    升级版next数组

  • 我们发现这个算法其实还是存在漏洞,如下图的情况,第2步的情况是完全没有必要的,因为在第1步时已经判断过“B”≠"D",第2步又判断了一次“B”≠"D"。那有没有办法可以省掉,或者是跳过这个多余步骤呢?

  • 其实我们只需要加一个判断就可以了,当if(k==-1||T[j]==T[k])判断完前k项与后k项相匹配后,j++;k++;此时j和k都向后推了一位,分别指向下一位,那么这时我们不着急记录长度k于next数组,而是加一个判断,预判断一下下一位(这里的下一位是指j++;k++;之前的下一位)是否相等,也就是T[j]与T[k]是否相等,如果不相等,我们记录k的值于next数组;如果相等的话,就会出现上述多余重复匹配的现象,那么我们在这里通过连续的两次回溯,来跳过它,也就是k = next[next[k]];(next[j]=next[k];将next[k]的值存于next[j]也是一样的道理),回溯完之后不进行匹配判断就进行下一次回溯,即是跳过了他的匹配步骤。
void Nextval(char T[],int *next)
{int lenT=strlen(T);int k=-1;//前缀 int j=0;//后缀 next[0]=-1; while(j<lenT){if(k==-1||T[j]==T[k]){j++;k++;//较之前next数组求法,改动在下面4行if(T[j]!=T[k]){next[j]=k;//之前只有这一行}else{next[j]=next[k];/*为不能出现T[j] = T[ next[j ]],所以当出现时连续进行两次回溯,k = next[next[k]]  */ } }else {k=next[k];} }
}

其他问题

  1. 看到这里你可能会想到,kmp算法是通过记录模式串T中的重复数据信息,来达到跳步,那么如果模式串T中没有相匹配的前后缀呢?如:“ABCDEF”那kmp算法还能发挥作用吗?
  • 如果模式串T中完全没有相匹配的前后缀,那么 此时KMP算法的效能将发挥到极致!每次都可以实现最大量化的移动距离。
  1. 待补充…

完整版代码

#include <stdio.h>
#include <string.h>
#define maxsize 100 void Nextval(char T[],int *next)
{int lenT=strlen(T);int k=-1;//前缀 int j=0;//后缀 next[0]=-1; while(j<lenT){if(k==-1||T[j]==T[k]){j++;k++;//较之前next数组求法,改动在下面4行if(T[j]!=T[k]){next[j]=k;//之前只有这一行}else{next[j]=next[k];/*为不能出现T[j] = T[ next[j ]],所以当出现时连续进行两次回溯,k = next[next[k]]  */ } }else {k=next[k];} }
}int KMP(char S[],char T[])
{int i=0,j=0,lenS,lenT;lenS=strlen(S);lenT=strlen(T);int next[maxsize];Nextval(T,next);while(i<lenS&&j<lenT){if(j==-1||S[i]==T[j]){i++;j++;}else{ j=next[j];}}if(j==lenT){   printf("Match succeed!\n");return i-j;} else {printf("Match failed!\n");return -1;}
}int main()
{char S[maxsize],T[maxsize];int target;strcpy(S,"AAAAAAAAAAttract");strcpy(T,"Attract");target=KMP(S,T);printf("Position is %d\n",target);return 0;
}

聊聊模式匹配算法

  • 模式匹配算法除了我们提到的BF,KMP还有效率更高的BM,Sunday算法,KMP算法在BF一步一步走的基础上,实现了跨步跳步,将时间复杂度从O(m*n)降到了O(m+n),但是跨步的距离还是有局限性的,跨步的距离取决于模式串T的长度,模式串长度越长,那么可能实现的跨步距离也越大,那么Sunday算法其实就是打破了这一局限性,不需要取决于模式串得长度来查找匹配,从而也能将算法效能突破新的高度。

参考:

  1. 从头到尾彻底理解
  2. 详解KMP算法
  3. 部分文图来自于:懒猫老师
  4. 《数据结构 第二版》,严蔚敏 & 吴伟民编著;

C语言-模式匹配(KMP算法)相关推荐

  1. 字符串模式匹配KMP算法详解(Python语言)

    问题描述 主串为 ′ababcabcacbab′ ′ a b a b c a b c a c b a b ′ 'ababcabcacbab',模式串为 ′abcac′ ′ a b c a c ′ 'a ...

  2. 数据结构——模式匹配kmp算法

    暴力算法 //暴力算法 int index(SString S,SString T,int pos) {int i=pos,j=1;while(i<=S[0]&&j<=T[ ...

  3. C语言 实现 KMP算法

    已传入github,可下载,想测试对应函数,调用传参即可,记得注释掉之前的主函数 文件名为2022_1_19,因为是是那天写的 GitHub - frankRenlf/c_programshttps: ...

  4. 用C语言实现KMP算法,KMP算法 纯C实现

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 我自己写的KMP算法: int nt[256]; void get_next1(char* T, int next[], int tlen) { int  ...

  5. 每日算法练习——模式匹配KMP算法(看毛片算法?)

    知识补充: 在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置.此算法通过运用对这个词在不匹配时本身就包含足够的信息来 ...

  6. 用人类语言讲述KMP算法

    KMP算法的全名为Knuth-Morris-Pratt算法,可以快速检验字符串t是否为字符串s的子字符串. 另一种子字符串相关的算法是 Rabin-Karp 算法,之前一篇文章提到过:Rabin-Ka ...

  7. 算法笔记:简单的字符串模式匹配-KMP算法(与BF算法对比时间复杂度)

    简单的讲就是字符串不回溯. #include<stdio.h> #include<stdlib.h> #include<string.h>int countBF = ...

  8. 串的模式匹配(KMP算法)

    [问题描述] 串的模式匹配算法实现(KMP算法) [输入形式] 第一行输入主串s: 第二行输入模式串t: 第三行输入起始位置pos: [输出形式] 输出模式串t的next值(以空格分隔) 输出模式匹配 ...

  9. 《数据结构》实验报告四:串的模式匹配(BF算法、KMP算法)

    一.实验目的 1.了解串的基本概念. 2.掌握串的模式匹配算法的实现 . 二.实验预习 说明以下概念 1.模式匹配: 串的模式匹配就是子串的定位运算. 设有两个字符串 S 和 T ,S为主串(正文串) ...

  10. 0x15.基本数据结构 — 字符串 (KMP算法(含详细证明)和最小表示法)

    目录 一.KMP模式匹配 1.引理: 2.引理证明: 3.使用优化的算法计算nextnextnext数组: 4.luogu P3375 [模板]KMP字符串匹配 5.UVA1328 Period 6. ...

最新文章

  1. 继承ViewGroup类
  2. 小冰公司CEO李笛:AI不会江郎才尽,创造力只会持续向上攀升丨MEET2022
  3. java 接口中 常量_讨论:Java 接口当中的 “常量接口”
  4. vue打包后css路径_Vue打包后访问静态资源路径问题
  5. 怎么看android sdk 文档
  6. JUnit5 快速指南
  7. ThreadLocal的重要方法介绍
  8. centos 安装idea 非可视化_太厉害了!目前 Redis 可视化工具最全的横向评测
  9. php curlopt_postfields,PHP的CURLOPT_POSTFIELDS参数使用数组和字符串的区别 - CSDN博客
  10. shell不允许输入空字符_shell脚本编程之控制脚本
  11. 牛客网———二叉树遍历
  12. 一个命令让redis服务端所有信息无所遁形~(收藏吃灰系列)
  13. 如何删除github上的文件
  14. 从头开始学做 canvas 动画引擎
  15. ASP.NET WebAPI导入EXCEL数据
  16. python 企业微信接口_python连接企业微信发送消息
  17. 关于桌面文件,软件图标,带蓝底白问号的解决方法
  18. appium2.0+ 单点触控和多点触控新的解决方案
  19. authorized_key 不生效。
  20. R语言使用t.test函数进行t检验、使用配对的t检验(paired)检验组间不独立数据的差异是否有统计学意义

热门文章

  1. Angular I18n 五部曲
  2. sizeof是函数吗,具体使用方法
  3. 笔记本装 Win10 后的亮度调节和 WiFi 问题
  4. 在线查快递单号跟踪物流时效
  5. 哈密顿回路、链路、竞赛图
  6. PS基础(11.5天)
  7. 恰到好处的安慰:放过他人,放过自己
  8. 每周学一点 egret(4) EUI
  9. 大文件压缩包处理解决方案,防止内存溢出
  10. Python 自动整理 Excel 表格