当前位置:   article > 正文

数据结构KMP算法详解

kmp算法

目录

1. KMP算法是什么?

2. KMP算法的由来

2.1 需要要解决的问题

2.2 一开始想到的方法

2.3 KMP算法诞生了

3.KMP算法的详解

4.KMP算法的实现

5.KMP算法的改进


1. KMP算法是什么?

  • KMP算法是一种改进的字符串匹配算法,即可以快速的从主串中找到子串的算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。
  • KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。

2. KMP算法的由来

2.1 需要要解决的问题

我们在数据结构的“串”这一章中提到的问题:主串找子串这个问题,
例如:现在我们有主串:arr1[ ] = "abababc",子串:arr2[ ] = “abc”。

2.2 一开始想到的方法

我们一开始想到的是暴力求解,我们是将子串和主串逐一匹配,如果第一个字符相等就继续匹配第二个字符,直到子串与主串全都匹配成功,就返回子串的位置,一旦其中某两个字符匹配不成功,主串就回到开始匹配字符的下一字符,而子串回到第一字符。
通过以上举例可以看出,传统的暴力求解,需要多次回溯,且第一个字符串和第二个字符串都需要回溯,明显回溯过去也一定是错的,那有没有什么办法让他回溯到特定的位置,不至于像暴力求解一样,产生许多不必要的步骤,于是就有了本篇的重点:KMP算法诞生了

2.3 KMP算法诞生了

KMP算法是一种改进的字符串匹配算法,即可以快速的从主串中找到子串的算法。

3.KMP算法的详解

(1)现在我们先看一个图:第一个长条代表主串,第二个长条代表子串,当发现指针指向的元素不匹配时,指针回溯到第一个元素,这个就是算法效率低的原因,称为幼稚的匹配算法。

(2)现在我们看一下聪明的KMP匹配算法是什么样子的:下图还是当发现指针指向的元素不匹配时,移动模式窜,使前缀移动到后缀的地方

(3)这一步弄懂了,KMP算法的精髓就差不多掌握了。事实上,每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且next数组的数值只与子串本身有关。
所以next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。

然后把他们放到一个数组中

4.KMP算法的实现

现在我们分析一下KMP算法的时间复杂度:
KMP算法中多了一个求数组的过程,多消耗了一点点空间。我们设主串s长度为n,子串t的长度为m。求next数组时时间复杂度为O(m),因后面匹配中主串不回溯,比较次数可记为n,所以KMP算法的总时间复杂度为O(m+n),空间复杂度记为O(m)。相比于朴素的模式匹配时间复杂度O(m*n),KMP算法提速是非常大的,这一点点空间消耗换得极高的时间提速是非常有意义的,这种思想也是很重要的。

  • 下面我们一起来欣赏计算机如何求得next数组的
  1. typedef struct
  2. {
  3. char data[MaxSize];
  4. int length; //串长
  5. } SqString;
  6. //SqString 是串的数据结构
  7. //typedef重命名结构体变量,可以用SqString t定义一个结构体。
  8. void GetNext(SqString t,int next[]) //由模式串t求出next
  9. {
  10. int j,k;
  11. j=0;k=-1;
  12. next[0]=-1;//第一个字符前无字符串,给值-1
  13. while (j<t.length-1)
  14. //因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
  15. //所以最后一次经过while循环时j为t.length-2
  16. {
  17. if (k==-1 || t.data[j]==t.data[k]) //k为-1或比较的字符相等时
  18. {
  19. j++;k++;
  20. next[j]=k;
  21. //对应字符匹配情况下,s与t指向同步后移
  22. //通过字符串"aaaaab"next数组过程想一下这一步的意义
  23. //printf("(1) j=%d,k=%d,next[%d]=%d\n",j,k,j,k);
  24. }
  25. else
  26. {
  27. k=next[k];
  28. **//我们现在知道next[k]的值代表的是下标为k的字符前面的字符串最长相等前后缀的长度
  29. //也表示该处字符不匹配时应该回溯到的字符的下标
  30. //这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符**
  31. //为什么要回退此处进行比较,我们往下接着看。其实原理和上面介绍的KMP原理差不多
  32. //printf("(2) k=%d\n",k);
  33. }
  34. }
  35. }
  • KMP算法代码解释
  1. int KMPIndex(SqString s,SqString t) //KMP算法
  2. {
  3. int next[MaxSize],i=0,j=0;
  4. GetNext(t,next);
  5. while (i<s.length && j<t.length)
  6. {
  7. if (j==-1 || s.data[i]==t.data[j])
  8. {
  9. i++;j++; //i,j各增1
  10. }
  11. else j=next[j]; //i不变,j后退,现在知道为什么这样让子串回退了吧
  12. }
  13. if (j>=t.length)
  14. return(i-t.length); //返回匹配模式串的首字符下标
  15. else
  16. return(-1); //返回不匹配标志
  17. }

5.KMP算法的改进

为什么KMP算法这么强大了还需要改进呢?
大家来看一个例子:
主串s=“aaaaabaaaaac”
子串t=“aaaaac”
这个例子中当‘b’与‘c’不匹配时应该‘b’与’c’前一位的‘a’比,这显然是不匹配的。'c’前的’a’回溯后的字符依然是‘a’。
我们知道没有必要再将‘b’与‘a’比对了,因为回溯后的字符和原字符是相同的,原字符不匹配,回溯后的字符自然不可能匹配。但是KMP算法中依然会将‘b’与回溯到的‘a’进行比对。这就是我们可以改进的地方了。我们改进后的next数组命名为:nextval数组。KMP算法的改进可以简述为: 如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。 这应该是最浅显的解释了。如字符串"ababaaab"的next数组以及nextval数组分别为:
下标     | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
子串     | a | b | a | b | a | a | a | b |
next     |-1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 |
nextval |-1| 0 |-1 | 0 |-1 | 3 | 1 | 0 |

我们来分析下代码:

  1. void GetNextval(SqString t,int nextval[])
  2. //由模式串t求出nextval值
  3. {
  4. int j=0,k=-1;
  5. nextval[0]=-1;
  6. while (j<t.length)
  7. {
  8. if (k==-1 || t.data[j]==t.data[k])
  9. {
  10. j++;k++;
  11. if (t.data[j]!=t.data[k])
  12. //这里的t.data[k]是t.data[j]处字符不匹配而会回溯到的字符
  13. //为什么?因为没有这处if判断的话,此处代码是next[j]=k;
  14. //next[j]不就是t.data[j]不匹配时应该回溯到的字符位置嘛
  15. nextval[j]=k;
  16. else
  17. nextval[j]=nextval[k];
  18. //这一个代码含义是不是呼之欲出了?
  19. //此时nextval[j]的值就是就是t.data[j]不匹配时应该回溯到的字符的nextval值
  20. //用较为粗鄙语言表诉:即字符不匹配时回溯两层后对应的字符下标
  21. }
  22. else k=nextval[k];
  23. }
  24. }
  1. int KMPIndex1(SqString s,SqString t)
  2. //修正的KMP算法
  3. //只是next换成了nextval
  4. {
  5. int nextval[MaxSize],i=0,j=0;
  6. GetNextval(t,nextval);
  7. while (i<s.length && j<t.length)
  8. {
  9. if (j==-1 || s.data[i]==t.data[j])
  10. {
  11. i++;j++;
  12. }
  13. else j=nextval[j];
  14. }
  15. if (j>=t.length)
  16. return(i-t.length);
  17. else
  18. return(-1);
  19. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/884468
推荐阅读
相关标签
  

闽ICP备14008679号