【算法】KMP算法学习

【算法】KMP算法学习


由三位大佬D.E.Knuth、J.H.Morris、V.R.Pratt同时发现,取三人名字命名KMP


其实就是一种改进的字符串匹配算法,关键是利用匹配后失败的信息,尽量减少模式串(W)与主串(T)的匹配次数以达到快速匹配的目的。

KMP算法利用已经部分匹配这个有效信息,保持 i 指针不回溯,通过修改 j 指针,让模式串尽量地移动到有效的位置

孤~影 博客

为什么位置 k 是 j 需要回溯的位置?(废话一样的证明)

参考如下例子:

主串: a b c a b c d h i j k

模式串: a b c e

当匹配到主串的第四个字符串a时,可知a和e不相等,因此需要移向下一位,但其实我们并不需要从模式串中的第一位重新开始比较。因为主串中的前三个字符串已经没有匹配的a了,所以不可能成功。

在这里插入图片描述

那么重点在于,当某一个字符与主串不匹配时,应该如何知道 j 指针要移动到哪里去呢?

当匹配失败时, j 要移动的下一个位置 k(模式串中的位置k) ,存在性质:最前面的 k 个字符和 j 之前的最后 k 个字符是一样的

数学公式表示一下:

P[0 ~ k-1] == P[j-k ~ j-1]

当 T[i] != P[j]

T[i-j ~ i-1] == P[0 ~ j-1]

由 P[0 ~ k-1] == P[j-k ~ j-1]

得: T[i-k ~ i-1] == P[0 ~ k-1]

就是说我们当前指针之前的模式串与主串元素都已经一一匹配的前提下,在指针i、j处发生了不匹配,那么只需模式串指针j回溯到位置k,这个位置k保证模式串的前k个元素即P[0 ~ k-1]与主串位置i的前k个元素匹配即T[i-k ~ i-1] 这样就不用模式串从头再比了,节省了时间提高了效率。这段话我自己加的,简直废话,是我学习的心得

由上述可知为啥那么可以将 j 移动到 k 而无需再比较前面的 k 个字符。

那么 如何求得这个 k 值呢? (前缀表)

因为在P的每一个位置都可能发生不匹配,也就是需要计算每一个位置 j 对应的 k 。(就是说其实在模式串中任意位置j发生不匹配时回溯的位置k都是定好的,每个对应的这个位置k取决于模式串本身)所以可以用一个数组 next 来进行保存,next[j] = k,表示当T[i] != P[j] 时, j 指针的下一位置。next数组就是KMP算法中的核心前缀表

贴一段经典代码:

public static int[] getNext(String ps) {
    char[] p = ps.toCharArray();
    int[] next = new int[p.length];
    next[0] = -1;
    int j = 0;
    int k = -1;
    while (j < p.length - 1) {
       if (k == -1 || p[j] == p[k]) {
           next[++j] = ++k;
       } else {
           k = next[k];
       }
    }
    return next;
}

首先初始化

在这里插入图片描述

这种情况下, j 为0,这时候不匹配,但是j已经是在最左边了,不能再移动了,所以这时应该是 i 指针后移,所以代码中会出现next[0] = -1,这一步的目的是初始化。

在这里插入图片描述

那么当 j 为1的时候,指针 j 是移到P[0]位置的,因为再往前已经没有位置了。

发现

当P[k] == P[j]时,有next[j+1] == next[j] + 1。

在这里插入图片描述

证明:

  • 因为P[j]之前已经有P[0 ~ k-1] == P[j-k ~j-1] 即next[j] == k
  • 此时P[k] == P[j] 那么P[0 ~ k] == P[j-k ~ j] 即 next[j + 1] == k + 1 == next[j] + 1

当P[k] != P[j]时,有 k = next[k]。

在这里插入图片描述

表现在代码上就是k = next[k] 如下图

在这里插入图片描述

写KMP:

public static int KMP(String ts, String ps) {
    char[] t = ts.toCharArray();
    char[] p = ps.toCharArray();
    int i = 0; // 主串的位置
    int j = 0; // 模式串的位置
    int[] next = getNext(ps);
    while (i < t.length && j < p.length) {
       if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0
           i++;
           j++;
       } else {
           // i不需要回溯了
           // i = i - j + 1;
           j = next[j]; // j回到指定位置
       }
    }
    if (j == p.length) {
       return i - j;
    } else {
       return -1;
    }
}

缺陷:

在这里插入图片描述

此时的next数组得到[-1,0,0,1],所以此时回溯到第1个元素

在这里插入图片描述

但是其实没什么意义,因为后面的B已经不匹配了,那么前面的B也一定是不匹配的,其根本原因就在于P[j] == P[next[j]];

实话实说,则段看的不是很明白

现在看明白了,意思是如果当前元素与该位置对应的位置k的元素一致的话,那么既然现在位置j都不匹配,那么回溯之后也一定是不匹配的,所以加判断,直接把next数组中k位置的的数赋给当前位置j

所以追加一个判断条件 完整代码:

public static int[] getNext(String ps) {
    char[] p = ps.toCharArray();
    int[] next = new int[p.length];
    next[0] = -1;
    int j = 0;
    int k = -1;
    while (j < p.length - 1) {
       if (k == -1 || p[j] == p[k]) {
           if (p[++j] == p[++k]) { // 当两个字符相等时要跳过
              next[j] = next[k];
           } else {
              next[j] = k;
           }
       } else {
           k = next[k];
       }
    }
    return next;
}

摘自博客孤~影的博客-KMP算法详解

说实话,,看完这篇 还是有点懵的,悟性有点差,再找找别的大佬。


代码随想录

前缀表的求取思路以及多种情况

匹配时找的是最长相等前后缀即当P[j] != T[i]时,T[i]的前k个字符与已经匹配的P[0~k]个字符是已经匹配的,所以只要回溯一小部分即可,前缀表 next[] 数组记录的就是模式串各个字符的最长相等前后缀

什么是前缀?

以aabaaf为例

前缀是包含首字符不包含尾字符的所有子串

  • a
  • aa
  • aab
  • aaba
  • aabaa

都是前缀,前缀不包含尾字符

什么是后缀?

相对的后缀是只包含尾字符不包含首字符的所有子串

最长相等前后缀

子串最长相等前后缀
a0
aa1
aab0
aaba1
aabaa2
aabaaf0

得到序列:0 1 0 1 2 0 也就是前缀表next[],或称prefix[]。

为什么叫next[]数组? 因为这个数组记录了需要回退到哪里。如果按第一篇博客中的方式,得到的是 -1 0 -1 0 1 -1 这点区别不涉及原理性的东西,只是实现方式的不同,实现的话哪一种都可以。

针对子串 a a b a a f next[]数组的三种情况

next[]遇见冲突 匹配规则
0 1 0 1 2 0匹配前一位字符的位置
-1 0 1 0 1 2匹配当前字符的位置
-1 0 -1 0 1 -1匹配前一位字符的位置+1

求next[]数组思路

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况
  4. 更新next[]数组

指针j指向前缀末尾位置,指针i指向后缀末尾位置

这里 j 并不仅仅是指向前缀末尾位置,代表了包括 i 的之前子串的最长相等前后缀!

  void getNext(int* next, const string& s) {
        int j = 0;
        next[0] = 0;//初始化0的位置一定只能回退到0
        for(int i = 1; i < s.size(); i++) {
            while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下表的操作
                j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了 为什么要回退到前一位?因为这个位置记录的是包括j位置的之前所有子串中出现的最长相等前后缀长度(dp思想?),也就是当前i位置发生不匹配时需要回溯的位置。那么因为这个回溯有可能是多步进行的,所以使用while循环保证一直回溯到满足条件的位置
            }
            if (s[i] == s[j]) {
                j++;
            }
            next[i] = j;
        }
    }

上述是前缀表不整体-1操作的数组。

void getNext(int* next, const string& s) {
    int j = -1;
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回溯
        }
        if (s[i] == s[j + 1]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

上述是前缀表整体-1操作的写法。

两种写法实质上都已经包含了next数组的全部信息,即在任意位置发生冲突时,需要回退到的对应位置k的信息。因此都可以使用,只是对应不同的next数组构建方式,需要使用不同的处理方法。

摘自 代码随想录的B站视频,carl大神就是大神,又理解了。


宫水三叶的题解

说实话光看原理不做题,肯定是学不会的。贴上一段LC题解大神宫水三叶的题解,图文并茂,更适合我这样的呆子。🤣

图解模式串next[]数组构建方法。

假设有匹配串 aaabbab 看next数组是怎样被构建出来的

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

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

摘自 【宫水三叶】简单题学 KMP 算法 图解让我印象更深刻了,感谢各位大佬。


可能还是有错误的地方,以后有了新的心得会再更正的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值