KMP一个非常经典的字符串模式匹配算法,虽然网上有很多kmp算法的博客,但是为了更好的理解kmp我还是自己写了一遍(这个kmp的字符串存储是基于堆的(heap),和老师说的定长存储略有不同,字符串索引从0开始)

  先来说说 KMP 的历史吧。

一、背景  

  KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度度O(m+n)。KMP也可以处理最重复长子串问题,最长子串问题……这里挂一道最简单的题leetcode的 实现 strStr(),大家看完可以去试一试。

补充说明:

  强调两个概念:真前缀 ,真后缀  

如图所示,所谓的真前缀,就是在指在除了自身之外的全部字符串的头部顺序组合;而"真后缀",就是指在除自身之外的一个字符串的全部尾串的顺序组合 。与后缀、前缀不同:

            真前/后缀不包含自身字符!!!

其实就类似于离散数学中集合里子集真子集的概念。

二、朴素字符串匹配算法

  其实就是我们最开始的时候写的字符串匹配,就是两个字符串逐一匹配。不作详细介绍,代码如下

  1. /**
  2. * @brief 朴素字符串匹配
  3. * @note
  4. * @param MainString: 主串
  5. * @param Pattern: 模式串
  6. * @retval
  7. */
  8. int SimpleStringMatch(char* MainString, char* Pattern)
  9. {
  10. int i = ;
  11. int j = ;
  12. int PatternLen = strlen(Pattern);
  13. int MainStringLen = strlen(MainString);
  14.  
  15. while (i < MainStringLen && j < PatternLen)
  16. {
  17. if (S[i] == Pattern[j])
  18. {
  19. i++;
  20. j++;
  21. }
  22. else
  23. {
  24. i = i - j + ;
  25. j = ;
  26. }
  27. }
  28. if (j == PatternLen)
  29. {
  30. return i - j;
  31. }
  32. return -;
  33. }

  很显然暴力匹配的时间复杂度为O(m*n)。m,n 分别取决于MainString和Pattern的长度,很显然这种时间复杂度还很高的。这个算法一旦匹配失败就主串索引 +1 ,模式串索引重置,当主串为 ABCDEABCDEF 模式串为 ABCDEF 。啊,完美体现了暴力匹配的缺点——逐一匹配(管你之前比没比过)。当你 A != F 时,我们人肯定会从第二个A处继续查找,让余下的部分和模式串继续进行匹配。如果计算机这样执行,那么算法的时间复杂度就从O(m*n)降到了O(m+n)从乘数级算法瞬间降到了无限接近常数级的算法,神优化啊(再次膜拜大神)。但是计算机不会啊,他只会勤勤恳恳的执行我们所写的代码,因此它就会从第一个B处开始比较,但是事实上从 B ~ E 的所有匹配都是无用功,为了解决这个问题 KMP应运而生。

三、KMP字符串模式匹配算法

  3.1 算法流程:

  (1)

第一步 主串"E……" 与 模式串 "ABCDABD" 的第一个字符进行比较。‘E!= ’A,模式串索引不变,主串索引+1

  (2)

E与A仍然不匹配,继续后移知道第一个匹配的位置

(3)

抵达第一处相同点

(4)

第二处相同点,继续

(5)

嗯!,不匹配了,怎么办呢。

(6)

第一反应肯定是把模式串整体后移一位然后重新一位位的比较,这样子是没有问题,但是这就不是kmp了,没有利用已完成的匹配信息。

(7)

当我们发现D和空格不匹配的时候,我们已经知道了前面6个字符为ABCDAB(主串)。KMP就是充分利用了这个信息。将模式串继续后移,没有将其移回比较过的位置。

(8)

计算机不比我们大脑,这种的事情对它来说已经很困难了,肯定要给它写个专门的算法了。KMP的索引的转跳依赖的是next[] 数组。如图,我们先用,先对KMP有一个完整的理解再来进行实现。理解才是关键。

i 0 1 2 3 4 5 6 7
模式串 A B C D A B D '\0'
next[i] -1 0 0 0 0 2 0

(9)

如图,D与空格不匹配,D之前的字符已经完成匹配,为已知信息。根据转跳数组可知,不匹配出D的next值为2,因此接下来从模式串所引为2的位置进行匹配。

(10)

同样的,C与空格不匹配,C处的next为0,所以下一个从索引为0的位置进行匹配。

(11)

A与空格比较不匹配,此处next值为-1,表示模式串的索引为1字符就不匹配,那么直接往后移一位。

(12)

一位位的比较直到完全匹配。

其实KMP的比较算法和朴素匹配的方法是一样的,KMP之所以快是快在索引的转跳上。接着我们就来说一下next数组是u如何实现的。

  

  3.2 next数组实现:

next数组的求解基于 "真前缀" 和 "真后缀" ,即next[i]等于P[0]...P[i - 1]最长的相同真前后缀的长度。(忘记的赶紧上去看看,要不然会一直懵的)。

i 0 1 2 3 4 5 6 7
模式串 A B C D A B D '\0'
next[i] -1 0 0 0 0 2 0

  1. i = 0,对于模式串的首字符,我们统一为next[0] = -1;
  2. i = 1,前面的字符串为A,其最长相同真前后缀长度为0,即next[1] = 0;
  3. i = 2,前面的字符串为AB,其最长相同真前后缀长度为0,即next[2] = 0;
  4. i = 3,前面的字符串为ABC,其最长相同真前后缀长度为0,即next[3] = 0;
  5. i = 4,前面的字符串为ABCD,其最长相同真前后缀长度为0,即next[4] = 0;
  6. i = 5,前面的字符串为ABCDA,其最长相同真前后缀为字符A,即next[5] = 1;
  7. i = 6,前面的字符串为ABCDAB,其最长相同真前后缀为字符AB,即next[6] = 2;
  8. i = 7,前面的字符串为ABCDABD,其最长相同真前后缀长度为0,即next[7] = 0。

那么,这个数组是如何实现不匹配自动跳转的呢?

  举个栗子:前置字符串

    假如 i = 6 时不匹配,其前置字符串为 “ABCDAB”,仔细观察,首尾都有 “AB”,这意味着主串和模式串刚刚比较完 “AB”。那,当进行下一次比较的时候,我们就可以直接用 i = 2 时的字符C进行下一次匹配。因为刚刚模式串后方的“AB”刚比较完,所以没有必要再进行。i =  6 时候字符D的其最长相同真前后缀为字符恰好也为“AB”,长度恰好等于索引,刚好能转跳到C。

但是现在有一个问题,看表中 i = 5 时,匹配失败, next数组值为1,这不符KMP的理念啊。理论上,我们应该把 i = 2 处的字符拿过来匹配,如果拿 i = 1 处的字符那就会产生一次多余的比较。这个问题的遗留并不是算法问题,而是算法没有优化,KMP未优化的算法是用也是有特定作用的。两种算法应用场景不同,各有所长。(最后会说明优化算法的)。

下面是代码实现:

  1. /**
  2. * @brief next[]
  3. * @note 未优化KMP,j == -1 不可删除
  4. * @param Pattern: 模式串
  5. * @param next[]: 转跳数组
  6. * @retval None
  7. */
  8. void GetNext(char* Pattern, int next[])
  9. {
  10. int Pattern_len = strlen(Pattern);
  11. int i = ; // Pattern 的下标
  12. int j = -;
  13. next[] = -;
  14.  
  15. while (i < Pattern_len - )
  16. {
  17. if (j == - || Pattern[i] == Pattern[j])
  18. {
  19. i++;
  20. j++;
  21. next[i] = j;//匹配就递推
  22. }
  23. else
  24. {
  25. j = next[j];//不匹配就转跳到上一个匹配的位置
  26. }
  27. }
  28. }

有没有看懂的,我觉得肯定有。有我也得分析一下这个算法干了什么。

其实,这个代码最难理解的就在于 if……else……

  先上张图(感谢大佬给我的图)

现在我假设 i 和 j 的位置如上,由前面代码中的 next[i] = j 得, i 的最长相同真前后缀分别是 [0, j-1] 和 [i-j, i-1],即这两段内容相同

走流程:

  1. if (j == - || Pattern[i] == Pattern[j])
  2. {
      i++;
  3. j++;
  4. next[i] = j;
  5. }
  6. else
  7. {
      j = next[j];
  8. }

next[j] 表 [0,j - 1] 区间中最长相同真前后缀的长度。如图

左侧两个椭圆来表示这个最长相同真前后缀,即这两个椭圆代表的区段内容相同;同理,右侧也有相同的两个椭圆。所以else语句就是利用第一个椭圆和第四个椭圆内容相同来加快得到[0, i - 1]区段的相同真前后缀的长度。说到在透彻一些就是 j = next[j],这句语句减少了无用的比较。

有没有想过,为什么next的第一个值为-1呢?

  第一,

    程序刚运行时,j是被初始为-1,直接进行 Pattern[i] == Pattern[j] 判断无疑会边界溢出;

  第二,

    else语句中j = next[j],j 是不断后退的,若 j 在后退中被赋值为 -1(也就是 j = next[0]),在 Pattern[i] == Pattern[j] 判断也会边界溢出。

  综上,其意义就是为了特殊边界判断,而且 j 一开始被赋值为-1,比较方便给第二项赋值。

四、KMP样例实现

  最好自己实现一遍!!!

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4.  
  5. int KMP(char* MainString, char* Pattern);
  6. void GetNext(char* Pattern, int next[]);
  7.  
  8. int main()
  9. {
  10. printf("%d\n",KMP("ljhgfdsa asdfghjkl\0", "dfghj\0"));
  11. }
  12.  
  13. int KMP(char* MainString, char* Pattern)
  14. {
  15. int next[] = {};
  16. GetNext(Pattern, next);
  17.  
  18. int i = ;
  19. int j = ;
  20. int s_len = strlen(MainString);
  21. int PatternLen = strlen(Pattern);
  22.  
  23. while (i < s_len && j < PatternLen)
  24. {
  25. if (j == - || MainString[i] == Pattern[j])
  26. {
  27. i++;
  28. j++;
  29. }
  30. else
  31. {
  32. j = next[j];
  33. }
  34. }
  35.  
  36. if (j == PatternLen)
  37. {
  38. return i - j;
  39. }
  40.  
  41. return -;
  42. }
  43.  
  44. void GetNext(char* Pattern, int next[])
  45. {
  46. int PatternLen = strlen(Pattern);
  47. int i = ;
  48. int j = -;
  49. next[] = -;
  50.  
  51. while (i < PatternLen - )
  52. {
  53. if (j == - || Pattern[i] == Pattern[j])
  54. {
  55. i++;
  56. j++;
  57. next[i] = j;
  58. }
  59. else
  60. {
  61. j = next[j];
  62. }
  63. }
  64. }

KMP

五、KMP优化

  还记得上面的那个问题吗?KMP不够完美的问题,其实只需要判断一下他们是不是相等的字符,然后进行处理即可。

  处理方式:获取前一个字符的最长字串。

自己先试试

  1. void GetNextval(char *Pattern, int nextval[])
  2. {
  3. int p_len = strlen(Pattern);
  4. int i = ;
  5. int j = -;
  6. nextval[] = -;
  7.  
  8. while (i < p_len - )
  9. {
  10. if (j == - || Pattern[i] == Pattern[j])
  11. {
  12. i++;
  13. j++;
  14.  
  15. //优化
  16. if (Pattern[i] != Pattern[j])
  17. {
  18. nextval[i] = j;
  19. }
  20. else
  21. {
  22. nextval[i] = nextval[j]; \\一样的时候最长字串来源于前一个
  23. }
  24. }
  25. else
  26. {
  27. j = nextval[j];
  28. }
  29. }
  30. }

getnext优化

KMP算法(未优化版): next数组表示最长的相同真前后缀的长度,我们不仅可以利用next来解决模式串的匹配问题,也可以用来解决类似字符串重复问题等等,这类问题大家可以在各大OJ找到。

KMP算法(优化版): 根据代码很容易知道(名称也改为了nextval),优化后的next仅仅表示相同真前后缀的长度,但不一定是最长(称其为“最优相同真前后缀”更为恰当)。此时我们利用优化后的next可以在模式串匹配问题中以更快的速度得到我们的答案(相较于未优化版),但是上述所说的字符串重复问题,优化版本则束手无策。

数据结构——KMP(串)的更多相关文章

  1. hdu 3336:Count the string(数据结构,串,KMP算法)

    Count the string Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) ...

  2. 数据结构--KMP算法总结

    数据结构—KMP KMP算法用于解决两个字符串匹配的问题,但更多的时候用到的是next数组的含义,用到next数组的时候,大多是题目跟前后缀有关的 . 首先介绍KMP算法:(假定next数组已经学会, ...

  3. 【Java】 大话数据结构(8) 串的模式匹配算法(朴素、KMP、改进算法)

    本文根据<大话数据结构>一书,实现了Java版的串的朴素模式匹配算法.KMP模式匹配算法.KMP模式匹配算法的改进算法. 1.朴素的模式匹配算法 为主串和子串分别定义指针i,j. (1)当 ...

  4. 浅谈数据结构之KMP(串中的模式匹配算法)

    KMP算法是一种模式匹配算法的改进版,其通过减少匹配的次数以及使主串不回朔来减少字符串匹配的次数,从而较少算法的相应代价,但是,事件万物是普遍归中的,KMP算法的有效性也是有一定的局限的,我将在本文的 ...

  5. 数据结构-模式匹配串算法(KMP)

    #include<cstdio> #include<iostream> #include<string> #include<cstring> #incl ...

  6. 大话数据结构(8) 串的模式匹配算法(朴素、KMP、改进算法)

    --喜欢记得关注我哟[shoshana]-- 目录 1.朴素的模式匹配算法2.KMP模式匹配算法 2.1 KMP模式匹配算法的主体思路 2.2 next[]的定义与求解 2.3 KMP完整代码 2.4 ...

  7. javascript实现数据结构:串--定长顺序存储表示以及kmp算法实现

    串(string)(或字符串)是由零个或多个字符组成的有限序列.串中字符的数目称为串的长度.零个字符的串称为空串(null string),它的长度为零. 串中任意个连续的字符组成的子序列称为该串的子 ...

  8. 实验数据结构——KMP算法Test.ming

    翻译计划     小明初学者C++,它确定了四个算术.关系运算符.逻辑运算.颂值操作.输入输出.使用简单的选择和循环结构.但他的英语不是很好,记住太多的保留字,他利用汉语拼音的保留字,小屋C++,发明 ...

  9. C#数据结构之串

    串(string)是n(n>=0)个字符组成的有限序列. 由于串中的字符都是连续存储的,在C#中有恒定不变的特性.一经创建就保持不变. 为了区别C#中的string,因此以stringDS类模拟 ...

随机推荐

  1. sparkRDD:第1节 RDD概述;第2节 创建RDD

    RDD的特点: (1)rdd是数据集: (2)rdd是编程模型:因为rdd有很多数据计算方法如map,flatMap,reduceByKey等: (3)rdd相互之间有依赖关系: (4)rdd是可以分 ...

  2. day20-Python运维开发基础(装饰器 / 类中的方法 / 类的方法变属性)

    1. 装饰器 / 类中的方法 / 类的方法变属性 # ### 装饰器 """ 定义:装饰器用于拓展原来函数功能的一种语法,返回新函数替换旧函数 优点:在不更改原函数代码的 ...

  3. Languages-used-on-the-Internet

    Languages-used-on-the-Internet 1. 互联网上使用的语言 1.1 网站内容语言 1.2 按语言互联网用户 1.3 维基百科文章统计 2. 综合以上表格数据出图表(2019 ...

  4. 关于eclipse项目右键没有project facets的解决方法遇到的问题

    [ 关于eclipse项目右键没有project facets的解决方法] [创建maven项目生成WebRoot目录,web.xml文件,以及修改编译路径classess的解决办法,以及解决找不到或 ...

  5. Python爬虫教程-爬取5K分辨率超清唯美壁纸源码

    简介 壁纸的选择其实很大程度上能看出电脑主人的内心世界,有的人喜欢风景,有的人喜欢星空,有的人喜欢美女,有的人喜欢动物.然而,终究有一天你已经产生审美疲劳了,但你下定决定要换壁纸的时候,又发现网上的壁 ...

  6. Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL

    由于mysql版本过高创建连接的时候会出现如下报告 解决办法:在mysql连接上加上&useSSL=true 如下:jdbc:mysql:///:3366:test?useUnicode=tr ...

  7. 如何创建Github账号及将本地项目上传至GitHub?

    如何将本地项目上传至GitHub 首先你需要一个github账号,所有还没有的话先去注册吧! https://github.com/ 我们使用git需要先安装git工具,这里给出下载地址,下载后一路直 ...

  8. 吴裕雄 Bootstrap 前端框架开发——Bootstrap 表格:表示成功的操作

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  9. 第1节 kafka消息队列:5、javaAPI操作

    8.kafka的API 详见代码   第一步:导入kafka的开发jar包 Kafka生产者 Kafka消费者

  10. 伪奢侈品iPhone大降价,肉搏国产手机胜算几何?

    据国外媒体报道,苹果在中国降低iPhone价格的策略已收到明显的效果,自从1月11日正式调整价格以来,iPhone在苏宁电器平台上的销量飙升83%,而天猫上的销量也增长了76%,其中最受欢迎的机型是i ...