朴素模式匹配算法和KMP算法是字符串匹配中的两种经典方法,各有特点。下面对两者进行系统性总结,并重点分析其性能差异与实现原理。
1. 朴素模式匹配算法(Brute Force)
基本思想:
从主串的每一个位置出发,逐个字符与模式串比较,一旦发现不匹配,则主串指针回退到下一个起始位置,模式串重新从头开始匹配。
最坏情况分析:
- 主串 S 长度为
n,模式串 P 长度为m - 最坏情形示例:
- 主串:
"aaaa...aab"(前 n−m+1 个'a',最后有变化) - 模式串:
"aaab"
- 主串:
- 在这种情况下,每次匹配都会在模式串的最后一个字符才失败,导致每趟比较
m次,共尝试n−m+1趟 - 总比较次数接近
(n−m+1) × m→ 时间复杂度为 O(n×m)
平均比较次数公式:约 12m(n−m+2)\frac{1}{2} m(n - m + 2)21m(n−m+2),仍属平方级增长
优点:实现简单、直观
缺点:效率低,尤其在重复字符多的文本中表现差
def naive_match(s, p):
n, m = len(s), len(p)
for i in range(n - m + 1):
match = True
for j in range(m):
if s[i + j] != p[j]:
match = False
break
if match:
return i # 返回首次匹配成功的位置
return -1 # 未找到
2. KMP 算法(Knuth-Morris-Pratt)
核心思想:
避免主串指针回溯!当发生失配时,利用模式串自身的最长相等真前后缀信息(即 next 数组),将模式串“滑动”到下一个可能匹配的位置。
next 数组定义:
对于模式串 p[0..m-1],定义 next[j] 表示:
在
p[0..j-1]中,最长的相等真前缀与真后缀的长度。
更形式化地:
next[j]={−1j=00j>0 且不存在 k∈(0,j) 使得 p[0..k−1]=p[j−k..j−1]max{k∣p[0..k−1]=p[j−k..j−1], 0<k<j}otherwise
\text{next}[j] =
\begin{cases}
-1 & j = 0 \\
0 & j > 0 \text{ 且不存在 } k \in (0,j) \text{ 使得 } p[0..k-1] = p[j-k..j-1] \\
\max\{k \mid p[0..k-1] = p[j-k..j-1],\, 0 < k < j\} & \text{otherwise}
\end{cases}
next[j]=⎩⎨⎧−10max{k∣p[0..k−1]=p[j−k..j−1],0<k<j}j=0j>0 且不存在 k∈(0,j) 使得 p[0..k−1]=p[j−k..j−1]otherwise
作用:当 p[j] 失配时,将模式串右移,使 p[next[j]] 对齐当前主串字符继续比较。
构造 next 数组(Get_next)
def get_next(p):
m = len(p)
next_arr = [0] * m
next_arr[0] = -1
j, k = 0, -1
while j < m - 1:
if k == -1 or p[j] == p[k]:
j += 1
k += 1
next_arr[j] = k
else:
k = next_arr[k]
return next_arr
KMP 匹配函数(KMPIndex)
def kmp_index(s, p):
n, m = len(s), len(p)
if m == 0:
return 0
next_arr = get_next(p)
i = j = 0 # i: 主串指针, j: 模式串指针
while i < n and j < m:
if j == -1 or s[i] == p[j]:
i += 1
j += 1
else:
j = next_arr[j]
if j == m:
return i - m # 找到匹配起始位置
return -1 # 未找到
时间复杂度:
- 构建
next数组:O(m) - 匹配过程:O(n)
- 总体:O(n + m) —— 显著优于朴素算法
应用场景:
- 文本编辑器搜索功能
- DNA序列比对
- 编程语言中的正则表达式底层优化(部分机制)
- 数据流中关键词检测
对比总结
| 特性 | 朴素算法 | KMP 算法 |
|---|---|---|
| 主串指针是否回溯 | 是 | 否 |
| 时间复杂度 | O(n×m) | O(n+m) |
| 空间复杂度 | O(1) | O(m)(存储 next 数组) |
| 实现难度 | 简单 | 中等 |
| 适用场景 | 小规模数据、实时性要求不高 | 大文本高效检索 |
手动计算模式串的 next 数组,关键在于理解其定义:
next[j]表示模式串前j个字符构成的子串中,最长相等真前缀与真后缀的长度。
其中,“真前后缀” 指的是不等于整个字符串本身的前缀或后缀。
我们以模式串 "ababaa" 为例,逐步推导每个位置的 next[j] 值。
模式串:p = "ababaa"
索引: 0 1 2 3 4 5
字符: a b a b a a
我们将逐位计算 next[0] 到 next[5]。
step 1: j = 0
- 定义规定:
next[0] = -1(无字符可比较,匹配失败时回退到起始前) - ✅
next[0] = -1
step 2: j = 1,子串 “a”
- 考察
p[0..0] = "a" - 真前缀集合:{ }(长度 ≥1 且 <1 → 空)
- 真后缀集合:{ }
- 最长相等真前后缀长度为 0?但注意:
- 实际上只有一个字符,没有非空真前后缀
- 所以
max{k | ...} = 0不成立 → 取默认值 - ✅
next[1] = 0
💡 规则:当
j > 0且不存在满足条件的k,则next[j] = 0
step 3: j = 2,子串 “ab”
- 子串:
p[0..1] = "ab" - 真前缀:
"a" - 真后缀:
"b" - 相等?否
- 无公共部分 → 长度为 0
- ✅
next[2] = 0
step 4: j = 3,子串 “aba”
- 子串:
p[0..2] = "aba" - 真前缀:
"a", "ab" - 真后缀:
"a", "ba" - 公共项:
"a"(出现在首尾) - 最长长度 = 1
- ✅
next[3] = 1
step 5: j = 4,子串 “abab”
- 子串:
p[0..3] = "abab" - 真前缀:
"a", "ab", "aba" - 真后缀:
"b", "ab", "bab" - 对比:
"ab"(前缀) vs"ab"(后缀)→ 匹配!
- 其他?
"aba"≠"bab"
- 最长匹配长度 = 2
- ✅
next[4] = 2
step 6: j = 5,子串 “ababa”
- 子串:
p[0..4] = "ababa" - 真前缀:
"a", "ab", "aba", "abab" - 真后缀:
"a", "ba", "aba", "baba" - 公共最长:
"a"→ yes"aba"→ yes(前3个和后3个)
"abab"≠"baba"- 所以最长是 3
- ✅
next[5] = 3
step 7: j = 6(最终完整串 “ababaa”,但我们只算到 next[5])
实际上 next 数组长度等于模式串长度,我们已计算完所有项。
最终结果:
| j(位置) | 字符 | 子串 | 最长相等真前后缀 | next[j] |
|---|---|---|---|---|
| 0 | a | “” | — | -1 |
| 1 | b | “a” | 无 | 0 |
| 2 | a | “ab” | 无 | 0 |
| 3 | b | “aba” | “a” | 1 |
| 4 | a | “abab” | “ab” | 2 |
| 5 | a | “ababa” | “aba” | 3 |
✅ 所以,next = [-1, 0, 0, 1, 2, 3]
小技巧:递推法记忆
也可以使用类似 KMP 构造过程的“双指针法”来快速手算:
初始化:next[0] = -1, j=0, k=-1
while j < 5:
if k == -1 or p[j] == p[k]:
j += 1; k += 1; next[j] = k
else:
k = next[k]
模拟如下:
- j=0, k=-1 → 进入 if → j=1, k=0 → next[1]=0
- j=1, k=0 → p[1]=‘b’, p[0]=‘a’ → 不等 → k = next[0] = -1
- j=1, k=-1 → if 成立 → j=2, k=0 → next[2]=0
- j=2, k=0 → p[2]=‘a’ == p[0]=‘a’ → j=3, k=1 → next[3]=1
- j=3, k=1 → p[3]=‘b’ == p[1]=‘b’ → j=4, k=2 → next[4]=2
- j=4, k=2 → p[4]=‘a’ == p[2]=‘a’ → j=5, k=3 → next[5]=3
结果一致:[-1, 0, 0, 1, 2, 3]


16万+

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



