【DSA】KMP & BM & Sunday

字符串匹配

以下是主流字符串匹配算法的系统梳理,涵盖核心思想、时间复杂度、适用场景及优缺点,结合技术原理与工程实践综合呈现:


📊 算法分类与概览

算法类型代表算法时间复杂度核心思想适用场景
基本算法暴力匹配 (Brute Force)O(m*n)逐字符比较短文本、简单场景
单模式匹配KMPO(m+n)失效函数避免回溯通用场景、理论教学
Boyer-Moore (BM)O(n/m) ~ O(m*n)反向匹配+双启发规则长模式串、实际应用
SundayO(n/m) 平均BM改进+关注下一字符快速匹配、中长文本
Rabin-KarpO(n*m) 平均哈希值比较多模式匹配基础
多模式匹配Trie树O(m) 建树, O(n) 匹配前缀树结构词典匹配、单词查询
Aho-Corasick (AC自动机)O(n+m+k)Trie+KMP思想敏感词过滤、病毒检测

🔍 基本匹配算法:暴力法 (Brute Force)

  • 原理:主串从每个字符开始与模式串逐位比较,失败后主串回溯到下一位置重新匹配1,3
  • 时间复杂度:最坏 O(m*n)(m, n为模式串和主串长度)。
  • 优点:实现简单,短文本效率高。
  • 缺点:长文本效率低,回溯冗余。
  • 代码示例(Java):
    for (int i = 0; i <= n - m; i++) {
        int j = 0;
        while (j < m && text[i+j] == pattern[j]) j++;
        if (j == m) return i; // 匹配成功
    }
    

⚙️ 经典单模式匹配算法

KMP算法 (Knuth-Morris-Pratt)

  • 核心思想:利用部分匹配表(next数组)记录模式串前缀后缀的最长公共长度。匹配失败时,模式串右移位数 = 已匹配字符数 - next[j],避免主串回溯6,7,8
  • 关键步骤:
    • 预处理:计算next数组(O(m))。
    • 匹配:主串指针不回溯,模式串按next数组跳跃(O(n))。
  • 时间复杂度:O(m+n)。
  • 优势:理论高效,适合模式串较长场景。
  • 局限性:实现复杂,实际性能常低于BM3

Boyer-Moore (BM)算法

  • 核心思想: 反向匹配(从模式串末尾开始) + 双启发规则:
    • 坏字符规则 (Bad Character):主串中不匹配的字符若不在模式串中,则跳过整个模式串;若存在,则对齐模式串中最右出现位置。
    • 好后缀规则 (Good Suffix):已匹配的后缀子串在模式串中再次出现时,对齐其最右位置。
  • 移动策略:取两规则计算值的最大值。
  • 时间复杂度:
    • 最坏 O(m*n)(如全相同字符)。
    • 平均 O(n/m)(跳跃性强)9,10
  • 优势:工业级高效(如grep工具),长模式串优势显著。
  • 缺点:预处理开销大。

Rabin-Karp算法

  • 核心思想:将模式串哈希值 H(p) 与主串中每个长度为m的子串哈希值比较。若哈希值相等,则进一步验证字符匹配2,4
  • 优化:滚动哈希(如多项式哈希),使相邻子串哈希值计算 O(1)。
  • 时间复杂度:平均 O(n+m),最坏 O(m*n)(哈希冲突时)。
  • 适用场景:多模式匹配基础(如AC自动机预处理)。

高效单模式变种

Sunday算法

  • 改进点:BM的简化版,匹配失败时关注主串中下一字符:
    • 若该字符不在模式串中,跳过模式串长度+1。
    • 若存在,对齐模式串中最右出现位置3,10
  • 优势:实现简单,平均性能优于BM(尤其英文文本)3

Horspool算法

  • 简化BM:仅用坏字符规则,且对齐模式串中最右字符(非任意字符)1,10
  • 适用场景:中等长度模式串。

🌐 多模式匹配算法

Trie树 (字典树)

  • 原理:将模式集构建树结构,边为字符。匹配时沿树遍历主串,到达叶节点即匹配成功4,5
  • 时间复杂度:建树 O(M)(M为模式集总长),匹配 O(n)。
  • 局限:仅适用精确匹配,内存占用高。

Aho-Corasick (AC自动机)

  • 核心思想:Trie + KMP的next数组:
    • Fail指针:匹配失败时跳转到最长后缀前缀节点(类似KMP)4,5
  • 时间复杂度:O(n + m + k)(k为匹配次数)。
  • 应用:敏感词过滤(如腾讯内容安全服务)5

🛠️ 应用场景与选型建议

  1. 短文本/简单场景:暴力法(如Java String.indexOf()2
  2. 单模式长文本:BM/Sunday(如文本编辑器搜索)9,10
  3. 多模式匹配:AC自动机(如敏感词过滤)4,5
  4. 动态模式集:Rabin-Karp(可扩展性高)2

💡 性能提示:实际效率受字符集分布(如DNA序列用BM跳跃强)、模式串长度、硬件缓存等因素影响。例如,短模式串时暴力法常优于KMP1,3。 如需特定算法代码实现(如BM的坏字符表构建、AC自动机的Fail指针生成),可进一步提供语言和场景说明。

KMP

KMP算法(Knuth-Morris-Pratt算法)是一种高效的单模式字符串匹配算法,由Donald Knuth、Vaughan Pratt和James H. Morris于1977年联合提出。其核心思想是通过预处理模式串生成部分匹配表(next数组),在匹配失败时避免主串回溯,仅调整模式串的位置,将时间复杂度优化至O(n+m)(n为主串长度,m为模式串长度)。以下是详细解析:


🔍 算法核心思想

解决暴力匹配的缺陷

  • 暴力匹配(Brute-Force):主串与模式串逐字符比较,失败时主串回溯至起始位置+1,模式串复位,时间复杂度 O(m×n)3,10
  • KMP优化:匹配失败时,主串指针不回溯,模式串利用 next数组 跳转到最长相同前后缀的下一位置继续匹配,减少无效比较1,8

关键概念:最长相同前后缀

  • 前缀:不包含最后一个字符的子串(如"ABC"的前缀为"A"、“AB”)。
  • 后缀:不包含第一个字符的子串(如"ABC"的后缀为"BC"、“C”)。
  • 最长相同前后缀:如"ABABA"的最长相同前后缀为"ABA"(长度3)3,10

⚙️ next数组(部分匹配表)

next数组的定义

  • next[j]表示:模式串下标0~j的子串中,最长相同前后缀的长度。 ​示例​:模式串"ABABC"的next数组为[0,0,1,2,0]2,8

next数组的构建步骤

  • 初始化next[0] = 0(单字符无前后缀),指针i=1(后缀尾),j=0(前缀尾)。
  • 迭代计算:
    • pattern[i] == pattern[j],则j++next[i] = ji++
    • 若不等且j > 0,则j = next[j-1](回退至上一匹配位置)。
    • j = 0,则next[i] = 0i++8,10构建过程示例(模式串:“ABABA”)
      字符索引01234
      字符ABABA
      next值00123

🔄 匹配流程

  1. 初始化指针:主串指针i=0,模式串指针j=0
  2. 逐字符比较:
    • text[i] == pattern[j],则i++, j++
  • j = m(模式串完全匹配),记录位置,并令j = next[j-1]继续搜索。
    • 若字符不等且j > 0,则j = next[j-1](模式串跳转)。
  • j = 0,则i++(主串后移)4,8匹配示例
  • 主串:
    "ABABABCABAB"
    
    ,模式串:
    "ABABC"
    
    • 匹配至i=4(主串’C’≠模式串’B’)→ j = next[3] = 2 → 跳过"AB"比较,继续匹配4

📊 性能分析

指标说明
时间复杂度预处理:O(m),匹配:O(n),总计 O(n+m)1,8
空间复杂度需存储next数组:O(m)1
对比暴力匹配避免主串回溯,尤其适合模式串重复度高主串极长的场景3

与BM算法对比

特性KMPBM(Boyer-Moore)
匹配方向从左向右从右向左
跳跃能力依赖next数组,跳跃较保守双启发规则(坏字符+好后缀),跳跃性强
适用场景小字符集、模式串重复度高大字符集(如英文、DNA序列)

⚖️ 优缺点与应用场景

优点

  1. 理论高效:最坏情况下仍保持线性时间复杂度。
  2. 无回溯:主串指针单向移动,适合流式数据处理4

缺点

  1. 空间开销:需存储next数组,模式串极长时可能影响性能。
  2. 实现复杂:理解next数组构建逻辑有一定门槛。
  3. 特定场景低效:模式串无重复时(如"ABCDE"),性能接近暴力匹配1,3

🛠️ 应用场景

  • 文本编辑器:查找/替换功能(如VS Code)。
  • 生物信息学:DNA序列模式匹配(如"ATCG"重复片段检测)。
  • 网络安全:病毒特征码扫描8,9

💻 代码实现(Python示例)

def kmp_search(text, pattern):
    def build_next(p):
        next_arr = [0] * len(p)
        j = 0  # 前缀尾指针
        for i in range(1, len(p)):  # i为后缀尾指针
            while j > 0 and p[i] != p[j]:
                j = next_arr[j-1]  # 回退至上一匹配位置
            if p[i] == p[j]:
                j += 1
            next_arr[i] = j
        return next_arr

    next_arr = build_next(pattern)
    i, j = 0, 0  # 主串和模式串指针
    res = []
    while i < len(text):
        if text[i] == pattern[j]:
            i += 1
            j += 1
        elif j > 0:
            j = next_arr[j-1]  # 模式串跳转
        else:
            i += 1  # 主串后移
        if j == len(pattern):  # 完全匹配
            res.append(i - j)
            j = next_arr[j-1]  # 继续搜索下一匹配
    return res

# 测试
text = "ABABABABCABAB"
pattern = "ABABC"
print(kmp_search(text, pattern))  # 输出: [2](匹配起始位置)

💎 总结

KMP算法通过 预处理模式串 生成next数组,在匹配失败时智能跳转模式串位置,避免主串回溯,实现了高效的字符串匹配。尽管在模式串无重复时优势减弱,但其理论完备性和线性时间复杂度使其在重复模式匹配流式数据处理等场景中不可替代。理解next数组的构建逻辑(即最长相同前后缀)是掌握该算法的关键,而结合具体场景选择KMP或BM等算法,能进一步提升工程效率1,8,9

BM

以下是关于BM(Boyer-Moore)算法的详细解析,结合其核心原理、规则实现、复杂度分析及代码示例进行系统阐述:


🔍 算法概述

BM算法由Bob Boyer和J Strother Moore于1977年提出,是一种高效的单模式字符串匹配算法。其核心思想是通过反向匹配(从模式串末尾向前比较)和启发式跳跃规则,跳过不必要的字符比较,显著提升匹配效率。实际应用中(如文本编辑器、grep工具),其性能通常优于KMP算法3-5倍3,6

⚡ 核心特征:

  1. 反向匹配:从模式串末尾向前比较字符。
  2. 双启发规则:
    • 坏字符规则(Bad Character Rule)
    • 好后缀规则(Good Suffix Rule)
  3. 跳跃式移动:失败时根据规则计算最大跳跃距离,避免逐字符移动。

⚙️ 核心规则详解

坏字符规则

当模式串与文本串字符不匹配时,文本串中的该字符称为坏字符。规则通过预处理记录模式串中每个字符的最后出现位置,计算跳跃距离:

  • Case 1:坏字符在模式串中存在跳跃距离 = 坏字符在模式串中的当前位置 - 该字符在模式串中最后一次出现的位置。
    • 示例:模式串"EXAMPLE",坏字符'P'在位置6,其最后出现位置为4 → 跳跃距离 = 6-4=28
  • Case 2:坏字符不在模式串中 直接跳跃整个模式串长度(m位)3预处理:构建坏字符表bc[](数组下标为字符ASCII值,值为最后出现位置):
def build_bc(pattern):
    bc = [-1] * 256  # 初始化所有字符位置为-1
    for i, char in enumerate(pattern):
        bc[ord(char)] = i  # 更新字符最后出现位置
    return bc

好后缀规则

当模式串后缀与文本串匹配但前一个字符失配时,该后缀称为好后缀。规则分三种情况处理:

  • Case 1:好后缀在模式串前部再次出现 将最靠右的匹配子串与好后缀对齐4,6
  • Case 2:无完整匹配,但好后缀的后缀与模式串前缀匹配 将最长匹配前缀对齐好后缀后缀6
  • Case 3:无任何匹配 直接跳跃整个模式串长度(m位)4预处理:构建好后缀表gs[](需先计算后缀数组suff[i],表示以i结尾的子串与模式串后缀的最长匹配长度):
def build_gs(pattern):
    m = len(pattern)
    suff = [0] * m
    gs = [m] * m  # 初始化为m(Case 3)
    
    # 计算suff数组
    suff[m-1] = m
    for i in range(m-2, -1, -1):
        k = 0
        while (i - k >= 0 and pattern[i - k] == pattern[m-1 - k]):
            k += 1
        suff[i] = k
    
    # 更新gs数组(Case 1 & 2)
    j = 0
    for i in range(m-1, -1, -1):
        if suff[i] == i+1:  # 前缀=后缀
            while j < m-1-i:
                gs[j] = m-1-i
                j += 1
    for i in range(m-1):
        gs[m-1-suff[i]] = m-1-i  # Case 1
    return gs

📊 规则应用优先级:

每次失配时,取两规则计算的跳跃距离最大值jump = max(bc_shift, gs_shift)3,6


🔄 算法执行流程

  1. 预处理:
    • 构建坏字符表bc[](时间复杂度O(m)
    • 构建好后缀表gs[](时间复杂度O(m)
  2. 匹配阶段:
    • 模式串与文本串右对齐,从右向左比较字符。
    • 若完全匹配,记录位置并右移继续搜索。
    • 若失配,按max(bc_shift, gs_shift)跳跃5,6伪代码
def bm_search(text, pattern):
    bc = build_bc(pattern)
    gs = build_gs(pattern)
    n, m = len(text), len(pattern)
    i = 0  # 文本串当前对齐位置
    
    while i <= n - m:
        j = m - 1  # 从模式串末尾开始比较
        while j >= 0 and text[i+j] == pattern[j]:
            j -= 1
        if j < 0:  # 完全匹配
            print("Match at:", i)
            i += gs[0]  # 继续搜索下一位置
        else:
            # 计算坏字符跳跃距离
            bc_shift = j - bc.get(ord(text[i+j]), -1)
            # 计算好后缀跳跃距离
            gs_shift = gs[j] 
            i += max(bc_shift, gs_shift, 1)  # 至少移动1位
    return -1

📈 复杂度分析

指标说明
预处理时间O(m + σ)(σ为字符集大小,如ASCII为256)5
匹配时间最好O(n/m)(如全坏字符跳跃),最坏O(mn)(如重复字符)3,6
空间占用O(m + σ)(存储bc[]gs[]5
典型场景
  • 高跳跃性:模式串较长且字符分布分散时,性能接近O(n/m)
  • 低效场景:模式串为重复字符(如"AAAA"),退化至O(mn)6

⚖️ 优缺点与应用场景

优势

  1. 实际效率高:长模式串下比KMP快3-5倍,尤其在英文文本中3,6
  2. 跳跃能力强:坏字符规则在字符集大时效果显著(如DNA序列)。

局限

  1. 预处理开销大,短模式串时不如暴力法。
  2. 实现复杂,需维护两个预处理数组。

🛠️ 适用场景

  • 文本编辑器(如VS Code、Sublime的搜索功能)
  • 病毒扫描(高效匹配特征码)
  • 大字符集数据(如生物信息学中的基因序列匹配)

💻 完整Python示例

def boyer_moore(text, pattern):
    def build_bc(p):
        bc = [-1] * 256
        for i, c in enumerate(p):
            bc[ord(c)] = i
        return bc

    def build_gs(p):
        m = len(p)
        gs = [m] * m
        suff = [0] * m
        suff[m-1] = m
        
        # 计算suff数组
        for i in range(m-2, -1, -1):
            k = 0
            while (i - k >= 0 and p[i - k] == p[m-1 - k]):
                k += 1
            suff[i] = k
        
        # Case 1 & 2更新
        j = 0
        for i in range(m-1, -1, -1):
            if suff[i] == i + 1:
                while j < m-1-i:
                    gs[j] = m-1-i
                    j += 1
        for i in range(m-1):
            if suff[i] > 0:
                gs[m-1-suff[i]] = m-1-i
        return gs

    bc_table = build_bc(pattern)
    gs_table = build_gs(pattern)
    n, m = len(text), len(pattern)
    i = 0
    
    while i <= n - m:
        j = m-1
        while j >= 0 and text[i+j] == pattern[j]:
            j -= 1
        if j < 0:
            return i  # 匹配成功
        else:
            bc_shift = j - bc_table[ord(text[i+j])]
            gs_shift = gs_table[j]
            i += max(bc_shift, gs_shift, 1)
    return -1  # 匹配失败

💎 总结

BM算法通过反向匹配双启发规则的协同,实现了匹配过程中的大规模跳跃,成为工程实践中最高效的单模式匹配算法之一。其优势在长模式串和大字符集场景下尤为显著,但需注意预处理开销和边缘场景的性能波动。理解其规则设计与跳跃逻辑,对优化高频字符串操作(如日志分析、基因比对)至关重要3,5,6

Sunday

以下是关于 SUNDAY算法 的详细解析,涵盖核心思想、匹配规则、时间复杂度、代码实现及优化策略,结合技术原理与工程实践进行系统阐述:


🔍 算法概述

SUNDAY算法 由 Daniel M.Sunday 于 1990 年提出,是一种 高效的单模式字符串匹配算法。其核心思想是通过 跳跃式移动模式串,在匹配失败时跳过尽可能多的无效字符,从而提升匹配效率。与 Boyer-Moore(BM)算法类似,但 SUNDAY 算法采用 从前往后匹配 的顺序,且关注点不同1,6

核心特征

  1. 匹配方向:从左向右匹配(与 BM 算法的从右向左相反)。
  2. 关键启发点:匹配失败时,关注 文本串中参与匹配的最末字符的下一位字符(称为 关注字符)。
  3. 跳跃策略:根据关注字符是否在模式串中出现,决定移动步长:
    • 若未出现 → 移动步长 = 模式串长度 + 1。
    • 若出现 → 移动步长 = 模式串长度 - 该字符在模式串中最右出现的位置2,7

⚙️ 算法原理与匹配流程

预处理:构建移动表(Shift Table)

  • 目的:记录模式串中每个字符 最右出现位置 到模式串末尾的距离 + 1(即跳跃步长)。
  • 步骤:
    • 初始化一个长度为 256(ASCII 字符集)的数组 shift[],默认值设为 m + 1m 为模式串长度)。
    • 遍历模式串,更新每个字符的跳跃步长:shift[char] = m - ii 为字符位置)3,6
  • 示例 (模式串
  "search"

):

字符search
跳跃步长654321

匹配过程

  1. 对齐模式串:将模式串与文本串左对齐,起始位置 pos = 0
  2. 逐字符比较:从左向右比较模式串与文本串对应字符。
  3. 匹配失败处理:
    • 计算关注字符:T[pos + m]m 为模式串长度)。
    • 查移动表
     shift[]
     ```:
     - 若 `shift[T[pos + m]] = m + 1` → 移动步长 = `m + 1`。
     - 否则 → 移动步长 = `shift[T[pos + m]]`。
   - 更新位置:`pos += shift[T[pos + m]]`[3,7](@ref)。
#### **匹配示例**

**文本串**:`"substring searching"`
​**模式串**​:`"search"`
​**步骤**​:
1. **初始对齐**:

substring searching search ↑ 关注字符:‘i’(不在模式串中)

- 移动步长 = 6 + 1 = 7 → 跳到 `'n'` 处[2,5](@ref)。
2. **二次对齐**:

substring searching search ↑ 关注字符:‘r’(在模式串第 3 位)

- 移动步长 = 6 - 3 = 3 → 对齐两个 `'r'`[5,7](@ref)。
3. **匹配成功**:

substring searching search ✓



------
### 📊 **时间复杂度分析**

| **场景**     | **时间复杂度** | **说明**                                                     |
| ------------ | -------------- | ------------------------------------------------------------ |
| **最佳情况** | O(n/m)         | 每次匹配失败时关注字符均不在模式串中(如模式串无重复字符)[3](@ref)。 |
| **最坏情况** | O(m*n)         | 模式串重复度高(如 `"aaaaa"`),每次仅移动 1 位[5](@ref)。   |
| **平均情况** | O(n)           | 实际应用中跳跃效率高(如英文文本)[3](@ref)。                |


------
### ⚖️ **优缺点与适用场景**

#### ✅ **优势**:

1. **实现简单**:代码量少于 KMP/BM,预处理仅需构建移动表[6,7](@ref)。
2. **跳跃能力强**:在大字符集(如英文、DNA序列)中平均跳跃步长大。
3. **匹配顺序灵活**:无需固定比较方向(可优化为优先比较低概率字符)[5](@ref)。
#### ❌ **局限**:

1. **最坏情况效率低**:模式串重复度高时退化为暴力匹配。
2. **依赖字符集**:需预分配移动表空间(ASCII 为 256,Unicode 需优化)[3](@ref)。
#### 🛠️ **适用场景**:

- **文本编辑器**:快速查找/替换(如 Sublime Text)。
- **日志分析**:在大量日志中搜索关键词。
- **生物信息学**:基因序列匹配(ATCG 字符集小,跳跃高效)[4,7](@ref)。


------
### 💻 **代码实现(Python)**

def sunday_search(text, pattern): m, n = len(pattern), len(text) if m == 0: return 0

# 构建移动表(默认步长 m+1)
shift = [m + 1] * 256  # ASCII 字符集
for i, char in enumerate(pattern):
    shift[ord(char)] = m - i  # 更新字符步长

pos = 0
while pos <= n - m:
    # 尝试匹配
    j = 0
    while j < m and text[pos + j] == pattern[j]:
        j += 1
    if j == m:  # 匹配成功
        return pos
    
    # 计算关注字符位置
    if pos + m >= n:  # 边界检查
        break
    focus_char = text[pos + m]
    # 根据移动表跳跃
    pos += shift[ord(focus_char)]

return -1  # 未找到

测试

text = “substring searching” pattern = “search” print(sunday_search(text, pattern)) # 输出: 10



------
### 🧠 **优化策略**

1. 
   避免最坏情况:
   - 检测模式串重复性,若重复度高则切换至 KMP 算法[5](@ref)。
2. 
   动态字符集处理:
   - Unicode 文本使用哈希表替代数组存储移动表,节省空间。
3. 
   多模式匹配扩展:
   - 结合 AC 自动机,实现高效多模式匹配(如敏感词过滤)[4](@ref)。


------
### 💎 **总结**

SUNDAY 算法以 **简洁的实现** 和 **高效的跳跃能力** 在字符串匹配领域占据独特地位。其核心在于 **利用关注字符预判无效区域**,大幅减少比较次数。尽管在最坏情况下性能受限,但在大字符集、低重复模式的场景中(如自然语言、生物序列),其平均效率接近理论最优值。工程实践中,可结合场景特性动态选择匹配算法(如短模式用暴力法,长模式用 SUNDAY/BM),以达到最佳性能[3,7](@ref)。
Licensed under CC BY-NC-SA 4.0
Last updated on Jul 17, 2025 09:26 CST
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy