朴素模式匹配算法和KMP算法是字符串匹配中的两种经典方法,各有特点

朴素模式匹配算法和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(nm+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{kp[0..k1]=p[jk..j1],0<k<j}j=0j>0 且不存在 k(0,j) 使得 p[0..k1]=p[jk..j1]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]
0a“”-1
1b“a”0
2a“ab”0
3b“aba”“a”1
4a“abab”“ab”2
5a“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]


在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bol5261

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值