KMP算法理解(参考BILIBILI正月点灯笼)

前言

KMP算法最近看了很多的博客和视频始终没有清楚理解NEXT数组的构建,在B站看到正月点灯笼大神的视频,里面提出的前缀表对我有很大启发,今天来记录分享一下。
BILIBILI 正月点灯笼

定义

string text;//目标字符串
string pattern; //匹配字符串
int i,j;//为两个字符串索引 text[i],pattern[j]
int n,m;//分别为两个字符串长度

算法

KMP框架

在灯神的视频里引入了一个前缀表(Prefix table)的概念来替代Next数组,我觉得更容易理解,在这里前缀的意思我就不赘述了,可以自行百度。
假设待匹配的字符串Pattern=“ABABAAC”。

   0  A
   0  AB
   1  ABA
   2  ABAB
   3  ABABA
   1  ABABAA
   0  ABABAAC

于是我们可以根据每一个字母的前缀数建立一个前缀表,为了方便后面KMP搜索,将其向后移一位,第一位定义为-1,于是有了这样的一个数组

int prefix[] = {-1,0,0,1,2,3,1};

对应到数组为这样的形式
在这里插入图片描述

当我们遍历text字符串时,若发现第j位Pattern不匹配时候我们这样处理(KMP匹配就不赘述了)
在这里插入图片描述
如图,我们在pattern字符串第2位发现A和B不匹配,我们这时候查找前缀表数组,发现第2位对应着是0,这时则将pattern第0位移动到这里j = prefix[j] = 0,如图。

在这里插入图片描述
如此往复,每次发现不匹配则将前缀表对应数字的下标的位置向后移动,如果移动到prefix[0]=-1,则将整个pattern向后移动一位(与第-1位移动到第0位一个意思)
在这里插入图片描述
按这样的模式匹配,当我们pattern遍历完成就可以确定一个匹配的数据,此时若我们还要继续往下匹配,我们将pattern移动到当前前缀表的数字处,即 j = prefix[j]。于是我们就可以写出kmp匹配的代码

void kmp_search(string text, string pattern) {
    int n = pattern.length(), m = text.length();
    int i = 0, j = 0;
    //prefix_table(pattern);
    //move_prefix_table(n);
    while (i < m) {
        if (j == n - 1 && text[i] == pattern[j]) {
            cout << "Find pattern at" << i - j << endl;
            j = prefix[j];
        }
        if (text[i] == pattern[j]) i++, j++;
        else {
            j = prefix[j];
            if (j == -1) i++, j++;
        }
    }
}
  1. 第一个判断为判断当前pattern是否匹配结束
  2. 第二个判断若相等i和j向后移动一位,若不相等如上述按前缀表移动,前缀表数值为-1时移动一位。

前缀表构建

至此,我们已经了解了怎么通过前缀表匹配,现在要来构造前缀表。若是通过暴力枚举,仍然要花费很大的时间复杂度构建前缀表,于是需要通过之前的数据和规律来构造

   0  A
   0  AB
   1  ABA
   2  ABAB
   3  ABABA
   1  ABABAA
   0  ABABAAC

这里我们用整型变量len来表示当前最大前缀长度(这里指的是真前缀,长度小于字符串),用整型变量i来表示当前匹配的位置,如图第一位永远为0,于是len = 0。对于接下来的每一个字符串我们只需要判断新增加的字符pattern[i]是否与pattern[len]相等,若相等则len++;这里仍然存在一个问题,如下图。
在这里插入图片描述
当我们插入第7位‘A’时,此时len=2,i=6,我们发现 pattern[len] != pattern[i],但是插入的‘A’与字符串开头相同,所以这种情况下len不能简单等于0,应该进行如下操作(灯神视频里讲的有些乱可以自己看视频理解下)。

void prefix_table(string pattern) {
    prefix[0] = 0;
    int len = 0;
    int i = 1;
    while (i < pattern.length()) {
        if (pattern[i] == pattern[len]) {
            len++;
            prefix[i] = len;
            i++;
        }
        else {
            if (len > 0) len = prefix[len - 1];
            else prefix[i++] = len;
        }
    }
}

这样我们就完成了前缀表的构建,但记得之前还提到过需要将前缀表向前移动一位利于KMP搜索,于是还需如下函数(在函数中无法动态获取数组大小,于是当做参数传入)。

void move_prefix_table(int n) {
    int i;
    for (int i = n; i > 0; i--) prefix[i] = prefix[i - 1];
    prefix[0] = -1;
}

于是整合全部功能就完成了KMP搜索。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int prefix[N];

void prefix_table(string pattern) {
    prefix[0] = 0;
    int len = 0;
    int i = 1;
    while (i < pattern.length()) {
        if (pattern[i] == pattern[len]) {
            len++;
            prefix[i] = len;
            i++;
        }
        else {
            if (len > 0) len = prefix[len - 1];
            else prefix[i++] = len;
        }
    }
}
//向前移一位,prefix[0] = -1
void move_prefix_table(int n) {
    int i;
    for (int i = n; i > 0; i--) prefix[i] = prefix[i - 1];
    prefix[0] = -1;
}

void kmp_search(string text, string pattern) {
    int n = pattern.length(), m = text.length();
    int i = 0, j = 0;
    prefix_table(pattern);
    move_prefix_table(n);
    while (i < m) {
        if (j == n - 1 && text[i] == pattern[j]) {
            cout << "Find pattern at" << i - j << endl;
            j = prefix[j];
        }
        if (text[i] == pattern[j]) i++, j++;
        else {
            j = prefix[j];
            if (j == -1) i++, j++;
        }
    }
}

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    string text,pattern;
    cin >> text >>pattern;
    kmp_search(text,pattern);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值