串
![![[Pasted image 20250713032527.png]]](https://i-blog.csdnimg.cn/direct/e62ab3b7306a456e9bc3aa794196c4f7.png)
字符串匹配与暴力算法
主串与模式串
字符串匹配是计算机科学中最古老、 研究最广泛的问题之一。
一个字符串是一个定义在有限字母表∑上的字符序列。 例如, ATCTAGAGA 是字母表∑ = {A,C,G,T}上的一个字符串。 字符串匹配问题就是在一个大的字符串 S 中搜索某个字符串 P 的所有出现位置。 在这个问题中, S 又被称为主串, P 又被成为模式串。
![![[Pasted image 20250713032617.png]]](https://i-blog.csdnimg.cn/direct/20a624fafe3844be9ab8e3b1e8a04b71.png)
暴力算法
一种平凡的思路是模式串和主串从主串起始位置开始匹配, 若发生匹配失败, 则模式串向后移动 1 位再进行比较, 直到匹配成功或主串的每个起始位置都已经被尝试过(匹配失败) 。
![![[Pasted image 20250713032650.png]]](https://i-blog.csdnimg.cn/direct/1385c1d3904c433c9e44a15fa07b097e.png)
如上图中, 对于模式 P = aab 和主串 T = acaabc, 将模式 P 沿着 T 从左到右滑动, 逐个比较字符以判断模式 P 在主串 T 中是否存在。
显然, 假设主串的长度为 n, 模式串的长度 m, 那么显然, 这个算法的时间复杂度为O(nm), 当 m 与 n 同阶时, 复杂度可能达到 O(n²)。
Kmp 算法
求解 next 数组的口诀
- 求出模式串的部分匹配值;
- 将部分匹配值右移一位;
- 在首位字符处补-1;
注: 若是选择题, 需要观察选项的 next 数组是从-1 开始的还是 0 开始的, 若是从 0 开始的需要将刚才求出的 next 数组中所有的值+1 即可得到答案;
下面着重讲解部分匹配值的求法;
先给出相关定义:
字符串前缀: 字符串最后一个字符之外的所有头部子串;
字符串后缀: 字符串第一个字符之外的所有尾部子串;
部分匹配值: 字符串的前缀和后缀的最长相等前后缀长度;
比如字符串”aabaa”, ’a’的前后缀都是空, ’aa’的前后缀分别是’a’和’a’, 那么此处的部分匹配值就是 1, …, 以此类推, 最后整个字符串的前缀有{a,aa,aab,aaba}后缀有{abaa,baa,aa,a},最长的相等前后缀长度就是 2
![![[Pasted image 20250713033200.png]]](https://i-blog.csdnimg.cn/direct/89903a2c3d3c47a3a76490ba56a36180.png)
然后将该部分匹配值表的内容统一右移一位(舍去最右边那一位):
![![[Pasted image 20250713033211.png]]](https://i-blog.csdnimg.cn/direct/ba722d8f812d45dd9ef7f7b675a87333.png)
在首位补-1, 即可得到 next 数组:
![![[Pasted image 20250713033220.png]]](https://i-blog.csdnimg.cn/direct/51527935b83f4d1d8feadffa4df0d9ab.png)
最终就求得了字符串 aabaa 的 next 表-1,0,1,0,1, 若需要从 0 开始就整体+1, 即 0,1,2,1,2
练习题
主串是 ababbababa,模式串‘ababa’ 的 next 数组是? 在使用 kmp 算法匹配时, 一共发生了几次失配?
![![[Pasted image 20250713033525.png]]](https://i-blog.csdnimg.cn/direct/5ee902827ad44ef7918f2fef34e0e5a2.png)
![![[Pasted image 20250713033540.png]]](https://i-blog.csdnimg.cn/direct/ab8c2d7aef5a449c8e328c8aeebfebb0.png)
失配时模式串指针回退到P[next[j]],P[next[4]] = P[2]
![![[Pasted image 20250713033603.png]]](https://i-blog.csdnimg.cn/direct/a1b0ce8fca844a6a915155183bd54747.png)
P[next[2]] = P[0]
![![[Pasted image 20250713033811.png]]](https://i-blog.csdnimg.cn/direct/a50aef2953234ac3b51c46180f997b26.png)
P[next[0]] = P[-1]
主串和模式串指针都右移一位
kmp算法
失配分析
![![[Pasted image 20250713180616.png]]](https://i-blog.csdnimg.cn/direct/c9f8e0c3a1124cd89bf5bd7f85afd4c1.png)
匹配成功必要条件: T.suffix == T.pre
若T.suffix == T.pre : 直接从S[j]和P[j]处开始比较即可
j’的含义:
当模式串P在P[j]处与S[i] 发生失配时,将P[j]移动到与s[j]对齐的位置继续匹配,P右移j-j'长度
在kmp算法中,用next[j]来替换这里的j’
next[j]的含义
![![[Pasted image 20250713181102.png]]](https://i-blog.csdnimg.cn/direct/d74fffb9cd3f4db982bf6661484e0659.png)
![![[Pasted image 20250713181118.png]]](https://i-blog.csdnimg.cn/direct/661197491147406088dfb97815add3d0.png)
![![[Pasted image 20250713181134.png]]](https://i-blog.csdnimg.cn/direct/654bdb107be4418ab76ce056750dbaf9.png)
![![[Pasted image 20250713181148.png]]](https://i-blog.csdnimg.cn/direct/bdd4f2a5864a401495d126fd0cfbd797.png)
![![[Pasted image 20250713181213.png]]](https://i-blog.csdnimg.cn/direct/5f16356b1291400d89f6a6ace9db403e.png)
显然:
在多个可选的T.pre == T.suffix中,只有选择较长的pre和suffix,才能确保滑动距离最短,满足所有情况。但是,pre和suffix不能取T,否则不能移动
构建next表
![![[Pasted image 20250713181252.png]]](https://i-blog.csdnimg.cn/direct/cae140357a43440bbfac109ed709a183.png)
next[0]=-1:
当P[0]处失配时,需要将P移过当前位置,等效为将P[-1]与S[j]对齐。
![![[Pasted image 20250713181332.png]]](https://i-blog.csdnimg.cn/direct/4e7003f097644f7a8eb24b3442e9236c.png)
next[2]=1:
当P[2]处失配时,匹配成功部分最长,可重叠前后缀长度为1。
![![[Pasted image 20250713181409.png]]](https://i-blog.csdnimg.cn/direct/49d8756d2eac421d8512b495613f8a0b.png)
next[3]=0:
匹配成功部分最长,当P[3]处失配时,可重叠前后缀长度为0。
![![[Pasted image 20250713181443.png]]](https://i-blog.csdnimg.cn/direct/f7d42a4b81c84abf8d0fda51f4ea1632.png)
next[4]=1:
当P[4]处失配时,匹配成功部分最长,可重叠前后缀长度为1。
![![[Pasted image 20250713181513.png]]](https://i-blog.csdnimg.cn/direct/aec69f252f3c40b89307552047ac13a5.png)
next[5]=2:
当P[5]处失配时,匹配成功部分最长,可重叠前后缀长度为2。
![![[Pasted image 20250713181633.png]]](https://i-blog.csdnimg.cn/direct/dce90d9aee0d4a24904106797bea6522.png)
next[6]=2:
当P[6]处失配时,匹配成功部分最长,可重叠前后缀长度为2。
![![[Pasted image 20250713181729.png]]](https://i-blog.csdnimg.cn/direct/0eb02e8e9ba54e1db38af596a2f8fca8.png)
next[7]=3:
匹配成功部分最长,当匹配成功时,可重叠前后缀长度为3。
next[] = [-1,0,1,0,1,2,2,3]
注意:上面的next表数组是从0开始计数的,当数组从1开始计数,需要整体加1:
next[] = [0,1,2,1,2,3,3,4]
考试时根据选项next表第一个元素是-1还是0判断
主算法
![![[Pasted image 20250713182001.png]]](https://i-blog.csdnimg.cn/direct/7841cc290dd940edad3788a769d35a1b.png)
失配时:
j = 5 , next[5] = 2,因此j = next[5] = 2
![![[Pasted image 20250713182038.png]]](https://i-blog.csdnimg.cn/direct/8eeaa854a56d4e71a03dd50fac7c391c.png)
失配时:
j = 5 , next[5] = 2, 因此j = next[5] = 2
![![[Pasted image 20250713182111.png]]](https://i-blog.csdnimg.cn/direct/713476b28e5c426eae230c43fe1059d7.png)
继续依次比较,可以匹配成功。
int KMP(char * S,char * T){
int next[10];
Next(T,next);//根据模式串T,初始化next数组
int i = 0; //i表示主串指针
int j = 0; //j表示模式串指针
while(i < strlen(S) && j < strlen(T)){
if(j == -1 || s[i] == T[j]){
i++;
j++;
}
else{
j = next[j];
//如果测试的两个字符不相等,i不动,j变为当前模式串的next值
}
}
if(j >= strlen(T)){//如果条件为真,说明匹配成功
return i -(int)strlen(T);
}
return -1;
}
改进版kmp算法
![![[Pasted image 20250713182530.png]]](https://i-blog.csdnimg.cn/direct/4ccbd6ddf99044798e22c318f175c6a8.png)
j=6处发生失配,原因:
P[j] = ‘a’ ≠ S[i]
按朴素kmp算法: j = next[j] = 3
![![[Pasted image 20250713182557.png]]](https://i-blog.csdnimg.cn/direct/876b521b3c1840118e9c4098fefe1a07.png)
此时发现,P[j]依然等于’a’。 此次匹配注定也是失败的。
因此,若P[next[j]] == P[j],这种替换也是无效的,所以在前面构建next[]表的基础上,还必须加入限制条件:
P[next[j]] ≠ P[j]
![![[Pasted image 20250713182636.png]]](https://i-blog.csdnimg.cn/direct/07363dbac36e48ce9423103612c253f6.png)
nextval[0] = -1:这一点和构建next表方式是一样的
![![[Pasted image 20250713182651.png]]](https://i-blog.csdnimg.cn/direct/54a3251cdac34d7db05bac45c4027e24.png)
nextval[1] = -1:next[0] = 0,但是由于P[0] == P[1],所以不能选用0。
![![[Pasted image 20250713182713.png]]](https://i-blog.csdnimg.cn/direct/c72942a26b2b46219b4fda2c516085fc.png)
nextval[2] = 1:匹配成功部分为”aa”,因此最长可匹配的前后缀为”a”和”a”,长度为1,且P[1]!=P[2].
![![[Pasted image 20250713182736.png]]](https://i-blog.csdnimg.cn/direct/b0481b555005405f9d7ecd6d877750fd.png)
nextval[3] = -1:匹配成功部分为”aab”,因此最长可匹配的前后缀为长度为0,且P[0] == P[3],所以不能选用该方案,退而选择nextval[3] = -1 。
![![[Pasted image 20250713182809.png]]](https://i-blog.csdnimg.cn/direct/e6a585072e6048e5ad9c8483a7171144.png)
nextval[4] = -1:匹配成功部分为”aaba”,因此最长可匹配的前后缀为“a”和”a”,长度为1,且P[1] == P[4],所以不能选用该方案,又因为P[0] == P[4],所以nextval[4]!=0。
![![[Pasted image 20250713182839.png]]](https://i-blog.csdnimg.cn/direct/ddb79049135d47e39937e0ccb991a102.png)
nextval[5] = 1:匹配成功部分为”aabaa”,因此最长可匹配的前后缀为“aa”和”aa”,长度为2,又因为P[2] == P[5],所以不能选用该方案,继续寻找备选,次长可匹配前后缀为’a’和’a’,长度为1,而且P[1]!=P[5],所以nextval[5] = 1
![![[Pasted image 20250713182910.png]]](https://i-blog.csdnimg.cn/direct/acf58ae4d89c44ba8c9580ab12cd46ed.png)
nextval[6] = -1:匹配成功部分为”aabaab”,因此最长可匹配的前后缀为“aab”和”aab”,长度为3,又因为P[3] == P[6], 所以不能选用该方案,继续寻找备选,次长可匹配前后长度为0,而且P[0] == P[6],所以nextval[5]!=0,因此nextval[6] = -1。
显然,nextval[7] = next[7] = 1
快速构建next表
![![[Pasted image 20250713183105.png]]](https://i-blog.csdnimg.cn/direct/9a6a6a6d378e44f689c68eeb8d67cb43.png)
本质就是找到一个合适的移动距离,使得P的前缀和P的后缀可以匹配
![![[Pasted image 20250713183050.png]]](https://i-blog.csdnimg.cn/direct/bdbdbbf10a4a4fb3b27a662f4afbf272.png)
若P[j-1] == P[next[j-1]]
![![[Pasted image 20250713183138.png]]](https://i-blog.csdnimg.cn/direct/c8cf8779a6434bd8b54d8c40aec7684b.png)
则next[j] = next[j-1] + 1
![![[Pasted image 20250713183205.png]]](https://i-blog.csdnimg.cn/direct/8eab0d1b1c3248df8581ce9467bc6914.png)
若P[j-1] != P[next[j-1]]
![![[Pasted image 20250713183222.png]]](https://i-blog.csdnimg.cn/direct/486d2ca218424fd0a88a1d911e384cc9.png)
则继续比较P[j-1]与P[next[next[j-1]]
void Next(int* next,char P[])
{
int i = 0, j = -1;
next[0] = -1;
while(i < strlen(P))
{
if(j == -1 || P[i] == P[j])
{
next[i + 1] = j+1;
i++;
j++;
}
else
{
j = next[j];
}
}
}
15万+

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



