KMP算法

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

KMP是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母

几个关键的概念

  • 前缀表(prefix table):记录某些信息的表
  • next数组:KMP算法的前缀表(专门用来记录有关回退匹配信息的表)

如何计算前缀表

  1. 长度为前1个字符的子串a,最长相同前后缀的长度为0
  2. 长度为前2个字符的子串aa,最长相同前后缀的长度为1(a与a重复)
  3. 长度为前3个字符的子串aab,最长相同前后缀的长度为0(a!=b)
  4. 长度为前4个字符的子串aaba,最长相同前后缀的长度为1(a=a)
  5. 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2(aa=aa)
  6. 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0(b!=f)
    在这里插入图片描述
    模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀

如何使用前缀表(next数组)

在这里插入图片描述
这个就是next数组的作用->如果是之前的话,这里匹配失败后主串和子串都需要回去重新来进行匹配,但是基于next数组,我们可以少匹配很多,而不是O(M*N)的时间复杂度->主串一直在向右进行,而子串则是在不匹配的时候进行一定程度上的回滚(如下图)
在这里插入图片描述
如果这里的图解释的不够清楚的话,看【最浅显易懂的 KMP 算法讲解】https://www.bilibili.com/video/BV1AY4y157yL?vd_source=29280a6402e27c696a32363167333156的动图讲解吧

next数组的生成原理

#include <vector>
#include <string>

using namespace std;

void getNext(int* next, const string& s) {
    // 1. 初始化
    int i = 0; // i代表前缀长度(也指向前缀的下一个字符)
    int j = 1; // j代表后缀末尾位置(从1开始,因为next[0]固定为0)
    
    next[0] = 0; // 初始化第一个位置

    // 2. 循环构建
    while(j < s.size()) {
        if(s[i] == s[j]) {
            // --- 匹配成功的情况 ---
            i++;        // 前缀长度增加
            next[j] = i;// 记录当前后缀位置j对应的最长公共前后缀长度
            j++;        // 后缀指针向后移,处理下一个字符
        } 
        else {
            // --- 匹配失败的情况 ---
            if (i > 0) {
                // 核心:如果i>0,说明前面还有匹配,根据next数组回退
                // 注意:这里不要 j++,回退后要用新的 s[i] 和当前的 s[j] 再比一次
                i = next[i-1]; 
            } else {
                // i已经回退到0了,还是不匹配
                next[j] = 0; // 认命,长度为0
                j++;         // 只能去处理下一个字符了
            }
        }
    }
}

这里还需要注意一下,我的这种next数组的生成方式是PMT(Partial Match Table)的,有的教材会基于PMT进行一定程度上的改变,比如0->-1、0->1这种左右移动

这是生成next数组的方法,下面来一步步的讲讲为什么
(注意这里的i和j和我代码里面有点区别,我这里表述的是从1开始的)

  1. 如果当前的长度为2,如果左边的i+1处和右边的j+1处的值相同,那么next[j+1]不就等于next[j]+1了么,等于了的话,那么就记录当前的i值到对应的next数组的位置上去
    在这里插入图片描述

  2. 但是后来再走一点点的话,因为匹配 C 失败了,我们不能拥有长度为 4 的公共前后缀。我们退而求其次,看看能不能找到更短一点的在这里插入图片描述

  3. 因为匹配失败了因此我们就需要回退
    因为大前缀 ABA 的尾巴是 A,大后缀 ABA 的尾巴也是 A
    结论:我们可以确信,当前字符串的结尾一定有一个 A 是匹配开头的 A 的(第一个字符和最后一个字符都是A这是一定的)

  4. 因此我们不需要重比第一个 A,直接试图去匹配第二个字符(我们将“期望匹配长度”从 3 降级(回退)到 next[3-1](即 next[2])的值,也就是 1)→然后现在我们就要继续进行匹配了,左边的i+1处的B和右边的j+1处的B匹配成功了,因此next就等于i+1(1+1=2)
    在这里插入图片描述
    注意清楚了呀,这里的i从原来的3->4进发失败了被回退到了3->1,因此左边只有一个A,所以这里进行匹配判断的是两个B

当然这里还有一个例子:

状态:
字符串: a  a  b  a  a  f
下标:   0  1  2  3  4  5
         ^     ^        ^
    j=2 (前缀结尾)      i=5 (后缀结尾)

比较 s[i] ('f') 和 s[j] ('b') -> 不等!我们需要缩小 j。
查 next[j-1] 即 next[1]。
next[1] 对应 "aa",它的 next 值是 1。
这意味着:虽然 "aab" 匹配不上,但前面的 "aa" 里还有一个长度为 1 的公共前后缀 "a"。所以 j 变成 1。

再次比较 s[i] ('f') 和 s[j] (s[1]'a') -> 还是不等!
继续回退:j = next[j-1] = next[0] = 0,j 变成了 0。

比较 s[i] ('f') 和 s[0] ('a') -> 还是不等!j 已经是 0 了,没法退了。所以 next[5] = 0

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值