【Algorithms 4】算法(第4版)学习笔记 22 - 5.3 子字符串查找

前言

本篇主要内容包括:Knuth-Morris-Pratt 子字符串查找算法Boyer-Moore 字符串查找算法Rabin-Karp 指纹字符串查找算法

参考目录

  • B站 普林斯顿大学《Algorithms》视频课
    (请自行搜索。主要以该视频课顺序来进行笔记整理,课程讲述的教授本人是该书原版作者之一 Robert Sedgewick。)
  • 微信读书《算法(第4版)》
    (本文主要内容来自《5.3 子字符串查找》)
  • 官方网站
    (有书本配套的内容以及代码)

学习笔记

注1:下面引用内容如无注明出处,均是书中摘录。
注2:所有 demo 演示均为视频 PPT demo 截图。
注3:如果 PPT 截图中没有翻译,会在下面进行汉化翻译,因为内容比较多,本文不再一一说明。

1:介绍

Prof. Sedgewick 开场白:

Today we’re going to look at substring search algorithms. This is a really fascinating family of algorithms. The problem is very simple to state. And the algorithms we’re going to look at are among the most ingenious that we’ve seen, so far.
今天我们将探讨子字符串查找算法。这是一组极其引人入胜的算法集合。待解决的问题表述起来非常简洁明了。而且,迄今为止,我们将要研究的这些算法堪称是我们见过的最为精巧的算法之一。

对应书本的概述:

![image-20240326083735625]

2:暴力算法 brute force

![L19-53SubstringSearch_10]

Java 实现:

![L19-53SubstringSearch_11]

最坏的情况:

![L19-53SubstringSearch_12]

另一种实现:

![image-20240326152816458]

3:Knuth-Morris-Pratt 子字符串查找算法

3.1:介绍

But I just want to start by saying this is one of the coolest algorithms that we’ll cover in this course. And it’s not an algorithm that anyone would come up with without a lot of hard work, but understanding this algorithm really gives somebody an appreciation for what’s possible with careful algorithmic thinking, even for such a simple problem as this. It’s a quite ingenious method.
但我想先说,这个算法是我们这门课程中将要涉及的最酷炫的算法之一。并且,这不是一个人们能够轻易想出来的算法,它需要大量的辛勤工作。但是理解这个算法确实能让人们体会到,即便是面对如此简单的问题,经过深思熟虑的算法思维也能带来无限可能,并让人对其充满敬意。这确实是一个相当巧妙的方法。

举例说明:

![L19-53SubstringSearch_17]

直觉设想: 如果我们正在文本中搜索模式 “BAAAAAAAAA”。

  • 假设我们在模式中匹配了前 5 个字符,但在第 6 个字符上出现了不匹配。
  • 我们知道在文本中的前 6 个字符是 “BAAAAB”。(字母表中只有 {A, B})
  • 此时无需回溯文本指针!

Knuth-Morris-Pratt 算法: 该方法巧妙地始终避免了回退。 (!)

![image-20240327083708246]

3.2:确定有限状态自动机 Deterministic finite state automaton (DFA)

3.2.1:DFA 介绍

![image-20240327084128704]

确定有限状态自动机(DFA)是一种用于抽象字符串搜索的模型。

  • 它包含有限数量的状态(包含了起始状态和结束状态)。
  • 对于字母表中的每一个字符,都存在一个且仅有一个状态转移规则。
  • 若一系列按照字符进行的状态转移最终达到结束状态,则判定该字符串被自动机接受(即匹配成功)。

![image-20240327084318025]

3.2.2:demo 演示

初始状态:

![image-20240327084843318]

从状态 0 开始,读到字符 A:

![image-20240327085031945]

进入状态 1:

![image-20240327085150496]

![image-20240327085216973]

在状态 1 读取到 A,保持状态 1:

![image-20240327085325078]

![image-20240327085404373]

在状态 1 读取到 B,变为状态 2:

![image-20240327085535922]

在状态 2 读取到 A,变为状态 3:

![image-20240327085630750]

在状态 3 读取到 C,变为状态 0:

![image-20240327085722670]

![image-20240327085748693]

同上操作:

在状态 0 读取到 A,变为状态 1。

在状态 1 读取到 A,保持状态 1。

在状态 1 读取到 B,变为状态 2。

在状态 2 读取到 A,变为状态 3。

在状态 3 读取到 B,变为状态 4:

![image-20240327090031642]

在状态 4 读取到 A,变为状态 5:

![image-20240327090102975]

在状态 5 读取到 C,变为状态 6,完成子字符串搜索:

![image-20240327090142499]

3.2.3:KMP DFA 的解释

![L19-53SubstringSearch_21]

Q: 在读取 txt[i] 之后,DFA 状态的解读是什么?

A: 状态值等于已匹配到的模式中字符的数量。(即 pat[] 中最长前缀同时也是 txt[0…i] 后缀的长度)

3.2.4:Java 实现

edu.princeton.cs.algs4.KMP

![image-20240327141946517]

edu.princeton.cs.algs4.KMP#search

![image-20240327142013707]

3.2.5:构造演示

为每一个字符指定一个状态,再加上一个额外的接受状态:

![image-20240327142408049]

匹配转移:

![L19-53SubstringSearch_27]

非匹配转移:

对于状态 0 ,读到非字符 A,状态都为 0:

![image-20240327143709578]

对于状态 1,读到字符 A,状态不变;读到字符 C,回到状态 0:

![image-20240327143907493]

对于状态 2 ,读到非字符 A,状态都回到 0:

![image-20240327144108409]

对于状态 3,读到字符 A,回到状态 1;读到字符 C,回到状态 0:

![image-20240327144250560]

对于状态 4,读到非字符 A,状态都回到 0:

![image-20240327144513398]

对于状态 5,读到字符 A,回到状态 1;读到字符 B,回到状态 4:

![image-20240327144652050]

构造完成:

![image-20240327145952291]

3.2.6:构造解析

![L19-53SubstringSearch_28]

有点绕,以上图的案例来进行说明:

已知:j = 5,c = A|B,pat[1…j-1] = BABA。

方法:在 DFA 模拟 BABA 的路线,得到状态值为 3。

代入 c 进行计算:对于状态 3,读到字符 A,回到状态 1;读到字符 B,进入状态 4。

因此最终可知:dfa['A'][5] = 1dfa['B'][5] = 4

将模拟路线 BABA 设置为 X,可以得到:

![L19-53SubstringSearch_29]

对应书本的操作说明:

![image-20240327155214924]

3.2.7:构造演示 2

![image-20240327154334032]

初始状态与前面一致:

![image-20240327153909011]

同上,对于状态 0 ,读到非字符 A,状态都为 0:

![image-20240327154016900]

对于状态 1:

![image-20240327154058321]

![image-20240327154216891]

对于状态 2:

![image-20240327154453633]

![image-20240327154554098]

对于状态 3:

![image-20240327154654498]

![image-20240327154741883]

对于状态 4:

![image-20240327154901205]

![image-20240327154920710]

对于状态 5:

![image-20240327154949859]

![image-20240327155014774]

对于状态 6:

![image-20240327155109247]

3.2.8:Java 实现

edu.princeton.cs.algs4.KMP#KMP

![image-20240327155450940]

3.3:分析

![L19-53SubstringSearch_33]

命题: KMP 子串搜索算法在查找长度为M的模式串在长度为N的文本中时,访问的字符数不超过 M+N 个。
证明: 构建 DFA 时每个模式字符仅访问一次;模拟 DFA 时,在最坏情况下每个文本字符也仅访问一次。

命题: KMP 算法构建 dfa[][] 数组所需的时间和空间复杂度与 R 和 M 成正比。
对于更大的字符集,改进版的 KMP 能够在时间和空间复杂度均与 M 成正比的情况下构建 nfa[] 数组。

4:Boyer-Moore 字符串查找算法

4.1:介绍

![L19-53SubstringSearch_36]

直觉理解:

  • 从右向左扫描模式串中的字符。
  • 当在文本中寻找不在模式中的字符时,最多可以跳过M个文本字符。

对应书本的描述:

![image-20240327161816473]

4.2:跳过的几种案例

![L19-53SubstringSearch_37]

案例 1:不匹配字符不在模式 pattern 中。

遇到不匹配字符 ‘T’,该字符不在模式中:将文本指针 i 向后移动一位,越过字符 ‘T’。

![L19-53SubstringSearch_38]

案例 2a:不匹配字符在模式 pattern 中。

在模式中遇到不匹配字符 ‘N’ 时:将文本中的 ‘N’ 对齐到模式中最右侧的 ‘N’。

![L19-53SubstringSearch_40]

案例 2b:不匹配字符在模式 pattern 中(但启发式没有帮助)。

在模式中遇到不匹配字符 ‘E’ 时:将索引 i 增加1。

![L19-53SubstringSearch_41]

预先计算模式中字符 c 最右侧出现位置的索引。(如果字符不在模式中,则为-1)

4.3:Java 实现

edu.princeton.cs.algs4.BoyerMoore

![image-20240327170156025]

edu.princeton.cs.algs4.BoyerMoore#search

![image-20240327170211297]

4.4:分析

![L19-53SubstringSearch_43]

属性: 采用 Boyer-Moore 不匹配字符启发式算法,在长度为 N 的文本中搜索长度为 M 的模式大约需要进行约 ~ N/M 次字符比较。

最坏情况: 在某些情况下,可能会恶化至 ~ MN 次字符比较。

Boyer-Moore 变种: 通过添加一种类似 KMP 的规则以防止重复模式,可以在最坏情况下将字符比较次数提升至大约 ~3N 次。

5:Rabin-Karp 指纹字符串查找算法

5.1:介绍

![L19-53SubstringSearch_45]

基本思想是使用模数散列(或称模块化哈希)。

  • 计算模式 pat[0…M-1] 的哈希值。
  • 对于每一个 i,计算文本 txt[i…M+i-1] 子串的哈希值。
  • 如果模式哈希值等于文本子串哈希值,则进一步检查是否完全匹配。

5.2:计算哈希函数

![L19-53SubstringSearch_47]

对应书中的描述:

![image-20240327174346152]

关键思想:

![image-20240327174616643]

![image-20240327175340415]

5.3:Java 实现

edu.princeton.cs.algs4.RabinKarp

![image-20240327175658648]

![image-20240327175713529]

edu.princeton.cs.algs4.RabinKarp#search

![image-20240327175758427]

![L19-53SubstringSearch_51]

蒙特卡洛版本: 如果哈希值匹配,则返回匹配结果。

拉斯维加斯版本: 若哈希值相匹配,则进行子串实际匹配检查;若出现误报碰撞,则继续进行搜索。

5.4:分析

![L19-53SubstringSearch_52]

理论: 如果 Q 是一个足够大的随机质数(约为 M*N2),那么发生错误碰撞的概率大约为 1/N。

实践操作: 选择一个较大的质数 Q 作为基数(但不要过大以免造成溢出)。在合理的假设下,发生碰撞的概率大约为 1/Q。

蒙特卡洛版本:

  • 总是运行在线性时间内。
  • 极有可能返回正确的结果(但并非总是如此)。

拉斯维加斯版本:

  • 总是能返回正确答案。
  • 极有可能在线性时间内运行完成(但在最坏情况下时间为 M*N)。

![L19-53SubstringSearch_53]

优势:

  • 可扩展至二维模式。
  • 可用于查找多个模式。

劣势:

  • 算术运算通常比字符比较慢。
  • 拉斯维加斯版本需要进行回溯。
  • 最坏情况下的性能保证较差。

Q. 如何有效地将 Rabin-Karp 算法扩展应用,以便在长度为 N 的文本中高效搜索 P 种可能的模式之一?

6:子字符串查找成本开销小结

![image-20240327181053741]

(完)

  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

MichelleChung

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

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

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

打赏作者

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

抵扣说明:

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

余额充值