KMP算法


前言

KMP也是困扰我许久的算法了,道理感觉能懂,但是一到代码就难以理解

但是所幸我现在终于是搞懂了

记一些笔记,希望帮到未来的自己和更多学习的人

前置概念

主串
也叫匹配串、模式串,相当于是要进行搜索的范围

子串
主串中包含的连续的字符

模板串
需要搜索的目标,如果能够在主串中找到与自身相同的子串即为匹配成功

朴素匹配算法
也叫幼稚匹配、简单匹配算法,就是暴力

KMP思路

主串和模板串

比如我们要在如图所示的主串中找到该模板串,

请先不要以计算机的思维去想如何实现,我们先以人类的思维入手——

我们总是会先找以g开头的部分,而不是一个一个去比对:

人类思维匹配示意

如图,我们大概能在进行第三次匹配时成功

总结一下就是:

顺序遍历主串,不回溯

移动模板串并跳过不必要的回溯

(这里的移动实际上就是遍历,移动只是一个利于结合图文进一步理解的说法——毕竟一个字符串能怎么动,还能在内存里面运动起来不成?)

next数组

既然要跳过不必要的回溯,那么该往哪里跳就成了我们要考虑的问题,先来看下面这个例子

KMP思路说明

next数组是要记录“平移的距离”,其实也就是记录出现不匹配时,p指针应该回溯到的位置

那么该如何去求这个数组呢?

其实就是要记录(结合图片把下面这句话好好捋一捋)

出现不匹配的元素前面的 字符串中的 相同的最长前缀后缀

如果理解了这句话,那么也不难发现求next数组只需要观察模板串

举个例子

这里我们习惯是下标从1开始,这样会有很多好处——我们后面再提

而且此处next[i]代表,当第i+1个元素不匹配时指针p应该回溯的位置——所以下标为0是有意义的

元素 a b c a b
下标 i 1 2 3 4 5
next[i] 0 0 0 1 2

手动求取的过程如下:

注意前缀和后缀是串的真子集

下标i 讨论的串 最长相同前后缀 结果
1 a 不讨论 i=1时,若回溯,则一定是到0
2 ab 不匹配则只能重新开始,回溯到0
3 abc 同上
4 abca a 回溯到第一个a,也就是i = 1这里
5 abcab ab 回溯到第第一个b,也就是i = 2这里

其实对于i=5的情况来说已经不用讨论了,因为到了这一步已经是完全匹配上了,但是我们还是把它记录下来

代码实现

const int N = 1e5 + 10;
int next[N];
char t[N];	// 模板串
// 数组下标均从1开始,但根据上文可知next[1]是默认为0的不用求,所以i从2开始
// 而且从2开始相较于从1开始会省去不少麻烦
// 而j从0开始是为了一种试探性的比较
for (int i = 2, j = 0; i <= n; i ++) 
{
    // j + 1是试探性地比较,不匹配则递归回溯直到 匹配或者j为0
	while (j && t[i] != t[j + 1]) 
    {
        j = next[j];	
    }
    // 跳出while的原因有两种,如果是因为匹配而跳出
    // 那么就让试探性的j + 1真正地进一步
    if(t[i] == t[j + 1]) 
    {
        j ++;
    }
    // 可以自己推导一下,在经历上述步骤后,
    // j已经代表了此时讨论的串中最长相同前后缀的长度
    next[i] = j;
}

开始匹配

在求完next数组后

就要开始进行匹配了,匹配的思路 和 求next异曲同工

// i是主串的下标,j+1是模板串下标,j仍可以用来试探next数组
for (int i = 1, j = 0; i <= m; i ++) {
    // S是主串,P是模板串
    while (j && S[i] != P[j + 1]) {
        j = next[j];
    }
    if (S[i] == P[j + 1]) {
        j ++;
    }
    if (j == n) {
        cout << i - n << " ";
        // 我们如果要找出主串中所有的模板串,那么就进行下一步,回溯
        j = next[j];
    }
}

完整代码

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
const int M = 1e6 + 10;
int n, m;
char P[N], S[M];
int ne[N];

int main() {
    cin >> n >> P + 1 >> m >> S + 1;
    for (int i = 2, j = 0; i <= n; i ++) {
        while (j && P[i] != P[j + 1]) {
            j = next[j];
        }
        if (P[i] == P[j + 1]) {
            j ++;
        }
        next[i] = j;
    }

    for (int i = 1, j = 0; i <= m; i ++) {
        while (j && S[i] != P[j + 1]) {
            j = next[j];
        }
        if (S[i] == P[j + 1]) {
            j ++;
        }
        if (j == n) {
            cout << i - n << " ";
            j = next[j];
        }
    }

    return 0;
}

文章作者: Serio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Serio !
  目录