算法基础学习笔记----数据结构---KMP算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度为O(m+n)  。

目录

一、思路 

 1.先考虑暴力算法​编辑

2.优化---KMP算法

解释:

二、next()函数

1.定义

代码:

2. 流程

3.模拟执行过程

4.时间复杂度

三、模式串与主串匹配

1.代码

2.流程

3.模拟执行过程

4.时间复杂度

四、重点

五、练习

六、参考


一、思路 

 1.先考虑暴力算法6143e1b42b154fa78c2ee8373e4032b1.png

 在暴力做法中,一旦发现模式串与主串中字符匹配失败,i指针就回退到位置2,j指针回退到位置1,然后再做比较,这种做法时间复杂度为O(m*n),缺点在于浪费了前面已经匹配的信息。

2.优化---KMP算法

 让i指针原地不动,j指针回跳到恰当的位置再做比较,恰当的位置即从前面已经匹配的信息当中,再找出仍然匹配的一段。具体实现就是通过next()函数来实现。函数本身包括了模式串的局部匹配信息。稍后讲解如何构造next函数。

在此便可以体会到KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

 77730df9359b41aba9b69ec634c8ced1.png

 第一轮匹配失败后,

从S串中去掉一个字符往后看,不可行,因为ab和aa不匹配;

从S串中去掉两个字符往后看,不可行,因为b和a不匹配;

从S串中去掉三个字符往后看,发现后面的aabaa与模式串中的前五个字符aabaa相等,这样j指针只需要回跳到位置6,再和主串进行比较即可,这是方案一;

还有另一种方案,继续采用从主串中依次删去字符的方法,从S串中去掉六个字符往后看(去掉四个和五个匹配失败),发现后面的aa和模式串中的前两个字符aa相等,这样j指针需要回跳到位置3,再与主串进行比较。

比较这两种方案,很明显方案一更好,因为用贪心的思想考虑,方案一中找到的相等的匹配段长度更长,越长起始位置就越靠左,如果这个位置后面部分恰好等于模式串,那这就是一组可行解。即使这一位置后面部分与模式串不匹配,那再采用从主串中依次删去字符的方法,在从S串中去掉四个和五个字符匹配失败后,去掉六个字符(即方案二),发现后面的aa和模式串中的前两个字符aa相等,这样j指针需要回跳到位置3,再与主串进行比较。这样的做法就能保证不漏解。

通过比较我们发现,这样的结论:

1.取最长的相等前后缀,可以保证不漏解。

2.通过模式串前后缀的自我匹配的长度,计算next函数,给j指针打一张表,匹配失败时就跳到next[j]的位置继续匹配。

解释:

1.前缀和后缀:例如:p = "abcab",这里考虑的前缀和后缀都是真前缀/真后缀(前后缀长度不能等于模式串自身长度),其前缀为从首字符开始不包含尾字符的子串,即"a","ab","abc","abca",后缀为从尾字符开始不包含首字符的子串,即"b","ab","cab","bcab"

2.在找最长相等前后缀时,完全可以把主串抛开,因为在匹配失败前,主串和模式串相等,那么如果能在主串中找到一段和模式串中前缀相等的匹配段,则一定也能找到与模式串前缀相等的模式串后缀。因此可以通过模式串前后缀的自我匹配的长度,来计算next函数,给j指针打一张表,当主串与模式串在任何位置匹配失败时,j指针都能回调到next[j]的位置继续匹配。

二、next()函数

1.定义

next函数:next[i]表示模式串P[1,i]中相等前后缀最长长度

暴力模拟一遍:

 右侧数字为需要枚举的次数,加和算得暴力解法的时间复杂度为O(n²),时间复杂度太高还需要再进行优化。

仔细观察,发现规律;

1.next函数值如果增加,那么它只能在上一步的基础上至多增加1;

2.next函数值如果减少,例:next[8] = 5,当增加一个字符至next[9]时,发现匹配失败,那就可以从前面匹配成功的五个字符中,再找出一段相等前后缀,aabaa中最长相等前后缀为aa,因此可以利用前面的next[5] = 2来计算next[9]。

这样无论是增加还是减少都能找到规律,那么就可以进行优化。

代码:

ne[1] = 0;
for (int i = 2, j = 0; i <= n; i ++ )//n为模式串长度
{
    while (j && p[i] != p[j + 1]) j = ne[j];//预判发现匹配失败,j就回跳到能匹配的位置,如果找不到能匹配的位置,j就回跳到0,退出while循环
    if (p[i] == p[j + 1]) j ++;//匹配成功,j指针后移一位,指向匹配前缀的末尾
    ne[i] = j;
}

2. 流程

双指针:i 扫描模式串, j扫描前缀。

初始化,ne[1] = 0, i = 2, j = 0;

每轮for循环,i向后走一步。

1.若p[i] != p[j + 1],j 就回跳到能匹配的位置,如果找不到能匹配的位置,j就回跳到0,退出while循环。

2.若p[i] == p[j + 1],让 j + 1,j指针后移一位,指向匹配前缀的末尾

3.next[i] 等于 j 的值。

3.模拟执行过程

 *j反映 j 指针回跳的情况。

不带*反映i指针和j指针每轮循环后最后移动到的位置。

利用p[i] 与p[j + 1]作比较,就是做一个预判,如果匹配,j指针就向右移动一位(进可攻),如果不匹配,就回跳到能匹配的位置(退可守),如果找不到能匹配的位置(退无可退),j 回跳到0,并退出while循环。

回跳:退而求其次,类似于套娃。

用以往匹配失败的信息,构造next()函数,尽量减少j指针回退的步数,从而减少模式串与主串的匹配次数。

4.时间复杂度

主要考虑while循环次数,即j指针走了多少步。j指针所走的总步数就决定了总的执行次数。每轮for,j至多+1,那么j一共向右至多走n步,往左跳的步数加起来最多不会超过n步,否则j变为负数,故j的总步数不会超过2n。例:模式串为aaa.....ab(长度为n),i扫描模式串,j扫描前缀,i = 2, j= 1,i = 3, j = 2,....i指向n, j指向倒数第三个字符a,j != 0, p[i] != p[j + 1],j = ne[j],进入while循环从倒数第三个位置一步一步往回跳,这样计算j的总步数也不会超过2n.因此时间复杂度为O(n).

三、模式串与主串匹配

此过程与求next函数过程很相似,可以结合记忆。

1.代码

for (int i = 1, j = 0; i <= m; i ++)//m为主串长度
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++;
    if (j == n) printf("%d\n", i - n + 1);
}

2.流程

双指针:i 扫描主串, j扫描模式串。

初始化,i = 1 , j = 0;

每轮for循环,i向后走一步。

1.若s[i] != p[j + 1],j 就回跳到能匹配的位置,如果找不到能匹配的位置,j就回跳到0,退出while循环。

2.若s[i] == p[j + 1],让 j + 1,j指针后移一位

3.若匹配成功,输出匹配位置。//如果要输出所有匹配位置,此处还需要再回跳,即加一行 j = ne[j];

3.模拟执行过程

 *j反映 j 指针回跳的情况。

不带*反映i指针和j指针每轮循环后最后移动到的位置。

4.时间复杂度

主要考虑while循环次数,即j指针走了多少步。j指针所走的总步数就决定了总的执行次数。每轮for,j至多+1,那么j一共向右至多走m步,往左跳的步数加起来最多不会超过m步,否则j变为负数,故j的总步数最多为2m。例:主串为aaa.....a(长度为n),模式串为ab,i扫描主串,j扫描模式串,i = 1, j = 1; i = 2, j = 0;i = 3, j = 1; i = 4, j = 0,.....j指针随着i指针向前走一步,再往后跳一步,循环往复,所以时间复杂度为O(m).

四、重点

1.next[i] 表示模式串p[1, i]中相等前后缀的长度。

2.双指针 i指针不回退, j指针来回跑。

3.时间复杂度为:O(m +n)。

五、练习

给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 P 在模式串 S 中多次作为子串出现。

求出模板串 P 在模式串 S 中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

数据范围

1≤N≤1e5
1≤M≤1e6

输入样例:

3
aba
5
ababa

输出样例:

0 2

 答案代码:

#include <iostream>
#include <stdio.h>

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

int n, m;
int ne[N];
char p[N], s[M];


//时间复杂度为O(m + n)
int main()
{
    cin >> n >> p + 1 >> m >> s + 1;//这样输入可以使字符串下标从1开始,便于执行操作
    
    //求next数组过程
    for (int i = 2, j = 0; i <= n; i ++ )//next[1] = 0;直接从2开始
    {
         while (j && p[i] != p[j + 1]) j = ne[j];
         if (p[i] == p[j + 1]) j ++;
         ne[i] = j;
    }
  
    //kmp匹配过程
    for (int i = 1, j = 0; i <= m; i ++ )
    {
         while (j && s[i] != p[j + 1]) j = ne[j];
         if (s[i] == p[j + 1]) j ++;
         if (j == n)
         {
             printf("%d ", i - n);//模板中是i - n + 1,但此题下标从0开始,故i - n + 1 - 1 = i - n
             j = ne[j];//求出模板串 P 在模式串 S中所以出现的位置的起始下标,找到一个位置后退回去再找一个
         }
    }

    return 0;
}

六、参考

1.b站--董晓算法 182 KMP 算法_哔哩哔哩_bilibili

2. AcWing算法基础课

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值