• 【手把手带你学会KMP算法】


    相信大家在遇到字符串匹配问题时,无论是听老师上课讲还是在网上查询资料时几乎都会用到KMP算法,本篇博客借鉴于大博哥对于KMP算法的分析以及自身对于KMP算法的看法,相信认真看完了后会对你有一些帮助。

    目录

     1 BF 算法

    2 KMP 算法

    3 KMP算法的优化



     1 BF 算法

    了解KMP算法之前,我们先来回忆一下BF算法(暴力求解),基本思想就是主串中元素与子串中元素一一比较,匹配失败就让子串返回到0下标,主串回溯到开始匹配的下一位。

    假设主串的长度位M  子串的长度为N  BF算法的时间复杂度为 O(M*N)

    动图详解(此图借鉴于其他博主):

     具体代码:

    1. int BF(char* mainStr, char* subStr,int pos)
    2. {
    3. assert(mainStr && subStr);
    4. if (!*subStr)
    5. {
    6. return 0;
    7. }
    8. if (!*mainStr)
    9. {
    10. return -1;
    11. }
    12. int lenMain = strlen(mainStr);
    13. int lenSub = strlen(subStr);
    14. int i = pos; //遍历主串
    15. int j = 0; //遍历子串
    16. while (i < lenMain && j < lenSub)
    17. {
    18. if (mainStr[i] == subStr[j])
    19. {
    20. i++;
    21. j++;
    22. }
    23. else
    24. {
    25. i = i - j + 1;
    26. j = 0;
    27. }
    28. }
    29. if (j == lenSub)
    30. {
    31. return i-j;
    32. }
    33. else
    34. {
    35. return -1;
    36. }
    37. }

    2 KMP 算法

    KMP算法的诞生:KMP算法是三位大牛:Knuth、Morris和Pratt同时发现的,于是取了他们名字的首字母然后组合起来,就成了该算法的命名。(来自百度)

     kmp算法的主要思想是:在主串中查找子串时,利用主串和子串中已经匹配的部分,跳过一些无用的比较,使主串的标记指针不会回溯,子串的标记指针移动。其实主要是子串中如果有部分内容和主串中一致,我们需要研究这些已知的匹配内容,这相当于我们需要研究子串,发现子串中存在的规律。KMP算法的时间复杂度为 O(M+N)

     听起来是不是有点懵,没关系,我们来举个栗子:

    主串:ababcabcdabcde

    子串:abcd

    由上述主串与子串数据可知目前在索引为2匹配失败,就算让主串回退到1也是没有必要的,因为主串中1位置字符b与子串0位置的a也不一样,所以我们的目的是:主串是不回退的,让子串回退到特定的位置

    那么子串应该回退到什么位置呢? 再来举个栗子:

    主串:abcababcabc

    子串:abcabc

    这里主串与子串在索引为5的位置不匹配,由于主串不回退,回退的是子串,通过肉眼观察我们发现子串回退到索引为2是比较合理的(我们要尽量在主串中找到和子串匹配的一部分串),由于主串中索引为5的字符a!=子串中索引为2的字符c,所以还要继续回退,直到回退到与字符a相等为止。那如果字符数量多了用肉眼观察肯定是不切合实际的,所以我们引出了next数组,用来保存子串中某个位置匹配失败后回退的位置

    KMP的精髓就是next数组,那么应该怎样来求next数组呢?假定next[j]=k;

    求k的规则:

    1. 找到匹配成功部分的两个相等的真子串(不包含本身)一个以下标0开始,另一个以j-1下标结束
    2. 总有next[0]=-1,next[1]=0。

    有了上面的规则我们就可以来求next数组了:

    栗子1:“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]

    通过上面的栗子我们可以得出总结:找的时候可以找重复的,但是必须是以0下标开始,找的前一位下标结束;而且数组中元素增大只能一个一个的加,减小可以跳跃减小。

    到这里我相信大家对求next数组应该没有什么问题了。但现在的问题是我们如何用公式来求next数组,总不能next数组让我们人为来算,这显然是不现实的。

    我们假设已知:next[i]=k;

     
    1. k i
    2. 给定一个字符串" a b c a b a b c a b c "
    3. 0 1 2 3 4 5 6 7 8 9 10
    4. next数组: [-1,0,0,0,1,2,1,2,3,4,5]

    我们假设此时i==8

    则我们可以知道:p[0]……p[k-1] == p[i-k]……p[i-1]

    所以由:next[i]=k  <--->   p[0]……p[k-1] == p[i-k]……p[i-1]

    如果现在 p[k]==p[i]  将其分别加在上式右边的两边得:

    p[0]……p[k] == p[i-k]……p[i]

    所以不难推出:next[i+1]=k+1;这也很好得解释了为什么增加只能一个一个的加。

    那么问题又来了,如果p[k] != p[i]呢?

    假设索引为8的元素由a改为b,那么k就要回退到next[k]的位置,也就是k=0,但还是不满足p[k]==p[i],就继续回退k=-1,此时已经找不到了,就直接将0给予给下一位,也就是:

    next[i+1]=0;

     好了,next数组的求解概念已经掌握,现在我们如何用代码来将其表示出来呢?

    代码的具体实现:

    1. int KMP(char* mainStr, char* subStr, int pos)
    2. {
    3. assert(mainStr && subStr);
    4. if (!*subStr)
    5. {
    6. return 0;
    7. }
    8. if (!*mainStr)
    9. {
    10. return -1;
    11. }
    12. int lenMain = strlen(mainStr);
    13. int lenSub = strlen(subStr);
    14. //动态申请next数组
    15. int* next = (int*)malloc(sizeof(int) * lenSub);
    16. if (NULL == next)
    17. {
    18. exit(-1);
    19. }
    20. GetNext(next,subStr);
    21. int i = pos; //遍历主串
    22. int j = 0; //遍历子串
    23. while (i < lenMain && j < lenSub)
    24. {
    25. if (mainStr[i] == subStr[j] || -1==j)
    26. {
    27. i++;
    28. j++;
    29. }
    30. else
    31. {
    32. j = next[j];
    33. }
    34. }
    35. if (j == lenSub)
    36. {
    37. free(next);
    38. return i - j;
    39. }
    40. else
    41. {
    42. free(next);
    43. return -1;
    44. }
    45. }

    这里面有一个容易出错的点,当j==-1时也不要忘记了处理,当j==-1时说明回到了索引为0的位置,并且还没有找到与其匹配的项,就让j从索引为0开始与i一一比对。

    得到next数组的具体代码:

    1. void GetNext(int* next, int* subStr)
    2. {
    3. assert(next && subStr);
    4. int lenSub = strlen(subStr);
    5. next[0] = -1;
    6. next[1] = 0;
    7. int i = 2;
    8. int k = 0;
    9. while (i < lenSub)
    10. {
    11. if ((-1 == k) || subStr[i - 1] == subStr[k])
    12. {
    13. next[i] = k + 1;
    14. i++;
    15. k++;
    16. }
    17. else
    18. {
    19. k = next[k];
    20. }
    21. }
    22. }

    由于这里的next[i]是未知的,而next[i-1]才是已知的,所以上面才是subStr[i - 1] == subStr[k]

    next[i] = k + 1; 同理当k==-1时说明到索引为0都还没有匹配的项就把其置为0.


    3 KMP算法的优化

    假定给一个这样的字符串: " a a a a a a a a a a g "

    它的next数组为:               [-1,0,1,2,3,4,5,6,7,8,9]      

     假设在索引为9的位置匹配失败,则j会不断回退直到j==-1,这样效率就比较低了,有什么办法能够一步到位回退呢?

    我们不妨优化一下next数组:为了区别,不妨将将优化后的数组取名为nextval

    nextval数组的规则如下:

    1 如果回退到的位置和当前字符一样,就写回退那个位置的nextval值;

    2 如果回退到的位置和当前字符不一样,就写当前原来的nextval值;

     举个栗子:给" a  b  c  a  a  b  b  c  a  b  c  a  a  b  d  a  b "

    next:               [-1, 0, 0, 0, 1, 1, 2, 0, 0, 1, 2, 3, 4, 5, 6, 0,1]

    nextval:          [-1, 0, 0,-1, 1, 0, 2, 0,-1, 0, 0,-1, 1, 0, 6,-1,0]

    具体代码:

    1. void GetNextval(int* nextval, int* subStr)
    2. {
    3. assert(nextval && subStr);
    4. int lenSub = strlen(subStr);
    5. nextval[0] = -1;
    6. nextval[1] = 0;
    7. int i = 2;
    8. int k = 0;
    9. while (i < lenSub)
    10. {
    11. if ((-1 == k) || subStr[i - 1] == subStr[k])
    12. {
    13. nextval[i] = k + 1;
    14. i++;
    15. k++;
    16. }
    17. else
    18. {
    19. k = nextval[k];
    20. }
    21. }
    22. i = 2;
    23. while (i < lenSub)
    24. {
    25. if (subStr[i] == subStr[nextval[i]])
    26. {
    27. nextval[i] = nextval[nextval[i]];
    28. }
    29. i++;
    30. }
    31. }

    好了,KMP算法就到这里了,如果有什么不对的地方欢迎各位佬在评论区指正。

  • 相关阅读:
    GAN实战笔记——第五章训练与普遍挑战:为成功而GAN
    Java基础之《undertow容器》
    Golang基础7-并发编程
    S7-200SMART通过循环移位实现MODBUS RTU轮询的具体方法示例
    18 OpenCV霍夫变换检测直线
    栈和队列的初始化,插入,删除,销毁。
    SpringBoot 配置文件使用详解
    DevComponents.DotNetBar2之SuperTabControl使用技巧
    蓝桥杯物联网竞赛_STM32L071_18_长短按键检测
    思科的joy提取加密流量特征教程以及基本使用
  • 原文地址:https://blog.csdn.net/m0_68872612/article/details/126827381