一.问题重述

现有字符串S1,求S1中与字符串S2完全匹配的部分,例如:

S1 = "ababaababc"

S2 = "ababc"

那么得到匹配的结果是5(S1中的"ababc"的a的位置),当然如果S1中有多个S2也没关系,能找到第一个就能找到第二个。。

-------

最容易想到的方法自然是双重循环按位比对(BF算法),但在最坏的情况下BF算法的时间复杂度达到了m * n,这在实际应用中是不可接受的,于是某3个人想出来了KMP算法(KMP是三个人的名字。。)

二.KMP算法具体过程

(先不要着急想看KMP算法的官方定义,很难看懂的,所以这里干脆不给定义了。。)

认真审视一下S2 = "ababc",很重要的一点是:开头出现了ab,中间部分又出现了ab(先记住这个细节)

现在假定一个时刻:

此时abab部分匹配成功,S1的位置指针指向了a,当前正在做的事情是a与c比对,结果是匹配失败

  • 如果是BF算法,那么下一步是S1的位置指针回溯到第二位的b,S2的位置指针重置到开头的a,然后b与a比对,然后。。
  • 如果是KMP算法,那么下一步是S1位置指针不变(指向a),然后查跳转表(next表)得到跳转值2,再把S2的指针移动到第二个a上,接下来比对a与a,然后。。

KMP的步骤看不明白没关系,毕竟我们还没有解释跳转表(next表)是什么,不过在这里我们只用关注结果就好了:

  • BF算法中S1的指针向左移动了(回溯)
  • KMP算法中S1的指针没有往回走(无回溯)

因为KMP算法不存在回溯过程,所以节省了不少时间(S1的指针只需要从头走到尾就可以了)

-------

再看看KMP算法的核心——跳转

为什么可以跳转?注意观察一下S2 = "ababc",这个串的特点是:开头出现了ab,中间部分又出现了ab(还记得这个细节吗),详细解释一下:

如果S2末尾的c与S1的第k位匹配失败,我们可以推断出两个信息:

  1. 本趟匹配失败了(c匹配失败意味着S1中的的某一部分不能匹配S2)
  2. S1中第k位前的4位一定是abab(只有S2中的abab与S1中的某一部分匹配成功后才可能出现S2的c与S1第k位的比对)

如果我们忽略第2点,那么下一步是S1指针回溯,也就是BF算法将要做的;如果我们抓住第2点,再加上S2串的特点:

  • 开头出现了ab,中间部分又出现了ab(再重申一遍)

就可以得到KMP算法(相当于S1中第k位之前的ab已经和S2开头的ab匹配了,所以可以直接跳转到S1的第k位与S2的第3位a比对)

好像有点明白了,那么这个跳转值2是怎么得到的?多举一些例子:

  • S2 = "aba"最后一位的跳转值为0
  • S2 = "abaa"最后一位的跳转值为1
  • S2 = "abcabc"最后一位的跳转值为2

发现什么了吗?没错,我们求S2中第x位(此处的x是从0开始算的)的跳转值的过程是这样的:

  1. 如果x = 0,那么S2[x]的跳转值为-1(首元的跳转值为-1)
  2. 如果x = 1,那么S2[x]的跳转值为0(第二个元素的跳转值为0)
  3. 如果S2[x - 1] = S2[0],那么S2[x]的跳转值为1
  4. 如果不满足第3条,那么跳转值为0
  5. 如果S2[x - 1] = S2[0] 并且 S2[x - 2] = S2[1],那么S2[x]的跳转值为2
  6. 如果S2[x - 1] = S2[0] 并且 S2[x - 2] = S2[1] 并且 S2[x - 3] = S2[2],那么S2[x]的跳转值为3
  7. 。。。

人用眼睛按照上面的方法“目测”跳转值是最快的,但同样的过程用于计算机的话就不那么容易实现了,计算机有计算机喜欢的方式,一篇简短的博文解释了这种方式

简单的说就是——“递推”,即由已知的首项为-1,第二项为0,递推得到后面所有项,详细过程不再赘述,上面的链接博文写的非常清楚

-------

下面可以得出KMP算法的具体过程了:

  1. 根据模式串S2构造跳转表(next表)
  2. 从S1头开始比对,查next表得跳转值,S1指针向右移动继续比对,直至S1末尾

说白了又是在用空间换时间(next表占用的空间),当然,在此算法中next表是长度等于模式串S2长度的线性表而已,并不需要太多空间

三.实现next函数

next函数用来构造next表(跳转表),如何构造next表才是KMP算法的关键(如果不关注KMP的证明过程的话。。)

我们可以按照链接博文的方式自己实现next函数:

  1. //参考例子:http://blog.sina.com.cn/s/blog_96ea9c6f01016l6r.html
  2.  
  3. #include<stdio.h>
  4.  
  5. void getNext(char a[], int n, int next[]){
  6. int i, j;
  7.  
  8. next[0] = -1;//首元跳转值为-1
  9. next[1] = 0;//第二个元素跳转值为0
  10.  
  11. for(i = 2; i < n; i++){
  12. j = i - 1;
  13.  
  14. //递推得到next表中剩余值
  15. while(j != -1){
  16. if(a[i - 1] == a[next[j]]){
  17. next[i] = next[j] + 1;
  18. break;
  19. }
  20. else{
  21. j = next[j];
  22. }
  23. }
  24. }
  25. }
  26.  
  27. main(){
  28. char a[] = "abaabcac";//模式串S2
  29. int next[8] = {0};//跳转表,初始化为全0
  30. int i;
  31.  
  32. //构造next表
  33. getNext(a, 8, next);
  34. //输出next表
  35. for(i = 0; i < 8; i++){
  36. printf("%d ", next[i]);
  37. }
  38. printf("\n");
  39. }

当然,上面的getNext函数好像仍然不够高效(双重循环..),但优点是易于理解。下面看看书上给的next函数:

  1. #include<stdio.h>
  2.  
  3. void getNext(char a[], int n, int next[]){
  4. int i, j;
  5. i = 0;
  6. next[0] = -1;//首元跳转值为-1
  7. j = -1;
  8.  
  9. //递推得到next表中剩余值
  10. while(i < n){
  11. if(j == -1 || a[i] == a[j]){
  12. ++i;
  13. ++j;
  14. next[i] = j;
  15. }
  16. else{
  17. j = next[j];
  18. }
  19. }
  20. }
  21.  
  22. main(){
  23. char a[] = "abaabcac";//模式串S2
  24. int next[8] = {0};//跳转表,初始化为全0
  25. int i;
  26.  
  27. //构造next表
  28. getNext(a, 8, next);
  29. //输出next表
  30. for(i = 0; i < 8; i++){
  31. printf("%d ", next[i]);
  32. }
  33. printf("\n");
  34. }

二者的计算结果是一样的,但书上给的算法消除了双重循环,不过这样做的结果只是让代码变得更复杂了而已,没有任何实质性的优化

什么?消除了双重循环竟然没有提高效率?不可能吧?我们不妨用计数器来验证一下:

  1. void getNext(char a[], int n, int next[]){
  2. int i, j;
  3. int counter = 0;///
  4.  
  5. next[0] = -1;//首元跳转值为-1
  6. next[1] = 0;//第二个元素跳转值为0
  7.  
  8. for(i = 2; i < n; i++){
  9. j = i - 1;
  10.  
  11. //递推得到next表中剩余值
  12. while(j != -1){
  13. if(a[i - 1] == a[next[j]]){
  14. next[i] = next[j] + 1;
  15. counter++;///
  16. break;
  17. }
  18. else{
  19. j = next[j];
  20. counter++;///
  21. }
  22. }
  23. }
  24.  
  25. printf("\n%d\n", counter);///
  26. }
  27.  
  28. void getNext(char a[], int n, int next[]){
  29. int i, j;
  30. int counter = 0;///
  31. i = 0;
  32. next[0] = -1;//首元跳转值为-1
  33. j = -1;
  34.  
  35. //递推得到next表中剩余值
  36. while(i < n){
  37. if(j == -1 || a[i] == a[j]){
  38. ++i;
  39. ++j;
  40. next[i] = j;
  41. counter++;///
  42. }
  43. else{
  44. j = next[j];
  45. counter++;///
  46. }
  47. }
  48.  
  49. printf("\n%d\n", counter);
  50. }

P.S.我们在具体操作的部分(最内部的if块与else块)插入了counter++;这样得到的结果才是可比的(算法进行了多少次具体操作)

运行结果如下:

左图是我们自己实现的next函数计数结果,少的这4步是外层循环次数的差异(书上的算法是n次,我们的算法是n-2次),如果我们的算法外层循环次数也是n的话,也需要14次具体操作(我们只是做了一个简单的优化)

两个next函数只是形式不同,其内部操作顺序是完全相同的,弄清楚了next函数,KMP算法就没什么难点了(如果不关心S1的指针不用回溯的原因的话,确实只有这一个难点..)

四.KMP算法正确性的证明

(本文不在此展开,以后理解了的话可能会在此处补充缺失的内容,详细解释为什么S1的指针不用回溯,为什么之前的部分不可能再匹配。。。)

不过单纯关注“实现”的话,我们只要理解了next函数就完全可以轻松实现KMP算法了(至于为什么可以这么做,怎么证明这样做是对的。。这是数学家的事情)

五.总结

KMP算法的核心是next表的构造过程,而构造next表的关键思想就是“递推”,理解了这个,完全可以分分钟写出KMP算法。。

一点题外话:

KMP算法也有变体,本文讨论的是最原始的KMP算法,常见的变体有:

  • next表首元为0(而不是-1),最终得到的结果就是给我们的next表中每个元素都+1,只是约定不同而已(从0开始与从1开始),并没有实质性的差别
  • next表中有多个-1(我们的结果中有且只有一个-1,也就是next表首元),这是一种实质性的优化,能有效的提高效率。其实也就是在构造next表的时候比我们多做了一步,构造过程变复杂了一点点,但匹配算法的比对次数减少了

经典串匹配算法(KMP)解析的更多相关文章

  1. 雷达无线电系列(二)经典CFAR算法图文解析与实现(matlab)

    一,CFAR基础知识介绍 简介 恒虚警检测技术是指雷达系统在保持虚警概率恒定条件下对接收机输出的信号与噪声作判别以确定目标信号是否存在的技术. 前提 由于接收机输出端中肯定存有噪声(包括大气噪声.人为 ...

  2. KMP串匹配算法解析与优化

    朴素串匹配算法说明 串匹配算法最常用的情形是从一篇文档中查找指定文本.需要查找的文本叫做模式串,需要从中查找模式串的串暂且叫做查找串吧. 为了更好理解KMP算法,我们先这样看待一下朴素匹配算法吧.朴素 ...

  3. 字符串匹配(KMP 算法 含代码)

    主要是针对字符串的匹配算法进行解说 有关字符串的基本知识 传统的串匹配法 模式匹配的一种改进算法KMP算法 网上一比較易懂的解说 小样例 1计算next 2计算nextval 代码 有关字符串的基本知 ...

  4. 模式串匹配之KMP算法

    模式串匹配之KMP算法 KMP算法 模式值计算(next[j]) (1) next[0]=-1,  第一个字符模式值为-1 (2) next[j]=-1, T中下标为j的字符与首字符相同,且j前面的1 ...

  5. 字符串匹配与KMP算法实现

    >>字符串匹配问题 字符串匹配问题即在匹配串中寻找模式串是否出现, 首先想到的是使用暴力破解,也就是Brute Force(BF或蛮力搜索) 算法,将匹配串和模式串左对齐,然后从左向右一个 ...

  6. Luogu 3375 【模板】KMP字符串匹配(KMP算法)

    Luogu 3375 [模板]KMP字符串匹配(KMP算法) Description 如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置. 为了减少骗分的情况,接下来 ...

  7. 字符串匹配的 KMP算法

    一般字符串匹配过程 KMP算法是字符串匹配算法的一种改进版,一般的字符串匹配算法是:从主串(目标字符串)和模式串(待匹配字符串)的第一个字符开始比较,如果相等则继续匹配下一个字符, 如果不相等则从主串 ...

  8. 字符串匹配的kmp算法 及 python实现

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

  9. HDU 1711 Number Sequence (字符串匹配,KMP算法)

    HDU 1711 Number Sequence (字符串匹配,KMP算法) Description Given two sequences of numbers : a1, a2, ...... , ...

随机推荐

  1. C#以记事本(指定程序)打开外部文档(指定文档)

    System.Diagnostics.Process.Start("notepad.exe", "D:\\a.txt");

  2. Adplus 抓取Crash Dump

    本实例在win8.1 安装window kits https://developer.microsoft.com/en-us/windows/hardware/windows-driver-kit 1 ...

  3. UI设计是青春饭?今天告诉你真相!

    最近有学员来问,“我想转行学习UI设计,但是听很多人说,UI设计是吃青春饭的,互联网公司是不是只选择年轻的血液而淘汰年纪大的?”今天,我来统一回答一下. UI设计是不是青春饭? 我们先来思考一个问题: ...

  4. composer install 时,提示:Package yiisoft/yii2-codeception is abandoned, you should avoid using it. Use codeception/codeception instead.的解决

    由 SHUIJINGWAN · 2017/11/24 1.composer install 时,提示:Package yiisoft/yii2-codeception is abandoned, yo ...

  5. Laravel + Vue 之 OPTIONS 请求的处理

    问题: 在 Vue 对后台的请求中,一般采用 axios 对后台进行 Ajax 交互. 交互发生时,axios 一般会发起两次请求,一次为 Options 试探请求,一次为正式请求. 由此带来的问题是 ...

  6. 设计模式之Adapter设计模式

    这个设计模式是我这两天刚学的,这儿算是我的读书笔记发布出来是供大家一起学习,后面有我自己的感悟,下面是我网上整理的 以下情况使用适配器模式 • 你想使用一个已经存在的类,而它的接口不符合你的需求. • ...

  7. 22条常用JavaScript开发小技巧

    1.使用var声明变量 如果给一个没有声明的变量赋值,默认会作为一个全局变量(即使在函数内赋值).要尽量避免不必要的全局变量. 2.行尾使用分号 虽然JavaScript允许省略行尾的分号,但是有时不 ...

  8. 百度 ueditor 1.2.0 注意事项 ,上传文件问题

    <script type="text/javascript" src="script/ueditor/ueditor.config.js" charset ...

  9. Core Dump 程序故障分析

    1.编写一个应用程序,使用gdb+core dump进行故障分析, core dump的概念: core dump又叫核心转存:当程序在运行过程中发生异常,这时Linux系统可以把程序在运行时的内存内 ...

  10. Jsp的语法和指令

    Jsp的三种注释 前端语言注释:<!-- --> 会被转译,也会被发送,但是不会被浏览器执行 java语言注释: 会被转译,但是不会被servlet执行 Jsp注释:<%--  -- ...