文章目录
什么是字符串的模式匹配
在主串 N 中,查找 M 子串。(N>>M 主串的长度可能远大于子串的长度)

两种模式匹配算法
字符串模式匹配 :在主串中找到与模式串相同的子串,并返回其(第一个元素)所在位置。
子串 是主串的一部分,一定存在(于主串中);
模式串,不一定能在主串中找到。
第一种:朴素模式匹配算法(BF)
主串长度为 n,模式串长度为 m。
朴素模式匹配算法:将主串中所有长度为m的子串 (最多对比n-m+1个子串)依次与模式串对比,直到找到一个完全匹配的子串(返回其第一个元素的位置位序),或将所有的子串都不匹配为止。
算法思想:"枚举所有可能的位置,逐一尝试匹配。"
- 主串长度为
n,模式串长度为m。 - 算法从主串的第
1个字符开始,截取长度为m的子串与T比较。 - 若匹配,则返回当前位置;若不匹配,则从主串的下一个位置重新开始比较。
- 直到主串剩余长度不足
m,则匹配失败返回0。
方式一:定位操作的方式实现
cpp
// 定位操作
int Index(SString S, SString T) { // S为主串,T为子串
int i = 1;
int n = S.length, m = T.length; // 获取串的长度
SString Sub; // 用于暂存子串
// 最多对比 n-m+1个子串
while (i <= n - m + 1) { // 边界条件:剩余长度必须 >= m
// 取出从位置i开始,长度为m的子串
SubString(Sub, S, i, m); // 调用已实现的求子串 取出长度为m的子串
// 子串和模式串对比,若不匹配,则匹配下一个子串
if (StrCompare(Sub, T) == 0) {
return i; // 匹配成功
}
i++; // 不匹配则往后移动一位继续查找
}
return 0; // 未找到 S中不存在与T相等的子串
}
方式二:数组下标实现朴素模式匹配算法
不使用字符串的基本操作 ,直接通过数组下标实现朴素模式匹配算法。
cpp
int Index(SString S, SString T) {
int i = 1, j = 1; // i 遍历主串S, j 遍历子串T
// 当i>S.length则结束循环
while (i <= S.length && j <= T.length) {
if (S.ch[i] == T.ch[j]) { // 1. 匹配成功,指针后移继续匹配
++i; ++j;
} else { // 2. 匹配失败,i 回溯,j 重置
i = i - j + 2; // 核心回溯公式
j = 1;
}
}
if (j > T.length) // 3. 子串全部匹配完
return i - T.length; // 返回起始位置
else
return 0;
}
时间复杂度
设主串长度为n ,模式串长度为m ,则最坏的时间复杂度为O(nm).

第二种:KMP算法(朴素模式匹配的优化)

利用 不匹配的字符之前,前面的这些元素一定是和模式串一致的,可以直接选择从下一个不同的地方开始匹配。

** i (主串指针)不动(即主串指针i不用回溯),j变化**
求next数组(预处理)
根据模式串T,求出next数组。
next数组之和模式串有关,与主串无关。
cpp
// 求模式串 T 的 next 数组(位序从1开始)
void get_next(SString T, int next[]) {
int i = 1, j = 0;
next[1] = 0; // 1. next[0] 无脑写0
while (i < T.length) { // 注意:i 从1遍历到 length-1,因为 next[length] 需要在循环内求
if (j == 0 || T.ch[i] == T.ch[j]) { // 2. 匹配或退无可退
++i; ++j;
next[i] = j; // 3. 记录当前 i 位置的 next 值
} else {
j = next[j]; // 4. 回溯 j(利用已求得的 next 数组)
}
}
}
模式匹配
利用next数组进行匹配(主串指针不回溯)
cpp
// KMP 匹配算法(主串 S,模式串 T,已求好的 next 数组)
int Index_KMP(SString S, SString T, int next[]) {
int i = 1, j = 1;
while (i <= S.length && j <= T.length) {
// 1. 若 j==0,表示第一个字符都不匹配,i 必须后移一位
// 2. 若当前字符匹配,继续比较下一对
if (j == 0 || S.ch[i] == T.ch[j]) {
++i; ++j;
} else {
// 3. 失配:i 不动,j 回溯到 next[j]
j = next[j];
}
}
if (j > T.length)
return i - T.length; // 匹配成功
else
return 0;
}
时间复杂度
KMP算法,最坏时间复杂度:O(m+n)
- 其中,预处理:求next数组时间复杂度:
O(m) - 模式匹配:模式匹配过程最坏时间复杂度:
O(n)
求模式串的next数组(手算练习)
next[1]都无脑写0;next[2]都无脑写1;
其他next:
- 在不匹配的位置前,划一刀分界线;
- 模式串一步一步往后退(向右走),直到分界线之前(左边)都"能对上",或模式串完全跨过分界线为止。
- 此时,j 指向哪儿,则next数组值就是多少。

next数组的优化:nextval数组(手算练习)
cpp
// 求优化后的 nextval 数组
void get_nextval(SString T, int nextval[]) {
int i = 1, j = 0;
nextval[1] = 0;
while (i < T.length) {
if (j == 0 || T.ch[i] == T.ch[j]) {
++i; ++j;
// 核心优化:若 i 和 j 位置的字符相等,则继续递归
if (T.ch[i] != T.ch[j]) {
nextval[i] = j; // 不同,直接赋值
} else {
nextval[i] = nextval[j]; // 相同,用 nextval 覆盖
}
} else {
j = nextval[j]; // 回溯时也用 nextval
}
}
}
手算解题:先求next数组,再由next数组求nextval数组。
cpp
nextval[1] = 0;
// 从next[2]开始
for (int j = 2; j <= T.length; j++)
{
if (T.ch[next[j]] == T.ch[j])
nextval[j] = nextval[next[j]]; // 优化部分
else
nextval[j] = next[j];
}
