0


算法笔记(一)——KMP算法

暴力匹配(BF)算法

基本概念

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。

分析BF算法

光看定义晦涩难懂,接下来我将举例子与大家一起学习:

假定我们给出字符串"ababcabcdabcde"做为主串,然后给出子串"abcd",现在我们需要查找子串是否在主串中出现,出现返回主串中第一个匹配的下标,失败返回-1.

对于这个问题我们很容易想到:从左到右依次匹配,如果字符相等,都向后移一位,如不相等, 子串从0下标重新开始,主串向右移动一位(假设原来从0下标开始,下次从1下标开始)

我们可以这样初始化:

根据我们的想法,之后我们就需要比较i指针和j指针指向的数字是否一致,如果一致都向后移动,如果不一致,如下图:

b和d不相等,那就把i指针回退到刚刚指针的下一个位置(刚刚指针是从0下标开始的),j指针退回到0下标重新开始。

代码实现

根据以上的分析,下面我们开始写代码:

C代码:

  1. #include<stdio.h>
  2. #include<string.h>
  3. #include<assert.h>
  4. int BF(char* str1, char* str2)
  5. {
  6. assert(str1 != NULL && str2 != NULL);
  7. int len1 = strlen(str1);//主串的长度
  8. int len2 = strlen(str2);//子串的长度
  9. int i = 0;//主串的起始位置
  10. int j = 0;//子串的起始位置
  11. while (i < len1 && j < len2)
  12. {
  13. if (str1[i] == str2[j])
  14. {
  15. i++;//相等i和j都向后移动一位
  16. j++;
  17. }
  18. else {//不相等
  19. i = i - j + 1;//i回退
  20. j = 0;//j回到0位置
  21. }
  22. }
  23. if (j >= len2) {//子串遍历玩了说明已经找到与其匹配的子串
  24. return i - j;
  25. }
  26. else {
  27. return -1;
  28. }
  29. }
  30. int main()
  31. {
  32. printf("%d\n", BF("ababcabcdabcde", "abcd"));//测试,为了验证代码是否正确尽量多举几个例子
  33. printf("%d\n", BF("ababcabcdabcde", "abcde"));
  34. return 0;
  35. }

java代码:

  1. public class Test {
  2. public static int BF(String str,String sub) {
  3. if(str == null || sub == null) return -1;
  4. int strLen = str.length();
  5. int subLen = sub.length();
  6. int i = 0;
  7. int j = 0;
  8. while (i < strLen && j < subLen) {
  9. if(str.charAt(i) == sub.charAt(j)) {
  10. i++;
  11. j++;
  12. }else {
  13. i = i-j+1;
  14. j = 0;
  15. }
  16. } i
  17. f(j >= subLen) {
  18. return i-j;
  19. } r
  20. eturn -1;
  21. }
  22. public static void main(String[] args) {
  23. System.out.println(BF("ababcabcdabcde","abcd"));
  24. System.out.println(BF("ababcabcdabcde","abcde"));
  25. }
  26. }

牛刀小试

通过上面的学习,对BF算法有了初步的认识,为了更深刻的了解和运用,下面和大家一起完成以下试题:

试题在这里>>实现strStr()

感兴趣的伙伴可以去做试试,下一章我们进行共同探讨;

BF算法的时间复杂度

最好的情况就是从第一次开始就匹配成功时间复杂度为O(1);

最坏的情况就是每次都匹配到最后一个才发现与主串不相同,比如"aaaaab",子串”aab"

看上图,除了最后一次,其余的都是每次匹配到最后,才发现,啊,我们不一样。

这种情况下,上图中,模式串在前 3 次,每次都要匹配 3 次,并且不匹配,直到第 4 次,全部匹配,不需要继续移动,所以匹配的次数为(6 - 3 + 1)* 3 = 12 次。

由此可知,对于主串长度为 n,模式串长度为 m ,最坏情况下的时间复杂度为 O((n - m + 1) * m) = O(n * m)。
相信善于思考的小伙伴会发现,如果是为了寻找的话,根本不需要将i移动到1的位置,因为前面几个字符都是匹配的,再将i移动到1的位置,j移动到0位置,位置错开了,显然也不会匹配,那么我们能不能丢掉以上没必要的步骤,减少指针回溯进行算法简化囊,有一个想法,i位置不动,只需要移动j位置,由此引出我们今天的主人公kmp算法.

KMP算法

基本概念

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

区别:KMP 和 BF 唯一不一样的地方在,我主串的 i 并不会回退,并且 j 也不会移动到 0 号位置

分析KMP算法

假定我们给出字符串"ababcabcdabcde"做为主串,然后给出子串"abcd",现在我们需要查找子串是否在主串中出现,出现返回主串中第一个匹配的下标,失败返回-1.

1.首先举例说明,为什么主串不回退

2.j的回退的位置

那么j是怎么回退到下标2的位置的囊?下面我们引出next数组

引出next数组

KMP 的精髓就是 next 数组:也就是用** next[j] = k**;来表示,**不同的 j 来对应一个 K 值,**这个 K 就是你将来要移动的 j要移动的位置。而 K 的值是这样求的:

  • ** 规则:找到匹配成功部分的两个相等的真子串(不包含本身),一个以下标 0 字符开始,另一个以 j-1 下标字符结尾。**
  • 不管什么数据 next[0] = -1;next[1] = 0;在这里,我们以下标来开始,而说到的第几个第几个是从 1 开始;

**求next数组的练习: **

练习一:举例对于”ababcabcdabcde”, 求其的 next 数组?

-1 0 0 1 2 0 1 2 0 0 1 2 0 0

练习 2: 再对”abcabcabcabcdabcde”,求其的 next 数组? "
-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0

核心的东西来了:
到这里大家对如何求next数组应该问题不大了,接下来的问题就是,已知next[i] = k;怎么求next[i+1] = ?
如果我们能够通过 next[i]的值,通过一系列转换得到 next[i+1]得值,那么我们就能够实现这部分。
那该怎么做呢?

首先假设: next[i] = k 成立,那么,就有这个式子成立:P0...Pk-1 = Px...Pi-1,得到: P0...Pk-1 = Pi-k..Pi-1;如下图分析:


然后我们假设如果 Pk = Pi;我们可以得到 P0...Pk = Pi-k..Pi;那这个就是 next[i+1] = k+1;

那么: Pk != Pi 呢?

代码实现

C代码:

  1. #include<stdio.h>
  2. #include<string.h>
  3. #include<assert.h>
  4. void GetNext(int* next, char* sub, int len2)
  5. {
  6. next[0] = -1;//规定第一个为-1,第二个为0,则直接这样定义就好了;
  7. next[1] = 0;
  8. int k =0;//前一项的k
  9. int j = 2;//下一项
  10. while (j < len2)
  11. {
  12. if (k==-1||sub[j-1] == sub[k])
  13. {
  14. next[j] = k + 1;
  15. j++;
  16. k++;
  17. }
  18. else
  19. {
  20. k = next[k];
  21. }
  22. }
  23. }
  24. int KMP(char* str, char* sub, int pos)
  25. {
  26. assert(str != NULL && sub != NULL);
  27. int len1 = strlen(str);
  28. int len2 = strlen(sub);
  29. assert(pos >= 0 && pos < len1);
  30. int i = pos;//i从指定下标开始遍历
  31. int j = 0;
  32. int* next = (int*)malloc(sizeof(int) * len2);//动态开辟next和子串一样长
  33. assert(next != NULL);
  34. GetNext(next, sub, len2);
  35. while (i < len1 && j < len2)
  36. {
  37. if (j == -1||str[i] == sub[j])//j==-1是防止next[k]回退到-1的情况
  38. {
  39. i++;
  40. j++;
  41. }
  42. else {
  43. j = next[j];//如果不相等,则用next数组找到j的下个位置
  44. }
  45. }
  46. if (j >= len2)
  47. {
  48. return i - j;
  49. }
  50. else {
  51. return -1;
  52. }
  53. }
  54. int main()
  55. {
  56. char* str = "ababcabcdabcde";
  57. char* sub = "abcd";
  58. printf("%d\n", KMP(str, sub, 0));
  59. return 0;
  60. }

java代码:

  1. public static void getNext(int[] next, String sub){
  2. next[0] = -1;
  3. next[1] = 0;
  4. int i = 2;//下一项
  5. int k = 0;//前一项的K
  6. while(i < sub.length()){//next数组还没有遍历完
  7. if((k == -1) || sub.charAt(k) == sub.charAt(i-1)) {
  8. next[i] = k+1;
  9. i++;
  10. k++;
  11. }else{
  12. k = next[k];
  13. }
  14. }
  15. }
  16. public static int KMP(String s,String sub,int pos) {
  17. int i = pos;
  18. int j = 0;
  19. int lens = s.length();
  20. int lensub = sub.length();
  21. int[] next= new int[sub.length()];
  22. getNext(next,sub);
  23. while(i < lens && j < lensub){
  24. if((j == -1) || (s.charAt(i) == sub.charAt(j))){
  25. i++;
  26. j++;
  27. }else{
  28. j = next[j];
  29. }
  30. }
  31. if(j >= lensub) {
  32. return i-j;
  33. }else {
  34. return -1;
  35. }
  36. }
  37. public static void main(String[] args) {
  38. System.out.println(KMP("ababcabcdabcde","abcd",0));
  39. System.out.println(KMP("ababcabcdabcde","abcde",0));
  40. System.out.println(KMP("ababcabcdabcde","abcdef",0));
  41. }

关键代码讲解

else{

j=next[j]

}

if (j == -1||str[i] == sub[j])
{
i++;
j++;
}

问题:为啥还有个j==-1?

如下图所示:当第一个字符就不匹配,i,j此时都是0**,j=next[j] >> j=next[0] >> j=-1; **此时j是-1,如果不添加j==-1这种情况,那么这个程序将结束返回没有匹配,但你仔细观察下图,P[5]~P[8]与子串相匹配,故答案显然错误.所以我们应该加上j==-1这种情况,让其从头再遍历;

next[0] = -1;
next[1] = 0;
int k =0;//前一项的k
int j = 2;//下一项

根据我们的规定next数组第一个和第二个数为-1和0,故没啥问题。k=0是前一项k的值,j=2是下一项.

if (k==-1||sub[j-1] == sub[k])
{
next[j] = k + 1;
j++;
k++;
}

根据上面的内容我们可知,p[j]==p[k],next[i]=k;则能推出next[i+1]=k+1;如下图所示,不过这里i是j-1,大家要注意这一点, p[j]==p[k]>>sub[j-1]==sub[k];next[i+1]=k+1>>next[j]=k+1;

else
{
k = next[k];
}

这个知识点上面讲过,当p[j]!=p[k]时候,k回退,一直找到p[j]==p[k]然后用这个next[i+1]=k+1;

牛刀小试

题目在这里>>重复的子字符串

感兴趣的伙伴可以去做试试,下一章我们进行共同探讨;

KMP算法的时间复杂度

假设在M字符串中找N字符串的起始位置,长度分别为m和n,使用KMP算法,一般认为时间复杂度是**O(m+n),**也就是计算next数组的时间复杂度是O(n),而匹配的时候是O(m).

以上是KMP算法的讲解,有不足的地方或者对代码有更好的见解,欢迎评论区留言共同商讨,共同进步!!

标签: 算法

本文转载自: https://blog.csdn.net/m0_58367586/article/details/123073696
版权归原作者 skeet follower 所有, 如有侵权,请联系我们删除。

“算法笔记(一)&mdash;&mdash;KMP算法”的评论:

还没有评论