kmp算法(详解)

在日常生活中我们常常会遇见在一篇文章中找关键词的事情。如果用程序来解决你会怎么做???
ps:假设文章字段为t, 关键词为p

暴力解法:

枚举文章中的每一个点,然后往后匹配是非为关键字???
就像这样, 挨个匹配每个字符。
kmp算法(详解)

int lent = strlen(t);
int lenp = strlen(p);
for(int i = 0; i < lent; i++)
{
    int flag = 1;//是否成功匹配
    if(i + j >= lent) break;
    for(int j = 0; j < lenp; j++)
    {
        if(t[i+j] != p[j])//匹配失败
        {
            flag = 0;
            break;
        }
    }
    if(flag) printf("%d", i-lenp+1);//输出匹配成功了的位置
}

显然复杂度是o(n^2)的,会耗费大量的时间。

KMP算法:

ps:从下表为1得位置开始储存p和t。(这样会更加方便)
暴力算法之所以耗费大量的时间,是因为常常会发生这样的现象:经过千辛万苦匹配到了p的最后一个字符,结果这时候不匹配了,于是乎向右移动一格,然后重头开始匹配。

nex数组:
那么如果我们可以使得在匹配失败后向右移动很长的一段距离,那么就可以节省大量的时间了。
由此我们为p定义一个nex数组,nex[i]的含义为nex[i]的含义为以p[i]j结尾的后缀,能够匹配的从1开始非平凡前缀的最大坐标, 非平凡的意思是不能是p本身。也就是说,找到最大的长度,p的前缀和以p[i]为结尾的后缀相同。
例子:
比如对于abab
nex值分别为0 0 1 2
对于aaaa
nex值分别为0 1 2 3

算法过程:
如果现在正在尝试匹配t[i] 和 p[j+1],如果这个时候匹配失败,那么可以直接令j = nex[j], 然后继续判断t[i] 是否等于p[j+1], 直到他们相等或者j为0。
t[i] == p[j+1] 的意思已经通过nex[j]找到了了一个前缀仍然匹配了t以t[i-1]结尾的后缀,此时t[i] == p[j+1], 说明对于j+1已经匹配成功了,可以开始下一个字符的匹配了。
j == 0的意思是这个时候p无法继续向右移动了,已经到头了,然后也可以不需要继续匹配了。

匹配的代码:

void kmp()
{
    int lent = strlen(t+1);
    int lenp = strlen(p+1);
	//尝试用t[i]与p[j+1]进行匹配
    for(int i = 1, j = 0; i <= lent; i++)
    {
        while(j && t[i] != p[j+1]) j = nex[j];
        if(t[i] == p[j+1]) j++;
        if(j == lenp)
        {
            printf("%d\n", i-lenp+1);//输出成功匹配的起始下表
            j = nex[j];
        }
    }
}

假设现在在ABABABC上匹配ABA(下标都从1开始)
ABA的nex值为0 0 1
初始时i == 1, j == 0(因为用t[i] 与 p[j+1]进行匹配, 这样初始化表示从p和t第一个字符开始匹配)
匹配过程:
i == 1, j == 0 : t[i] == p[j+1]
i == 2, j == 1 : t[i] == p[j+1]
i == 3, j == 2 : t[i] == p[j+1] (输出下表1并j = nex[j])
i == 4,j == 1 : t[i] == p[j+1]
i == 5, j == 2: t[i] == p[j+1] (输出下表3并j = nex[j])
i == 6, j == 1: t[i] == p[j+1]
i == 7, j == 2: t[i] != p[j+1] (j = nex[j])
i == 7, j == 1: t[i] != p[j+1] (j = nex[j])
i == 7, j == 0: 跳出循环
结束匹配
最终i == 8, j == 0

注意项: 为什么nex的值时最长的前缀,而不能是比最长要小一点,因为j = nex[j]其实是相当与把p直接向右移动一大段距离,而是的nex值越长,其实是为了移动的距离小一点,防止造成匹配的遗漏

构建nex数组:

nex数组的构建与匹配过程基本相同,我们是通过从下标1开始遍历,逐个获取每个位置的nex值,也就是说当我们需要求nex[i]时, nex[1] 到nex[i-1] 的值都已经求到了,如果我们可以用i之前的nex值求得i得nex值,那就可以了。。那么根据nex的定义,如果现在要求nex[i], 而j在每一次迭代后等于nex[i-1]。
说明现在以j结尾得前缀是仍然是和i-1结尾得后缀相同得,如过现在p[i]等于p[j+1],则nex[i] 等于j+1,否则继续j = nex[j]。
如果最后j = 0, 那么说明找不到一个长度至少为1得前缀与以p[i]为后缀的字符串相同, 此时nex[i] 等于0。
例如:
求ABABABC的第六个字符nex值。
前五个nex值为0 0 1 2 3
根据nex[5]我们可以找到一个前缀和与下标5为后缀相同,也就是ABA, 因为第四位恰好时B与第六位相同, 那么就找到了最大的前缀。所以nex[6]为4。

下面是代码

void getnex()
{
    int lenp = strlen(p+1);
    for(int i = 2, j = 0; i <= lenp; i++)
    {
        while(j && p[i] != p[j+1]) j = nex[j];
        if(p[i] == p[j+1]) j++;
        nex[i] = j;
    }
}

模板题
例题:传送门
解析:

#include <bits/stdc++.h>

using namespace std;
const int N = 1e6 + 10;
char t[N], p[N];
int nex[N];

void getnex()
{
    int lenp = strlen(p+1);
    for(int i = 2, j = 0; i <= lenp; i++)
    {
        while(j && p[i] != p[j+1]) j = nex[j];
        if(p[i] == p[j+1]) j++;
        nex[i] = j;
    }
}

void kmp()
{
    int lent = strlen(t+1);
    int lenp = strlen(p+1);

    for(int i = 1, j = 0; i <= lent; i++)
    {
        while(j && t[i] != p[j+1]) j = nex[j];
        if(t[i] == p[j+1]) j++;
        if(j == lenp)
        {
            printf("%d\n", i-lenp+1);
            j = nex[j];
        }
        printf("%d %d\n", i , j);
    }
}

int main()
{
    scanf("%s%s", t+1, p+1);
    getnex();
    kmp();
    int lenp = strlen(p+1);
    for(int i = 1; i <= lenp; i++) printf("%d ", nex[i]);
}

这题有点小难。
例题:传送门
解析:

#include <bits/stdc++.h>

using namespace std;
const int N = 5e6+10;
int nex[N];
char t[N], p[N];
int now;

void get_nex()
{
    int len = strlen(p+1);
    for(int i = 2, j = 0; i <= len; i++)
    {
        while(j && p[i] != p[j+1]) j = nex[j];
        if(p[i] == p[j+1]) j++;
        nex[i] = j;
    }
}

int kmp(char *p, char *t)
{
    int j = 0;
    int lent = now;
    int lenp = strlen(p+1);
    int st = max(lent - lenp+1, 1);
    for(int i = st; i <= lent; i++)
    {
        while(j && t[i] != p[j+1]) j = nex[j];
        if(t[i] == p[j+1])
        {
             j++;
        }
    }
    return j;
}

int main()
{
    int n;
    scanf("%d", &n);
    scanf("%s", t+1);
    now = strlen(t+1);
    for(int i = 2; i <= n; i++)
    {
        scanf("%s", p+1);
        get_nex();
        int j = kmp( p, t);
        int lenp = strlen(p+1);
        for(int i = j + 1; i <= lenp; i++) t[++now] = p[i];//从最终重复起始位置挨个把字符复制进t中
        t[now+1] = '\0';
    }
    printf("%s", t+1);
    return 0;
}

上一篇:centos7.3 快速安装 mariadb(mysql)


下一篇:KMP-匹配所有模式串