KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,用于在主串(文本)中快速查找子串(模式)的位置。其核心思想是通过预处理模式串,利用已匹配的信息避免不必要的回溯,将时间复杂度从暴力匹配的O(m*n)优化至O(m+n)。
核心原理
-
部分匹配表(Partial Match Table):
- 预处理模式串,生成一个
next
数组(或称失效函数)。 next[i]
表示模式串前i
个字符组成的子串中,最长相同前后缀的长度。- 例如模式串
"ABABC"
的next
数组为[0, 0, 1, 2, 0]
。
- 预处理模式串,生成一个
-
匹配过程:
- 当字符失配时,根据
next
数组跳过模式串中已匹配的前缀部分,直接比较下一个可能的位置。 - 主串指针不回溯,模式串指针回溯到
next[j]
的位置。
- 当字符失配时,根据
示例步骤
假设主串为 "ABABABABC"
,模式串为 "ABABC"
:
- 预处理模式串得到
next = [0, 0, 1, 2, 0]
。 - 匹配到第5个字符时(主串的
'A'
vs 模式串的'C'
)失配,模式串指针根据next[4]=2
跳转到第2个字符继续匹配。
优势
- 减少重复比较:利用
next
数组跳过已匹配的部分。 - 线性时间复杂度:预处理O(m),匹配O(n),总复杂度O(m+n)。
代码框架(Python示例)
def kmp(text, pattern):
def build_next(p):
next = [0] * len(p)
j = 0
for i in range(1, len(p)):
while j > 0 and p[i] != p[j]:
j = next[j-1]
if p[i] == p[j]:
j += 1
next[i] = j
return next
next = build_next(pattern)
j = 0
for i in range(len(text)):
while j > 0 and text[i] != pattern[j]:
j = next[j-1]
if text[i] == pattern[j]:
j += 1
if j == len(pattern):
return i - j + 1 # 返回匹配的起始位置
return -1 # 未找到
应用场景
- 文本编辑器中的查找功能
- DNA序列匹配
- 任何需要高效字符串匹配的场景
KMP算法的关键在于通过预处理提取模式串的自相似性,从而优化匹配效率。理解next
数组的构建和回溯逻辑是掌握该算法的核心。
例题
KMP匹配算法中,子串S= ’aaaab‘,主串T= ’abaaaabca‘。求匹配过程中的比较次数?
问题描述
- 子串(模式串)S:
'aaaab'
- 主串(文本)T:
'abaaaabca'
- 目标: 使用KMP算法匹配S在T中的位置,并统计字符比较的总次数。
KMP算法步骤
- 预处理模式串S,生成
next
数组。 - 在主串T中匹配S,利用
next
数组优化匹配过程,同时记录每次字符比较。
1. 构建模式串S的next数组
模式串 S = 'aaaab'
,长度为5。
索引i | 子串 S[0:i+1] | 最长相同前后缀长度 | next[i] |
---|---|---|---|
0 | 'a' | 0 | 0 |
1 | 'aa' | 1(前缀'a'=后缀'a') | 1 |
2 | 'aaa' | 2('aa'='aa') | 2 |
3 | 'aaaa' | 3('aaa'='aaa') | 3 |
4 | 'aaaab' | 0(无相同前后缀) | 0 |
最终 next = [0, 1, 2, 3, 0]
。
2. 匹配过程与比较次数统计
主串 T = 'abaaaabca'
,模式串 S = 'aaaab'
。
初始化:主串指针 i = 0
,模式串指针 j = 0
,比较次数 count = 0
。
逐步匹配过程:
-
i=0, j=0:
- T[0]='a' vs S[0]='a' → 匹配。
i++
,j++
,count++
(当前count=1
)。- 状态:
i=1
,j=1
。
-
i=1, j=1:
- T[1]='b' vs S[1]='a' → 失配。
- 根据
next[j-1] = next[0] = 0
,模式串指针回退到j=0
。 count++
(当前count=2
)。- 状态:
i=1
,j=0
。
-
i=1, j=0:
- T[1]='b' vs S[0]='a' → 失配。
j
已是0,无法回退,主串指针i++
。count++
(当前count=3
)。- 状态:
i=2
,j=0
。
-
i=2, j=0:
- T[2]='a' vs S[0]='a' → 匹配。
i++
,j++
,count++
(当前count=4
)。- 状态:
i=3
,j=1
。
-
i=3, j=1:
- T[3]='a' vs S[1]='a' → 匹配。
i++
,j++
,count++
(当前count=5
)。- 状态:
i=4
,j=2
。
-
i=4, j=2:
- T[4]='a' vs S[2]='a' → 匹配。
i++
,j++
,count++
(当前count=6
)。- 状态:
i=5
,j=3
。
-
i=5, j=3:
- T[5]='a' vs S[3]='a' → 匹配。
i++
,j++
,count++
(当前count=7
)。- 状态:
i=6
,j=4
。
-
i=6, j=4:
- T[6]='b' vs S[4]='b' → 匹配。
i++
,j++
,count++
(当前count=8
)。j == len(S)
,匹配成功,返回起始位置i-j=6-4=2
。
总结
- 总比较次数: 8次(每次字符对比均计入,包括成功和失败)。
- 匹配位置: 主串T中从索引2开始匹配成功(即
T[2:7] = 'aaaab'
)。
关键点说明
- next数组的作用:在失配时跳过模式串中已匹配的前缀(如步骤2中
j
从1回退到0)。 - 比较次数的统计:每次主串和模式串的字符对比(无论成功或失败)均计入总数。
- KMP的优势:本例中避免了主串指针
i
的回溯(如暴力匹配需要回溯到i=1
重新开始),提高了效率。
通过逐步分析,可以清晰看到KMP如何通过next
数组优化匹配过程,同时准确统计比较次数。