KMP算法

一.绪论

很久之前就学习了KMP算法 , 也在网上查阅了许多的资料 , 但是一直对KMP算法的代码不甚熟悉 . 如果单纯靠背诵代码 , 而不理解其本质 , 是舍本逐末 . 所以今天力求用简短之篇幅讲解KMP算法 , 一是回顾知识 , 二来希望能让本文的读者有一点启发 .

二 : KMP算法讲解


给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


KMP算法 , 主要适用于这样的场景 , 即字符串A中是否包括字符串B .

解决这一问题 , 我们可以暴力匹配 , 如下图所示 , i和j分别从主串和子串的开头进行匹配 :
在这里插入图片描述

当两者走到下图所示位置时 , 发现匹配失败了 . 此时i和j都需要进行回溯 ;

在这里插入图片描述

j回溯到子串开头位置 , i回溯到主串下标为1的地方 , 再次开始匹配 .
在这里插入图片描述

暴力算法 , 就是重复上述的过程 , 直到我们找到一组匹配的结果为为止 . 代码如下 :

int ViolentMatch(char* s, char* p)
{
  int sLen = strlen(s);
  int pLen = strlen(p);
 
  int i = 0;
  int j = 0;
  while (i < sLen && j < pLen)
  {
    if (s[i] == p[j])
    {
      //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++    
      i++;
      j++;
    }
    else
    {
      //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0    
      i = i - j + 1;
      j = 0;
    }
  }
  //匹配成功,返回模式串p在文本串s中的位置,否则返回-1
  if (j == pLen)
    return i - j;
  else
    return -1; 
}

而KMP算法 , 它利用之前已经部分匹配这个有效信息,保持i 不回溯,通过修改j 的位置,让模式串尽量地移动到有效的位置。还是上面的例子 , 在第一轮匹配中 , i和j走到了下图所示的位置 , 并发现失配 :

在这里插入图片描述

此时i不回溯 , j回溯到子串下标为2的位置 , 继续开始匹配 :

在这里插入图片描述

i和j继续向后走 , 发现j走到了子串的末尾 , 此时在主串中找到了匹配的结果 :

在这里插入图片描述

这么做果然快多了 ! 现在有一个疑问了 , 我前面直接说 , j回溯到子串下标为2的位置 , 此时有两个疑问 :

1.为什么? —> 为什么回溯到这个位置 ?
2.怎么做? —> 如何找到这个位置 ?

此处我们先插播几个概念 , 后面可能会用到 :

  1. 前缀 : 包含首位字符但不包含末位字符的子串
  2. 后缀 : 包含末位字符但不包含首位字符的子串
  3. 相等前后缀 : 就是前缀和后缀一样
  4. 最长相等前后缀 : 一个字符串可能会有多个前缀和后缀相等 , 这里就是指最长的那个 .

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

所以现在的关键问题就是 , 我们需要有这样一个数组 , 通常就叫next数组 , 这个数组记录了当子串中下标为j的字符与主串失配后 , j应该回溯到的位置 . 比如上面的例子中 , 当j = 5 时 , 失配 , 此时next[5] = 2 , 所以j回溯到下标2的位置 , 这个2也是j = 5 之前的子串的最长相等前后缀 .

比如 , 下面这个字符串 , 它的next数组就是这样的 :

在这里插入图片描述

  1. 对于第一个a , 它前面什么都没有 , 所以next[0]默认为-1 ;
  2. 对于第二个a , 它前面是一个a , 而一个a的最长公共前后缀长度是0 , 所以next[1] = 0 ;
  3. 对于第三个b , 它前面是aa , 其最长公共前后缀长度是1 , 所以next[2] = 1 .

这是我们手动求next数组 , 那当然很简单 . 我们现在需要的 , 是用代码实现next数组的求解 , 这就是我们的下一个重点任务 !

三 : 求next数组

定义 : next 数组考虑的是除当前字符外的最长相同前缀后缀 .

初始化 : next[0] = -1 , next[1] = 0 .

遍历顺序 : 从前往后 , 因为求后面的next值是要用到前面的next值的 .

计算 : 假设我们已知next[ j ] = k , 说明 j 之前的字符串的最长相等前后缀长度是 k . 此时我们的目标是求next[ j + 1 ] . 整个字符串为p :

在这里插入图片描述

在这里插入图片描述

我们来详细解说一下 :

在这里插入图片描述
在这里插入图片描述

到这儿就解说得差不多了 , 上面的图片也是帮助大家以形式化方式理解这一操作过程的 . 根据我们的分析 , 写出代码 :

    private void getNext(int[] next, String p) {
        next[0] = -1;//next[0]其实没有意义,
        int pLen = p.length();
        int j = 0;
        int k = -1;//next[j]=k,所以j=0时,k=next[0]=-1
        /*j从0开始,因为while循环中j是先++再赋值,所以j从0开始,后面就是先给1下标位置
        赋值.我们前面分析next[1] = 0,那为什么不直接初始化呢?因为p字符串长度未必大于1,
        在while循环中进行赋值,可以省去一步判断,如果p.length()<=1,那么while就进不来*/
        while(j < pLen - 1) {
        /*一样的原因,后面是给j++进行赋值,所以j=pLen-2时进入循环,
        就给next[pLen-1]赋值了*/
            if(k == - 1 || p.charAt(j) == p.charAt(k)) {
            /*
            当p[j]==p[k],此时next[j+1] = k+1,也就是下面这三行代码
            */
                k++;
                j++;
                next[j] = k;
            } else {
            /*否则,将next[k]赋值给k*/
                k = next[k];
            }
        }
    }

求next数组纯享版 :

    private void getNext(int[] next, String p) {
        int pLen = p.length();
        next[0] = -1;
        int k = -1;
        int j = 0;
        while(j < pLen - 1) {
            if(k == - 1 || p.charAt(j) == p.charAt(k)) {
                k++;
                j++;
                next[j] = k;
            } else {
                k = next[k];
            }
        }
    }

得到next数组了 , 结合我们前面对KMP算法的分析 , 就可以写出寻找子串的代码了 :

    public int strStr(String haystack, String needle) {
    	char[] s = haystack.toCharArray();
        char[] p = needle.toCharArray();
        int i = 0;
        int j = 0;
        int sLen = haystack.length();
        int pLen = needle.length();
        int[] next = new int[pLen];
        getNext(next, needle);
        while(i < sLen && j < pLen) {
            if(j == -1 || s[i] == p[j]) {//匹配成功,i和j同步前进
                i++;
                j++;
            } else {//匹配失败,i不动,j回溯到next[j]位置
                j = next[j];
            }
        }
        if(j == pLen) {//如果j走到末尾,匹配成功,返回主串中匹配成功的初始位置
            return i - j;
        } else {
            return -1;//否则返回-1
        }
    }

这也是力扣这题的答案 .

至此 , KMP算法就整个讲解完了 . 当然KMP算法还有一些优化的策略 , 你大可以查阅其他资料进行进一步学习 . 本文仅仅是最初阶的KMP算法的实现罢了 .


本文到此结束 !

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值