KMP算法:从暴力到优化的讲解

KMP算法:从暴力到优化的讲解

笔者已被折磨好久:(

一、暴力匹配:

假设我们要在主串S中找模式串P的位置(比如在"ABABC"中找"ABC"),暴力比较直接

暴力匹配步骤:

  1. 主串指针i和模式串指针j都从0开始
  2. S[i] == P[j],则i++,j++比较下一个
  3. 若不匹配,则i退回到上一次开始的下一位,j直接重置为0(模式串从头开始)
  4. 重复直到j走完模式串(匹配成功)或者i走完主串(匹配失败)

暴力匹配的问题:效率低

例如当主串为S = AAAAAAB,模式串为P = AAAB:

  • 前三个"A"都能成功匹配,但第四个S[3] = "A" vs P[3] = "B"不匹配
  • 暴力使i回退到1,j重置为0,再重新比较(但前两个"A"已经匹配过,浪费了时间),时间复杂度最差为O(n*m)(n为主串长度,m为模式串长度)

二、KMP算法的优化:

优化方式:主串指针i不回退,只通过调整模式串指针j来复用已经匹配过的信息

关键问题:j该回退到哪里

S[i]!=P[j]时,已经匹配的部分是P[0...j-1],我们需要找到这个部分中最长的相同的前后缀子串j回到这个前缀子串的结尾,这样前面的字符就不用重复比较了

例如:

  • 模式串P = "ABABC",已经匹配的部分P[0...3] = ABAB,它的最长相同前后缀是"AB",长度为2
  • S[i]P[4]不匹配,j直接回退到2而不是0,因为"AB"已经匹配过了(next[4] = 2)

三、next数组:记录"最长相同前后缀"长度

next[i] = j的含义:模式串(如果下标从1开始)P[1...i]中最长相同前后缀的长度为j,即P[1...j] = P[i-j+1...i]

“前后缀”:

  • 前缀:从开头算起,不包含最后一个字符的子串(如"ABC"的前缀是"A""AB"
  • 后缀:从结尾算起,不包含第一个字符的子串(如"ABC"的后缀是"C""BC"
  • 最长相同前后缀:前缀和后缀中内容相同、长度最长的那个(如"ABAB"的前缀"AB"和后缀"AB"相同,长度2)

next数组示例(模式串P="ABABC"):

j(模式串位置)P[1..j](子串)最长相同前后缀next[j](长度)
1“A”无(不能包含自身)0
2“AB”前缀"A"≠后缀"B"0
3“ABA”前缀"A"=后缀"A"1
4“ABAB”前缀"AB"=后缀"AB"2
5“ABABC”前缀"AB"≠后缀"BC"0

所以next = [0,0,1,2,0]

四、KMP匹配过程:

主串S="ABABXABABC",模式串P="ABABC"next=[0,0,1,2,0]

匹配步骤:

  • 前4步跳过,i=4j=4时,S[4]="X" != P[4]="C"
  • next[j-1] = next[3] = 2j回退到2(不用回退到0!)
  • 现在比较S[4]="X" vs P[2]="A"(不匹配)
  • next[j-1] = next[1] = 0j回退到0
  • S[4]="X" vs P[0]="A"(不匹配) → i直接+1(i=5),j保持0
  • 继续后续比较…

五、求next数组

next数组只和模式串有关,用“已知推未知”:

  1. next[0] = 0(第一个字符没有前后缀)
  2. j >= 1,假设已知next[j-1] = k(即P[0..k-1] == P[j-k..j-1]
  3. 比较P[j]P[k]
    • 若相等:next[j] = k + 1(最长相同前后缀延长1)
    • 若不等:让k = next[k-1](找更短的相同前后缀),重复比较,直到k=0
    • k=0仍不等:next[j] = 0

P="ABABC"的next数组:

  • j=0next[0] = 0
  • j=1k = next[0] = 0P[1]="B" vs P[0]="A"不等 → next[1] = 0
  • j=2k = next[1] = 0P[2]="A" vs P[0]="A"相等 → next[2] = 0+1=1
  • j=3k = next[2] = 1P[3]="B" vs P[1]="B"相等 → next[3] = 1+1=2
  • j=4k = next[3] = 2P[4]="C" vs P[2]="A"不等 → k=next[1]=0,仍不等 → next[4] = 0

六、例题:模式串多次出现的位置查询

以下给出题目链接:

https://www.acwing.com/problem/content/833/

题目描述

给定一个字符串 S 和一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。模式串 P 在字符串 S 中多次作为子串出现,求出模式串 P 在字符串 S 中所有出现的位置的起始下标(下标从 0 开始计数)。

输入格式

  1. 第一行输入整数 N,表示字符串 P 的长度。
  2. 第二行输入字符串 P
  3. 第三行输入整数 M,表示字符串 S 的长度。
  4. 第四行输入字符串 S

输出格式

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

数据范围

1 < N < 1e5,1 < M < 1e6

题解
//超级无敌KMP算法 
//本题j 始终代表着匹配成功的下标数
#include<iostream>
#include<cstdio>
#include<cstring>

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

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

int main()
{
    cin>>n>>p+1>>m>>s+1;  //为了好处理字符串  下标都从1开始记录
    
    
    /*ne数组预处理  与匹配极为相像*/
    for(int i = 2,j = 0;i<=n;i++)  //由于ne[1]为1但意味着都是重新开始匹配 所以直接从i = 2开始匹配
    {
        while(j && p[i]!=p[j+1]) j = ne[j];  //只要j没有退回原位那么匹配失败则都返回其最大前缀的位置
        if(p[i] == p[j+1])  j++;  
        ne[i] = j;  //记录下当前 前i个字符的最长连续相等的前后缀长度 即p[1..j] = p[i-j+1....i]
    }
    
    
    /*字符串匹配*/
    for(int i = 1,j = 0;i<=m;i++)  //由于一开始可能就有子串   因此i = 0
    {
        while(j && s[i]!=p[j+1]) j = ne[j];  //此处意义与预处理相同
        if(s[i] == p[j+1]) j++;
        if(j == n)  //不同在这里  匹配要求找到全部完全匹配的字串未止  而预处理只需将所有ne初始化 不在意j的长度
        {
            printf("%d ",i-n);  //若完全匹配  则在母串中的位置是i-n  本题由0开始 因此此处不用+1
            j = ne[j];  //因为可能不止一个完全匹配的子串   所以移动到下一个方便匹配位置开始继续重复
        }
    }
    return 0;
    
}
终于写完了 笔者花了好几天的时间琢磨这个KMP算法 在折磨的同时感叹数学的美 笔者结合AI和各类题解作以上总结 对KMP算法的理解还很浅薄 希望在疑问中继续成长
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值