KMP算法:从暴力到优化的讲解
笔者已被折磨好久:(
一、暴力匹配:
假设我们要在主串S中找模式串P的位置(比如在"ABABC"中找"ABC"),暴力比较直接
暴力匹配步骤:
- 主串指针
i和模式串指针j都从0开始 - 若
S[i] == P[j],则i++,j++比较下一个 - 若不匹配,则
i退回到上一次开始的下一位,j直接重置为0(模式串从头开始) - 重复直到
j走完模式串(匹配成功)或者i走完主串(匹配失败)
暴力匹配的问题:效率低
例如当主串为S = AAAAAAB,模式串为P = AAAB:
- 前三个"A"都能成功匹配,但第四个
S[3] = "A"vsP[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=4,j=4时,S[4]="X" != P[4]="C" - 查
next[j-1] = next[3] = 2→j回退到2(不用回退到0!) - 现在比较
S[4]="X" vs P[2]="A"(不匹配) - 查
next[j-1] = next[1] = 0→j回退到0 S[4]="X" vs P[0]="A"(不匹配) →i直接+1(i=5),j保持0- 继续后续比较…
五、求next数组
next数组只和模式串有关,用“已知推未知”:
next[0] = 0(第一个字符没有前后缀)- 对
j >= 1,假设已知next[j-1] = k(即P[0..k-1] == P[j-k..j-1]) - 比较
P[j]和P[k]:- 若相等:
next[j] = k + 1(最长相同前后缀延长1) - 若不等:让
k = next[k-1](找更短的相同前后缀),重复比较,直到k=0 - 若
k=0仍不等:next[j] = 0
- 若相等:
求P="ABABC"的next数组:
j=0:next[0] = 0j=1:k = next[0] = 0,P[1]="B" vs P[0]="A"不等 →next[1] = 0j=2:k = next[1] = 0,P[2]="A" vs P[0]="A"相等 →next[2] = 0+1=1j=3:k = next[2] = 1,P[3]="B" vs P[1]="B"相等 →next[3] = 1+1=2j=4:k = next[3] = 2,P[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 开始计数)。
输入格式
- 第一行输入整数
N,表示字符串P的长度。 - 第二行输入字符串
P。 - 第三行输入整数
M,表示字符串S的长度。 - 第四行输入字符串
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;
}
1003

被折叠的 条评论
为什么被折叠?



