KMP 算法
题目描述
给定一个文本串 s 和一个模式串 p,在 s 中查找 p 第一次出现的位置。如果不存在则返回 -1。
暴力匹配的时间复杂度为 O(m × n),而 KMP 算法通过预处理模式串的"前后缀信息",将匹配过程优化到 O(m + n),核心思想是匹配失败时利用已知信息跳过不必要的比较。
解题方法:KMP(Knuth-Morris-Pratt)
KMP 算法的关键在于 next 数组(也称前缀函数)。它记录了模式串中每个位置之前的最长相等前后缀长度。当匹配失败时,模式串指针不回溯到开头,而是跳转到 next 数组指示的位置,从而避免重复匹配。
算法思路
- 构建 next 数组:遍历模式串,用双指针计算每个位置的最长相等前后缀长度。前缀指针 j 始终指向当前已匹配前缀的下一个位置,当字符匹配时 j 前移,不匹配时 j 回退到 nxt[j-1]。
- 匹配过程:遍历文本串,同样用双指针。字符匹配时模式串指针前移,不匹配时利用 next 数组回退。当模式串指针走到末尾,说明匹配成功。
代码解析
def build_next(patt):
# 构建 next 数组(前缀函数)
m = len(patt)
nxt = [0] * m # nxt[i] 表示 patt[:i+1] 的最长相等前后缀长度
j = 0 # 前缀指针
for i in range(1, m): # i 是后缀指针
# 不匹配时,前缀指针回退到前一个位置的 next 值
while j > 0 and patt[i] != patt[j]:
j = nxt[j - 1]
# 匹配成功,前缀指针前移
if patt[i] == patt[j]:
j += 1
nxt[i] = j
return nxt
def kmp(s, p):
# KMP 字符串匹配,返回 p 在 s 中首次出现的位置
if not p:
return 0
nxt = build_next(p)
j = 0 # 模式串指针
for i in range(len(s)): # 文本串指针
# 不匹配时利用 next 数组回退
while j > 0 and s[i] != p[j]:
j = nxt[j - 1]
if s[i] == p[j]:
j += 1
# 匹配完成
if j == len(p):
return i - len(p) + 1
return -1
print(kmp("abababc", "ababc")) # 输出: 2代码要点说明
- build_next:用模式串自己匹配自己的方式构建 next 数组。
nxt[i]记录patt[:i+1]的最长相等前后缀长度,i 从 1 开始,因为单个字符的前后缀长度为 0。 - 匹配回退:当
s[i] != p[j]时,j = nxt[j - 1]是关键一步。它利用已匹配部分的前后缀信息,将模式串向右滑动到合适位置,避免从头开始。 - 时间复杂度:构建 next 数组和匹配过程都是线性扫描,每个字符最多被比较两次。
复杂度分析
- 时间复杂度:O(m + n),其中 m 为模式串长度,n 为文本串长度。构建 next 数组 O(m),匹配过程 O(n)。
- 空间复杂度:O(m),需要存储 next 数组。