字符串匹配|kmp笔记
很久之前学的了。
我很懒,不太喜欢画图。
做个笔记回忆一下:
kmp
朴素比对字符串
所谓字符串匹配,是这样一种问题:“字符串 T 是否为字符串 S 的子串?如果是,它出现在 S 的哪些位置?” 其中 S 称为主串;T 称为模式串。如在字符串s abcabcabcabd
中找到子串T abcabd
:
先设两个指针i、j,i表示S的指针,j表示T的指针 i=j=0 ↓(i) abcabcabcabd abcabd ↑(j) 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) . . . ↓ abcabcabcabd abcabd ↑ c≠d,回溯(i=1,j=0) ↓ abcabcabcabd abcabd ↑ b≠a,回溯(i=2,j=0) . . . ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ . . . ↓ abcabcabcabd abcabd ↑ 匹配成功,找到模式串(print(i))
优化
上面的复杂度是 O(nm) ,为什么这么多,发现是回溯花费时间过多。我们合理的希望是i不回溯,即:
先设两个指针i、j,i表示S的指针,j表示T的指针 i=j=0 ↓(i) abcabcabcabd abcabd ↑(j) 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) . . . ↓ abcabcabcabd abcabd ↑ c≠d,i不回溯,因为ab已经匹配完了,所以我们跳到上一个ab的位置(j=2) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) ↓ abcabcabcabd abcabd ↑ a≠d,i不回溯(j=2) ↓ abcabcabcabd abcabd ↑ 匹配成功,移动指针(i++,j++) . . . ↓ abcabcabcabd abcabd ↑ 匹配成功,找到模式串(print(i)) 全程i不会减少
nxt数组
我们假设知道一个叫做nxt的数组,代表下一个j,当匹配失败时就可以 j=nxt[j] 来防止i的回溯。那么我们可以快速算出他的子串,如下代码:
int KMP(){ for(int i=0,j=0;i<n;i++){ while(j>0 && str[i]!=pnt[j]){ j=nxt[j-1]; // 为什么是 nxt[j-1],因为第j位和第i位已经不匹配了,j-1位和i-1位才是匹配的,所以用j=nxt[j-1] } if(str[i]==pnt[j]){ j++; // 匹配成功 } if(j==m){ // 匹配成功 return i-j+1; } } return -1; }
nxt数组是什么
nxt代表重复真子集长度,和回文串差不多,但不是回文串。区别
回文串:abccba 重复真子集:abcabc
欸,那么我们可以看出当已经有不匹配:
↓ abcabcabcabcd abcabcd ↑
因为前面的abc
已经匹配完了,我们不需要回溯回去再匹配,只需要跳到上一个abc的位置就行了。
↓ abcabcabcabcd abcabcd ↑
我们nxt储存的就是与它重复的这部分的位置。以 abcababdabc
为例:
a:0(因为是真子集,不包括自身) ab:0 abc:0 _ _ abca:1 __ __ abcab:2 _ _ abcaba:1 __ __ abcabab:2 abcababd:0 _ _ abcababda:1 __ __ abcababdab:2 ___ ___ abcababdabc:3
那么我们会发现,他们重复这部分的下标(以0开始)刚好就是重复真子集长度:
有S=abcabcabd T=abcabd 当匹配到: ↓ abcabcabd abcabd ↑ 时,说明前面的ab已经配好了,我们移动到上一个也有ab的地方: ↓ abcabcabd abcabd ↑ 即可成功匹配
计算nxt数组
我们可以用递推的思想,先设有nxt[0]=0(必然的),然后设有快指针i=1,慢指针j=0,刚好,我们会发现重复部分的长度也是j的值。
对于匹配成功,则j++
对于匹配失败,则从上一位nxt中找到重复部分回溯j。
看不懂就看一下计算过程吧
计算abcabdabcabc的nxt,ij定义同上,上面箭头表示i,下面箭头表示j ↓(i) abcabdabcabc ↑(j) 不相同,故nxt[i(1)]=0 ↓(i++,下不再阐述) abcabdabcabc ↑ 不相同,故nxt[i(2)]=0,j不变(因为j是0,不必回溯) ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(3)]=1 ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(4)]=2 ↓ abcabdabcabc ↑ 不相同,故j回溯到nxt[j-1(1)]的重复长度(0) ↓ abcabdabcabc ↑ 无法再回溯,nxt[i(5)]=0 ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(6)]=1 ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(7)]=2 ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(8)]=3 ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(9)]=4 ↓ abcabdabcabc ↑ 相同,故j++,nxt[i(10)]=5 ↓ abcabdabcabc ↑ 不相同,故j回溯到nxt[j-1(4)]的重复长度(2) ↓ abcabdabcabc ↑ 发现相等,j++,nxt[i(11)]=j=3 遍历完成,退出
代码如下:
void makeNext(){ nxt[0]=0; for(int i=1,j=0;i<m;i++){ while(j>0 && pnt[i]!=pnt[j]){ j=nxt[j-1]; // 因为nxt表示重复部分的下标,我们可以回溯回去 } if(pnt[i]==pnt[j]){ j++; } nxt[i]=j; } }
代码:
#include<cstdio> #include<cstring> #include<string> char str[1010],pnt[1010]; int n,m; int nxt[1010]; void makeNext(){ nxt[0]=0; for(int i=1,j=0;i<m;i++){ while(j>0 && pnt[i]!=pnt[j]){ j=nxt[j-1]; } if(pnt[i]==pnt[j]){ j++; } nxt[i]=j; } } int KMP(){ for(int i=0,j=0;i<n;i++){ while(j>0 && str[i]!=pnt[j]){ j=nxt[j-1]; } if(str[i]==pnt[j]){ j++; } if(j==m){ return i-j+1; } } return -1; } int main(){ scanf("%s %s",str,pnt); n=strlen(str); m=strlen(pnt); makeNext(); printf("%d",KMP()); }
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现