数据结构 kmp字符串匹配_算法学习笔记(13): KMP算法

d7fee2cc3f812bdcd98d9f5717846e6c.png

KMP算法(全称Knuth-Morris-Pratt字符串查找算法,由三位发明者的姓氏命名)是可以在文本串s中快速查找模式串p的一种算法。

要想知道KMP算法是如何减少字符串查找的时间复杂度的,我们不如来看暴力匹配方法是如何浪费时间的。所谓暴力匹配,就是逐字符逐字符地进行匹配(比较s[i]p[j]),如果当前字符匹配成功(s[i]==p[j]),就匹配下一个字符(++i, ++j),如果失配,i回溯,j置为0(i=i-j+1, j=0)。代码如下:

// 暴力匹配
int i = 0, j = 0;
while (i < s.length())
{
    if (s[i] == p[j])
    {
        ++i;
        ++j;
    }
    else
    {
        i = i - j + 1;
        j = 0;
    }
    if (j == p.length())  // 匹配成功,打印结果并回溯
    {
        cout << i - j << endl;
        i = i - j + 1;
        j = 0;
    }
}

举例来说,假如s="abababcabaa", 我们暴力匹配,过程会是怎样?

2abc9ecea5a306dfeb291c5452796565.png

从头开始匹配,第一个字符是a,匹配成功。

5f98357f3edcd18691535fed9649777e.png

第2~4个字符也匹配成功,继续。

a7ec8c14148d0439d818c7beeaad9ba2.png

下一位,匹配失败,回溯。

b0330d79a2c0b27793e30b016a956fa0.png

匹配失败,继续尝试。

fe651d30ea43e37627b0caf53669a052.png

下一位,匹配成功。

aed0e03627f906dbda50f06c4d0590d5.png

就这样一直匹配到结尾。

设两个字符串的长度分别为

,则暴力匹配的最坏时间复杂度是
。究其原因,在于i进行回溯浪费了时间。能不能让i不走回头路呢?然而,如果i不回溯,同时又把
j置为0,很可能会出现缺漏,如下图。

16fcc7c44dd7aea10a7ee8289a66c7e8.png
这样配下去会漏掉一个匹配

于是为了让j被赋为一个合适的值,我们引入了PMT(Partial Match Table,部分匹配表)。


j应该被赋值为多少,是只与模式串自身有关的。每个模式串,都对应着一张PMT,比如"ababcabaa"对应的PMT如下:

9bb1467801cce98fb0680d63e5806f08.png

这是什么意思呢?简单地说,PMT[i]就是,从p[0]往后数、同时从p[i]往前数相同的位数,在保证前后缀相同的情况下,最多能数多少位。(但要小于p的长度)

f73bbb6ea831996fc1588f8ce53e3923.png

专业点说,它是真前缀真后缀的集合之交集中,最长元素的长度。(这里的“真”字与“真子集”中的“真”字类似)

为什么PMT可以用来确定j指针的位置呢?让我们先回到暴力匹配算法第一次失配时的情形:

d4aba404abd91424e32dc00553d35790.png

这时,s中的'a'p中的'c'没有配上,我们计划保持i指针(上面的指针)不变,而把j指针左移。我们注意到,"abab"已经匹配成功了,它拥有一个前缀"ab",以及一个后缀"ab"(虚线部分),所以我们可以把这个"ab"利用起来,变成下面这样:

b6d4d23b32311281315ddc39a086da0b.png

实际上这时我们正是在令j=pmt[j-1]。再举一个例子:

1a04209492e3b63d09124c174cb7177d.png

发生失配,我们令j=pmt[j-1](=3)(也就是符合条件的最长前缀紧接着的下一位):

07101b2e4810d031d1510de210320d94.png

仍不匹配,我们继续:

0ecd092e2ed59af7cd3f9aeb904a064f.png

bcf8b6b02a53e11ac60492bc5379a48f.png

这次取得了成功,继续匹配下去即得答案。

以上这些过程,转换为代码,是这样的:

for (int i = 0, j = 0; i < s.length(); ++i)
{
    while (j >= 0 && s[i] != p[j]) // 失配,不断循环直到匹配成功。如果无论如何都无法匹配成功则得到-1
        j = j ? pmt[j - 1] : -1;
    ++j; // 指针后移一位(如果匹配失败,相当于将指针置于字符串首)
    if (j == p.length()) // 字符串匹配成功,打印结果并回溯
    {
        cout << i - j + 1 << endl;
        j = pmt[j - 1];
    }
}

很多文章中会使用next数组,即把PMT整体向右移一位(特别地,令next[0]=-1),表示在每一位失配时应跳转到的索引。也就是说,失配时,按照i -> next[i] -> next[next[i]] -> ...的顺序跳转。其实原理和实现都是差不多的。


现在问题来了,PMT怎么求?如果暴力求的话,时间复杂度是

,并不理想。神奇的是,我们可以在
错开一位后,让p自己匹配自己(这相当于是用前缀去匹配后缀)。我们知道pmt[0]=0,而之后的每一位则可以通过在匹配过程中记录j值得到。

还是以刚刚的模式串为例:

62cf372beb5aca61fd459c48dee7906e.png

匹配失败,则pmt[1]=-1+1=0i指针后移。

c00a1eb1448f83c5db4654a18101c00a.png

接下来匹配成功,可知pmt[2]=j+1=1,然后将两个指针都右移(其实为了代码简洁,可以先移动指针,即写成pmt[2]=++j

f8b18a914c7d3fd2168e216856b4fad1.png

继续匹配成功,pmt[3]=++j.

91ee76c16038a334be4d5383012468fc.png

下一位失配,因为前面的pmt已经算出来了,我们可以像匹配文本串时那样地使用它。pmt[2-1]pmt[1]等于0,所以退回到开头。

d82af1ad0deafad32771071424f50a11.png

在开头处匹配失败,则pmt[4]=-1+1=0。接下来也按这种方法操作:

a0feacc4278c7654c55f55fd273d03b5.png

最后一位出现失配,这次我们先令j=pmt[j-1]=1

df012980082bbd1059eed70d39e8a7eb.png

b49a7cfed83d5ad7f29a850ecc87e92a.png

再次匹配,匹配成功。自此,我们通过一趟自我匹配,求出了PMT,代码如下:

pmt[0] = 0;
for (int i = 1, j = 0; i < p.length(); ++i)
{
    while (j >= 0  && p[i] != p[j])
        j = j ? pmt[j - 1] : -1;
    pmt[i] = ++j;
}

现在已经可以解决洛谷模板题了:

(洛谷P3375 【模板】KMP字符串匹配)

题目描述 如题,给出两个字符串
,其中
​的子串,求出
中所有出现的位置。
为了减少骗分的情况,接下来还要输出子串的前缀数组 next。 (如果你不知道这是什么意思也不要问,去百度搜 kmp算法 学习一下就知道了。) 输入格式 第一行为一个字符串,即为
。 第二行为一个字符串,即为
输出格式 若干行,每行包含一个整数,表示
​ 在
中出现的位置 接下来 11 行,包括
个整数,表示前缀数组
的值。
#include <bits/stdc++.h>
using namespace std;
int pmt[1000005];
string s, p;
int main()
{
    ios::sync_with_stdio(false);
    cin >> s >> p;
    pmt[0] = 0;
    int cnt = 0;
    for (int i = 1, j = 0; i < p.length(); ++i)
    {
        while (p[i] != p[j] && j >= 0)
            j = j ? pmt[j - 1] : -1;
        pmt[i] = ++j;
    }
    for (int i = 0, j = 0; i < s.length(); ++i)
    {
        while (s[i] != p[j] && j >= 0)
            j = j ? pmt[j - 1] : -1;
        ++j;
        if (j == p.length())
        {
            cout << i - j + 2 << endl; // 根据样例输出,这里要的是1-index数组,所以是加2
            j = pmt[j - 1];
        }
    }
    for (int i = 0; i < p.length(); ++i)
        cout << pmt[i] << ' ';
    cout << endl;
    return 0;
}

(话说,说是输出next数组,其实要输出的是PMT…… )


上面的算法只能称作MP算法,真正的KMP算法还有一个Knuth提出的优化。

例如对于"abababc"这个模式串,如果我们用它来匹配"abababd",在最后处要跳转3次才能发现匹配失败:

41d4812749b7ea00be3d0f44c91a4a75.png

其实中间这几次跳转毫无意义,我们明知道da是不能匹配的,却做了很多无用功。所以我们可以在计算pmt时做一点小改动 :

for (int i = 1, j = 0; i < p.length(); ++i)
{
    while (j >= 0 && p[i] != p[j])
        j = j ? pmt[j - 1] : -1;
    pmt[i] = p[i + 1] == p[j + 1] ? pmt[j++] : ++j;
}

这是什么原理?如下图,这时自我匹配成功了,第i+1位在失配时按理会跳转到第j+1位,但是我们发现第i+1位和第j+1位的字符是一样的。我们知道第j+1位失配后会跳转到第pmt[j+1-1]pmt[j]位,所以我们干脆跳过j+1直接到pmt[j]去。(注意这里是递推的)

5100e7b4b4940c9d3ae2700260c2d504.png

相反,匹配到下图这种情形时,p[i+1]!=p[j+1],就可以像之前那样处理。

815e998ef06bd4d87e2d2f3901259e94.png

其实这样得到的pmt数组已经不符合我们定义的PMT的性质了,如果较真的话可以重新拿一个名为nextval之类的数组存。

KMP算法总的时间复杂度是

,这是因为
++i++j都只进行了
次,虽然
j在过程中有减小,但j在任何时刻不可能小于
,所以
j减小的次数也不可能超过

Pecco:算法学习笔记(目录)​zhuanlan.zhihu.com
【2025亚太杯B题】辐射制冷技术的建模与优化——亚太地区大学生数学建模竞赛(思路、代码、论文持续更新中.......)内容概要:本文档围绕2025亚太杯B题“辐射制冷技术的建模与优化”展开,提供数学建模竞赛所需的思路、代码和论文写作支持,内容持续更新。文档列举了大量科研仿真资源,涵盖智能优化算法、机器学习、路径规划、电力系统、信号处理等多个技术领域,重点服务于数学建模参赛者和技术研究员。资源以Matlab和Python为主要实现工具,包含多种算法在实际问题中的应用案例,如粒子群优化、遗传算法、卡尔曼滤波、深度学习等,并提供配套代码和仿真模型。同时附有网盘链接,便于获取完整资料。; 适合群:参加数学建模竞赛的本科生、研究生,具备一定编程基础(尤其是Matlab/Python)和数学建模能力的科研初学者;从事智能优化、电力系统、信号处理等相关领域的技术员。; 使用场景及目标:①辅助完成亚太杯等数学建模竞赛题目,特别是B题辐射制冷技术的建模与优化;②为科研项目提供算法实现参考,提升仿真效率与模型精度;③学习多种智能算法在工程问题中的具体应用方式。; 阅读建议:建议按目录顺序系统浏览,结合提供的代码实例进行调试与复现,重点关注与自身研究方向相关的模块;充分利用网盘资源,对照思路与代码深化理解,提升建模与编程实战能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值