通俗易懂的KMP算法「附图文解析」

1. BF算法

BF「BruteForce」算法,是普通的字符串匹配模式算法

主串和子串一一比较

  • 相等:比较下一个
  • 不相等:主串回退到上次比较起始下标的下一位;子串回退到0下标

返回第一个匹配相等的下标

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9eRFIjE-1651715043045)(/Users/cxf/Desktop/MarkDown/images/BF.png)]

package KMP;

public class Main {
    public static int BF(String str, String sub) {
        if (str == null || sub == null) {
            return -1;
        }
        int strLen = str.length();
        int subLen = sub.length();
        int i = 0, j = 0;
        while (i < strLen && j < subLen) {
            if (str.charAt(i) == sub.charAt(j)) {
                ++i;
                ++j;
            } else {
                // 回退:由于要回退到本次查询的下一下标,所以+1
                i = i - j + 1;
              	// j回退到 0
                j = 0;
            }
        }
        if (j >= subLen) {
            return i - j;
        }
        return -1;
    }

    public static void main(String[] args) {
        System.out.println(BF("ababcabcdabcde", "abcd"));
        System.out.println(BF("ababcabcdabcde", "abcde"));
        System.out.println(BF("ababcabcdabcde", "abcdef"));
    }
}

// 运行结果
5
9
-1

2. KMP算法

KMP是一种字符串匹配模式的改进算法

算法核心是:利用匹配失败后的信息,尽量减少主串与子串的匹配次数已达到快速匹配的目的。具体就是通过一个 next[]数组 的实现。

**区别:**主串 i 并不回退,子串 j 也不回退到 0 下标

2.2. 主串为什么不回退

目前在 2下标匹配失败 str[2] != sub[2]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o8h2oCML-1651715043046)(/Users/cxf/Desktop/MarkDown/images/主串为什么不回退.png)]

因此按照 BF 算法,主串str 就算回退到 1下标,也是没有必要的

为此大佬们发明出了主串不回退,子串回退的解决方案,那么子串该如何回退呢?

2.3. 子串回退的位置

目前在 5下标匹配失败 str[5] != sub[5]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AlsWBcXd-1651715043047)(/Users/cxf/Desktop/MarkDown/images/子串匹配失败主串不回退.png)]

举个例子,此时匹配失败了。我们不回退 主串str,因为走到这一步说明 主串的i下标之前和子串的j下标之前前面是有一部分相同的 不然下标不可能走到 5

下面是子串回退的最理想位置,子串 j 直接一步回退到 2下标。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aN4zvHB7-1651715043048)(/Users/cxf/Desktop/MarkDown/images/子串回退.png)]

问题又来了——该如何回退到 2下标呢?为此大佬们提供了一个 next数组

2.4. 引出 next 数组

KMP的精髓就在于 next数组:next[i] = k 不同的 i 对应一个 K 值,这个 K 就是将来要移动 i 的位置

求K值方法:

规则:找到匹配成功部分的两个真子串「不包含本身」,一个以下标 0 开始,一个以下标 i-1 字符结尾

不管什么数据,next[0] = -1,next[1] = 0,因为 0 下标之前没有匹配的字符所以规定是-1,1 下标之前也只有一个字符,但不包含本身所以是 0

求下面的 next 数组

next数组1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fozc2uQL-1651715043048)(/Users/cxf/Desktop/MarkDown/images/next数组1匹配步骤.png)]
省略掉后边的匹配步骤,最终 next数组结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCRhGfcx-1651715043049)(/Users/cxf/Desktop/MarkDown/images/next数组1.png)]

next数组2这里直接写出结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d56zOgnK-1651715043049)(/Users/cxf/Desktop/MarkDown/images/next数组2.png)]

看懂这两幅图的关键就是死扣概念:0下标起始,i-1下标结束「就是说0下标起始的字符串终止要和i-1下标字符串相等才行,千万不要异想天开的越过0下标去匹配i-1下标」

能看懂这里,说明已经对 next 数组怎么来的问题不大了。接下来的问题就是 一直next[i]=k,如何求 next[i+1]

如果我们能通过 next[i]经过一些列转换得到next[i+1],那我们就能实现这一部分。

具体该如何做呢?

刚才两个 next 数组感觉学会的朋友可以继续来看这个数组「主要用验证next[i]=k」来检测自己的 next 数组是否掌握会了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lF5MiMUd-1651715043050)(/Users/cxf/Desktop/MarkDown/images/next数组3.png)]

next[8] = k = 3,为何 next[9] = k = 4 呢?「假设 next[i]=k 是成立的。」

那么 i的回退就是k=3,回到 3 下标,此时来到了 a

我们发现 字符串S[0, 3-1] 和 字符串S[5, 8-1]

也就是:S[0, k-1] 和 S[x, i-1] 相等「下图中红色圈圈部分」

根据字符串长度相等可得:k-1-0 = i-1-x --> x = i-k

换算后就是:S[0, k-1] 和 S[i-k, i-1] 相等

由之前的next[8] 我们再看 next[9]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LwWJQ4ob-1651715043050)(/Users/cxf/Desktop/MarkDown/images/next数组3_1.png)]

上图中绿色部分 S[k] = S[i]

根据上边换算后的公式推导即可扩展为

S[0, k-1] == S[i-k, i-1]

由于 S[k]==S[i]

所以可得:S[0,k] == S[i-k,i]

一切的成立都是源于 next[i]=k

结合上述分析,我们再来看最后一步的推导步骤

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PvQaQja0-1651715043051)(/Users/cxf/Desktop/MarkDown/images/综合上述分析.png)]

就有个推论:next[i+1]=k+1

思考: 如果 S[k] != S[i] 那么 next[i+1]=

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZEajCgq-1651715043052)(/Users/cxf/Desktop/MarkDown/images/next数组3_2.png)]

  • 首先看第一步回退,i 根据 next[5]=2 退到 2 下标

  • 发现 S[2] != S[5]

  • i 再继续根据 next[2]=0 回退到 2下标

  • 发现 S[0] == S[5]

    • 后续就可以继续 next[i+1] = k+1

回退规律:根据 next数组,一直回退到 S[k] == S[i]

看懂了上述的解析,就已经了解 60% 的KMP了「后续还有一个代码的实现和 next 数组的优化等着你来学习」

Java代码实现

/**
 * @param str 主串
 * @param sub 子串
 * @param pos 从子串的 pos 位置开始匹配
 * @return 找到子串在主串当中的下标
 */
public static int KMP(String str, String sub, int pos) {
    if (str == null || sub == null) return -1;
    int lenStr = str.length();
    int lenSub = sub.length();
    if (lenStr == 0 || lenSub == 0) return -1;
    if (pos < 0 || pos >= lenSub) return -1;
    int next[] = new int[lenSub];
    // 得到 next 数组
    getNext(sub, next);
    int i = pos;// 遍历主串
    int j = 0;// 遍历子串
    while (i < lenStr && j < lenSub) {
        if (str.charAt(i) == sub.charAt(j)) {
            ++i;
            ++j;
        } else {
            j = next[j];
        }
    }
    if (j >= lenSub) {
        return i - j;
    }
    return -1;
}

private static void getNext(String sub, int[] next) {
    next[0] = -1;
    next[1] = 0;
    // i从第三个数开始,提前走了一步
    int i = 2;
    // k值从第二个开始:代表前一项
    int k = 0;
    // 遍历子串
    for (; i < sub.length(); i++) {
        if (k == -1 || sub.charAt(i - 1) == sub.charAt(k)) {
            // S[k] == S[i]
            next[i] = k + 1;
            ++k;
            ++i;
        } else {
            // S[k] != S[k]:K一直回退到 S[k] == S[i] 为止
            k = next[k];
        }
    }
}

public static void main(String[] args) {
    System.out.println(KMP("ababcabcdabcde", "abcd", 0));
    System.out.println(KMP("ababcabcdabcde", "abcde", 0));
    System.out.println(KMP("ababcabcdabcde", "abcdef", 0));
}
// 运行结果
5
9
-1

C语言代码实现

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

void getNext(const char *sub, int *next){
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int k = 0;
    int lenSub = strlen(sub);
    for(; i < lenSub; ++i){
        if(sub[i-1] == sub[k]){
            next[i] = k+1;
            ++i;
            ++k;
        }else{
            k = next[k];
        }
    }
}

int KMP(const char *str, const char *sub, int post){
    assert(str != NULL && sub != NULL);
    int lenStr = strlen(str);
    int lenSub = strlen(sub);
    int i = post;
    int j = 0;
    int *next = (int *)malloc(sizeof(int) * lenSub);
    assert(next != NULL);
    getNext(sub, next);
    while(i < lenStr && j < lenSub){
        if(str[i] == sub[j]){
            ++i;
            ++j;
        }else{
            j = next[j];
        }
    }
    if(j >= lenSub){
        return i-j;
    }
    return -1;
}

int main(){
    printf("%d\n", KMP("ababcabcdabcde", "abcd", 0));
    printf("%d\n", KMP("ababcabcdabcde", "abcde", 0));
    printf("%d\n", KMP("ababcabcdabcde", "abcdef", 0));
    return 0;
}

代码写到这里,细心的朋友可能会发现套路:这不就是BF算法基础上加了一个 getNext(subStr, next[]) 蛮!

确实是有个小套路!

代码套路:

KMP(str, sub, pos){
  int lenStr=str.length();
  int lenSub=sub.length();
  int i=pos;
  int j=0;
  int[] next;
  getNext(sub, next);
  while(i < lenStr && j< lenSub){
    if(str[i] == sub[j]){
      ++i;
      ++j;
    }else{
      /*
      BF算法:主串回退下一下标;子串回退0
      i = i-j+1;
      j=0;
      
      KMP算法:主串不移动;子串回退 next[j]
      */
      j = next[j];
    }
  }
}

getNext(sub, next){
  int i=2;// next下标:从第三个开始
  int k=0;// next值从第二个开始
  for(; i<sub.length(); ++i){
    // next[i] = k --> next[i+1] = k+1:由于 i 从第三个开始,所以要 -1「这里就是一句上一个 next 根据sub[i-]==sub[k]推出下一个 next[i]的k值」
    if(sub[i-1] = sub[k]){
      next[i] = k+1;
      ++k;
    }else{
      k = next[k];
    }
  }
}

2.5 next数组的优化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ukQ5JDWX-1651715043052)(/Users/cxf/Desktop/MarkDown/images/next数组的优化.png)]

原有的 next数组 回退会发现不能一步到位,回退之后还需要进一步的回退,所以就对next数组做了修改。

细心的朋友可能会专研:为什么会一直倒退呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmTYRruM-1651715043053)(/Users/cxf/Desktop/MarkDown/images/1.png)]

假设在 7下标 遇到了匹配失败,则它需要一步步不回退到

7回到 6,6回5,5回4。。。回到0

因此:

  • 如果回退到的位置和当前字符一样,就写则回退到位置的 nextVal 值
  • 如果回退到的位置和当前字符不一样,就写当前字符原来的值

练习 nextVal

模式串 abcaabbcabcaabdab,该模式串的 next 数组的值为( D ) , nextval 数组的值为 (F) 。

A. 0 1 1 1 2 2 1 1 1 2 3 4 5 6 7 1 2
B. 0 1 1 1 2 1 2 1 1 2 3 4 5 6 1 1 2
C. 0 1 1 1 0 0 1 3 1 0 1 1 0 0 7 0 1
D. 0 1 1 1 2 2 3 1 1 2 3 4 5 6 7 1 2
E. 0 1 1 0 0 1 1 1 0 1 1 0 0 1 7 0 1
F. 0 1 1 0 2 1 3 1 0 1 1 0 2 1 7 0 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-858MGOXQ-1651715043054)(/Users/cxf/Desktop/MarkDown/images/nextVal练习题.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gl2th3VU-1651715043055)(/Users/cxf/Desktop/MarkDown/images/拜谢.jpeg)]

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值