【数据结构】串——模式匹配
目录
- 串
- 串的模式匹配算法
- 模式匹配
- 朴素匹配 / 暴力匹配
- KMP 算法
串
串的模式匹配算法
模式匹配
定义:
模式匹配是指在一个“主串”(Text)中查找“模式串”(Pattern)出现的位置,并判断是否匹配。
换句话说:
- 主串 S:你要搜索的长文本,例如
"abcabcabc"
- 模式串 T:你要找的子串,例如
"cab"
问题:找出 T 第一次出现在 S 中的位置。
形式化描述:
- 主串:
S = s1 s2 ... sn
- 模式串:
T = t1 t2 ... tm
- 找出最小的
i
,使得S[i..i+m-1] = T[1..m]
- 若找不到,返回 0
例子
主串:S = "abcabcabc"
模式串:T = "cab"
- 从 S 的第 1 个字符开始比:
"abc"
vs"cab"
→ 不匹配 - 从 S 的第 2 个字符开始比:
"bca"
vs"cab"
→ 不匹配 - 从 S 的第 3 个字符开始比:
"cab"
vs"cab"
→ 匹配 ✅
返回位置:3(位序从1开始)
模式匹配算法分类
- 朴素匹配 / 暴力匹配(Naive / Brute Force)
- 原理:从主串每个位置挨个尝试匹配模式串
- 时间复杂度最坏 O(n·m)
- 优点:实现简单,容易理解
- 缺点:效率低
- KMP 算法
- 利用模式串内部的重复信息,避免无谓回溯
- 时间复杂度 O(n + m)
- 其他高级算法(Boyer-Moore、Sunday 算法等)
- 进一步优化,适合长文本匹配
朴素匹配 / 暴力匹配
朴素匹配就是在 主串 中查找 模式串 出现的位置的最直接方法。
概念化描述:
- 主串:
S = s1 s2 ... sn
- 模式串:
T = t1 t2 ... tm
问题:找出主串中第一个连续子串等于模式串的位置(位序从 1 开始)。
如果找不到,就返回 0。
简单说,就是 从头到尾挨个尝试匹配。
所以也叫 暴力匹配(Brute Force),因为它不使用任何优化策略。
算法思路:
假设主串长度 n
,模式串长度 m
:
- 从主串第一个字符(位序1)开始,尝试匹配模式串。
- 如果模式串全部匹配成功 → 返回当前位置。
- 如果匹配失败 → 主串起点右移 1 个字符,模式串重新从头开始。
- 重复步骤2,直到主串末尾。
匹配公式:
假设当前主串起点为 i
,模式串索引为 j
(下标从0开始):
- 如果
S[i+j] == T[j]
→j++
,继续比较下一个字符 - 如果
S[i+j] != T[j]
→i = i + 1
,j = 0
,从主串下一个起点重新尝试
时间复杂度:
- 最坏情况:模式串每个字符都要和主串多次比较
- 时间复杂度:
O(n * m)
- 空间复杂度:
O(1)
,只需要几个计数器
所以效率低,但直观易理解。
// 朴素匹配函数
int Index(HString S, HString T, int pos) {int i = pos - 1; // 主串起点(位序从1开始)int j = 0; // 模式串起点while(i < S.length && j < T.length) {if(S.ch[i] == T.ch[j]) { // 匹配i++; j++;} else { // 不匹配,回溯i = i - j + 1; // 主串起点右移1j = 0; // 模式串从头开始}}if(j == T.length)return i - T.length + 1; // 返回匹配位置elsereturn 0; // 未找到
}
**匹配过程:**示例:S = “abcabcabc”,T = “cab”
初始状态:
主串 S:a b c a b c a b c
模式串 T:c a b
匹配起点 i = 0
匹配指针 j = 0
Step 1:i=0, j=0 比较 S[0] vs T[0] → a vs c,不匹配
主串: [a] b c a b c a b c
模式串: [c] a b
结果: × 不匹配,回溯 → i = i - j + 1 = 1, j = 0
Step 2:i=1, j=0 比较 S[1] vs T[0] → b vs c,不匹配
主串: a [b] c a b c a b c
模式串: [c] a b
结果: × 不匹配,回溯 → i = 2, j = 0
Step 3:i=2, j=0 比较 S[2] vs T[0] → c vs c,匹配
主串: a b [c] a b c a b c
模式串: [c] a b
结果: ✓ 匹配 → i=3, j=1
Step 4:i=3, j=1 比较 S[3] vs T[1] → a vs a,匹配
主串: a b c [a] b c a b c
模式串: c [a] b
结果: ✓ 匹配 → i=4, j=2
Step 5:i=4, j=2 比较 S[4] vs T[2] → b vs b,匹配
主串: a b c a [b] c a b c
模式串: c a [b]
结果: ✓ 匹配成功
匹配位置 = i - j + 1 = 3
匹配完成,模式串 "cab"
在主串 "abcabcabc"
的第 3 个字符开始出现。
模式匹配就是在主串里找子串出现的位置。
朴素匹配是最基础的方法,通过挨个尝试和回溯实现。
理解匹配过程是学习 KMP 等高效算法的前提。
KMP 算法
KMP算法是解决字符串模式匹配的高效算法。它的核心思想:
当匹配失败时,不需要把主串的起点回溯到下一个字符,而是利用 模式串自身的重复信息,直接跳过已经匹配过的字符,从而避免重复比较。
- 主串:
S = s1 s2 ... sn
- 模式串:
T = t1 t2 ... tm
- KMP 能在 O(n + m) 时间复杂度内完成匹配,而朴素匹配最坏是 O(n·m)。
KMP的核心:Next数组(部分匹配表)
-
Next数组记录模式串内部的 前缀和后缀的最大相等长度。
-
定义:
next[j]
表示模式串 T[0…j] 的最大相等真前缀和真后缀长度(不包括整个串本身)。 -
用途:匹配失败时,模式串向右滑动多少位,避免重复比较。
-
Next数组计算方法:
设 i
为当前考察位置,j
为前缀长度:
- 初始化:
next[0] = 0
,i = 1
,j = 0
- 如果
T[i] == T[j]
→j++
,next[i] = j
,i++
- 如果
T[i] != T[j]
:- 如果
j != 0
→j = next[j-1]
- 如果
j == 0
→next[i] = 0
,i++
- 如果
Next数组本质上告诉我们:匹配失败时,模式串可以跳到哪儿继续匹配。
void ComputeNext(char T[], int m, int next[]) {int i = 1, j = 0;next[0] = 0; // 第一个位置没有真前后缀while(i < m) {if(T[i] == T[j]) {j++;next[i] = j;i++;} else {if(j != 0)j = next[j-1];else {next[i] = 0;i++;}}}
}
KMP 算法步骤:
- 计算模式串的 Next 数组
- 主串 S 指针
i = 0
,模式串 T 指针j = 0
- 循环比较 S[i] 和 T[j]:
- 相等 →
i++
,j++
- 不等 → 如果
j != 0
,j = next[j-1]
;否则i++
- 相等 →
- 如果
j == m
→ 匹配成功,返回i - m + 1
int KMP(char S[], int n, char T[], int m) {int next[100];ComputeNext(T, m, next);int i=0, j=0;while(i<n) {if(S[i] == T[j]) {i++; j++;} else {if(j != 0)j = next[j-1];elsei++;}if(j == m) // 匹配成功return i - m;}return -1; // 未找到
}
示例:S = “abcabcabc”,T = “abcab”
-
先计算 Next 数组,模式串 T =
"a b c a b"
索引: 0 1 2 3 4 T: a b c a b Next: 0 0 0 1 2
Next 数组告诉我们匹配失败时,模式串跳到哪里继续比较。
-
匹配过程
-
初始状态:
S: a b c a b c a b c T: a b c a b i = 0 (主串指针) j = 0 (模式串指针)
-
Step 1:i=0, j=0
S[i]=a, T[j]=a → ✓ 匹配 i=1, j=1 主串: [a] b c a b c a b c 模式串: [a] b c a b
-
Step 2:i=1, j=1
S[i]=b, T[j]=b → ✓ 匹配 i=2, j=2 主串: a [b] c a b c a b c 模式串: a [b] c a b
-
Step 3:i=2, j=2
S[i]=c, T[j]=c → ✓ 匹配 i=3, j=3 主串: a b [c] a b c a b c 模式串: a b [c] a b
-
Step 4:i=3, j=3
S[i]=a, T[j]=a → ✓ 匹配 i=4, j=4 主串: a b c [a] b c a b c 模式串: a b c [a] b
-
Step 5:i=4, j=4
S[i]=b, T[j]=b → ✓ 匹配 i=5, j=5 → j == T.length → 匹配成功 匹配位置 = i - j = 0
如果匹配失败的情况:
假设模式串 T =
"abca"
,S ="abcabcabc"
,演示失败后的跳转:- 当 i=3, j=3 比较 S[3]=a, T[3]=a → 匹配 ✓
- i=4, j=4 比较 S[4]=b, T[4]=? → T[4]不存在 → 匹配失败
- 根据 Next 数组 next[3]=1 → j = 1,模式串跳到 T[1] 继续匹配
- i 不回退 → 保持 i=4,继续比较 S[4] 与 T[1]
这就是 KMP 的核心:主串指针不回退,模式串指针根据 Next 数组跳跃。
-