算法导论-第32章-字符串匹配

字符串匹配算法在文本文件中查找模式、DNA序列搜寻、网络引擎搜索中都有应用。

字符串匹配问题的形式化定义:假设文本是一个长为 n n n 的数组 T [ 1.. n ] T[1..n] T[1..n],而模式是一个长度为 m m m 的数组 P [ 1.. m ] P[1..m] P[1..m],其中 m ≤ n m \le n mn。字符数组 P P P T T T 通常称为字符串。

Figure 32.1

如果 0 ≤ s ≤ n − m 0 \le s \le n-m 0snm,并且 T [ s + 1.. s + m ] = P [ 1.. m ] T[s+1..s+m]=P[1..m] T[s+1..s+m]=P[1..m] (即如果 T [ s + j ] = P [ j ] T[s+j]=P[j] T[s+j]=P[j],其中 1 ≤ j ≤ m 1 \le j \le m 1jm),那么称模式 P P P 在文本 T T T 中出现,其偏移为 s s s。如果 P P P T T T 中以偏移 s s s 出现,则称 s s s有效偏移,否则,称为无效偏移

32.1节讲解朴素字符串匹配算法,32.2节讲解Rabin-Karp算法,32.3节讲解利用优先自动机进行字符串匹配,32.4节讲解Knuth-Morris-Pratt算法。

除了朴素算法外,其他字符串匹配算法都基于模式进行了预处理,然后找到所有有效偏移,我们称第二步为“匹配”。下表给出了每个算法的预处理时间和匹配时间。每个算法的总运行时间是预处理时间和匹配时间的和

算法预处理时间匹配时间
朴素算法 0 0 0 O ( ( n − m + 1 ) m ) \Omicron((n-m+1)m) O((nm+1)m)
Rabin-Karp算法 Θ ( m ) \Theta(m) Θ(m) O ( ( n − m + 1 ) m ) \Omicron((n-m+1)m) O((nm+1)m)
有限自动机算法 O ( m ∑ ) \Omicron(m\sum) O(m) Θ ( n ) \Theta(n) Θ(n)
Knuth-Morris-Pratt Θ ( m ) \Theta(m) Θ(m) Θ ( n ) \Theta(n) Θ(n)

符号和术语

  • ∑ ∗ \sum^* 表示包含所有有限长度的字符串集合, ∑ \sum 表示字母表;
  • ∣ x ∣ |x| x 表示字符串 x x x 的长度;
  • 两个字符串 x x x y y y 的连结用 x y xy xy 表示,长度为 ∣ x ∣ + ∣ y ∣ |x|+|y| x+y,由 x x x 的字符后接 y y y 的字符构成;
  • 字符串 w w w 是字符串 x x x 的前缀,记作 w ⊏ x w \sqsubset x wx;字符串 w w w 是字符串 x x x 的后缀,记作 w ⊐ x w \sqsupset x wx
  • 空字符串 ε \varepsilon ε 是任何一个字符串的前缀和后缀。

后缀重叠引理:假设 x , y x,y x,y z z z 是满足 x ⊐ z x \sqsupset z xz y ⊐ z y \sqsupset z yz 的字符串。如果 ∣ x ∣ ≤ ∣ y ∣ |x| \le |y| xy,那么 x ⊐ y x \sqsupset y xy;如果 ∣ x ∣ ≥ ∣ y ∣ |x| \ge |y| xy,那么 y ⊐ x y \sqsupset x yx;如果 ∣ x ∣ = ∣ y ∣ |x| = |y| x=y,那么 x = y x = y x=y

Figure 32.2

为了符号简单,我们把模式 P [ 1.. m ] P[1..m] P[1..m] 由前 k k k 个字符组成的前缀 P [ 1.. k ] P[1..k] P[1..k] 记作 P k P_k Pk。因此 P 0 = ε , P m = P = P [ 1.. m ] P_0=\varepsilon,P_m=P=P[1..m] P0=ε,Pm=P=P[1..m]

32.1 朴素字符串匹配算法

朴素字符串匹配算法是通过一个循环找到所有有效偏移,判断条件为 P [ 1.. m ] = T [ s + 1.. s + m ] P[1..m] = T[s+1..s+m] P[1..m]=T[s+1..s+m],其中 s s s 总共有 n − m + 1 n-m+1 nm+1 个可能的值。

NAIVE-STRING-MATCHER(T, P)
    n = T.length
    m = P.length
    for s = 0 to n-m
        if P[1..m] == T[s+1..s+m]
            print "Pattern occurs with shift" s

朴素字符串匹配过程可以看作“模式”沿着“文本”滑动(偏移),每次偏移都要检测模式中的字符和文本中对应的字符是否相等。

Figure 32.3

在最坏情况下,朴素字符串匹配算法运行时间为 O ( ( n − m + 1 ) m ) \Omicron((n-m+1)m) O((nm+1)m)。偏移需要 n − m + 1 n-m+1 nm+1 次,每次偏移,都要将模式 P P P 中的 m m m 个字符进行比较。

32.2 Rabin-Karp算法

在实际应用中,Rabin-Karp所提出的字符串匹配算法能够较好的运行, 并且还可以从中归纳出相关问题的其他算法,比如二维模式匹配。Rabin-Karp算法的预处理时间为 Θ ( m ) \Theta(m) Θ(m),并且在最坏情况下,它的运行时间为 Θ ( ( n − m + 1 ) m ) \Theta((n-m+1)m) Θ((nm+1)m)。基于一些假设,它的运行时间还是比较好的。

《算法导论》第3版,这部分给我看懵了,建议看这个:https://algo.itcharge.cn/06.String/02.String-Single-Pattern-Matching/02.String-Rabin-Karp/

算法步骤:

  1. 对于给定的文本串 T T T 与模式串 P P P,求出文本串 T T T 的长度为 n n n,模式串 P P P 的长度为 m m m
  2. 通过滚动哈希算法求出模式串 P P P 的哈希值 h a s h _ p hash\_p hash_p
  3. 再通过滚动哈希算法对文本串 T T T n − m + 1 n - m + 1 nm+1 个子串分别求哈希值 h a s h _ t hash\_t hash_t
  4. 然后逐个与模式串的哈希值比较大小。
    1. 如果当前子串的哈希值 h a s h _ t hash\_t hash_t 与模式串的哈希值 h a s h _ p hash\_p hash_p 不同,则说明两者不匹配,则继续向后匹配。
    2. 如果当前子串的哈希值 h a s h _ t hash\_t hash_t 与模式串的哈希值 h a s h _ p hash\_p hash_p 相等,则验证当前子串和模式串的每个字符是否真的相等(避免哈希冲突)。
      1. 如果当前子串和模式串的每个字符相等,则说明当前子串和模式串匹配。
      2. 如果当前子串和模式串的每个字符不相等,则说明两者不匹配,继续向后匹配。
  5. 比较到末尾,如果仍未成功匹配,则说明文本串 T T T 中不包含模式串 P P P,方法返回 − 1 -1 1

实现 Rabin-Karp 算法中一个重要步骤是 「滚动哈希算法」,通过滚动哈希算法,将每次计算子串哈希值的复杂度从 O ( m ) \Omicron(m) O(m) 降到了 O ( 1 ) \Omicron(1) O(1),从而提升了整个算法效率。

Rabin-Karp 算法中的滚动哈希算法主要是利用了 「Rabin fingerprint 思想」。这种算法思想利用了子串中每一位字符的哈希值,并且还可以根据上一个子串的哈希值,快速计算相邻子串的哈希值,从而使得每次计算子串哈希值的时间复杂度降为了 O ( 1 ) \Omicron(1) O(1)

下面我们用一个例子来解释一下这种算法思想。

假设给定的字符串的字符集中只包含 d d d 种字符,那么我们就可以用一个 d d d 进制数表示子串的哈希值。

举个例子,假如字符串只包含 a . . z a..z a..z 26 26 26 个小写字母,那么我们就可以用 26 26 26 进制数来表示一个字符串, a a a 表示为 0 0 0 b b b 表示为 1 1 1,以此类推, z z z 就用 25 25 25 表示。

比如 c a t cat cat 的哈希值就可以表示为:
H a s h ( c a t ) = c × 26 × 26 + a × 26 + t × 1 = 2 × 26 × 26 + 0 × 26 + 19 × 1 = 1371 Hash(cat)=c \times 26 \times 26 + a \times 26 + t \times 1 \\ =2 \times 26 \times 26 + 0 \times 26 + 19 \times 1 \\ =1371 Hash(cat)=c×26×26+a×26+t×1=2×26×26+0×26+19×1=1371

这里为什么没有像书上那样取模?后面给出

这种按位计算哈希值的哈希函数有一个特点:在计算相邻子串时,可以利用上一个子串的哈希值。

比如说 c a t cat cat 的相邻子串为 a t e ate ate。按照刚才哈希函数计算,可以得出 a t e ate ate 的哈希值为:
H a s h ( a t e ) = a × 26 × 26 + t × 26 + e × 1 = 0 × 26 × 26 + 19 × 26 + 4 × 1 = 498 Hash(ate)=a \times 26 \times 26 + t \times 26 + e \times 1 \\ =0 \times 26 \times 26 + 19 \times 26 + 4 \times 1 \\ =498 Hash(ate)=a×26×26+t×26+e×1=0×26×26+19×26+4×1=498
如果利用上一个子串 c a t cat cat 的哈希值计算 a t e ate ate,则 a t e ate ate 的哈希值为:
H a s h ( a t e ) = ( H a s h ( c a t ) − c × 26 × 26 ) ∗ 26 + e × 26 = ( 1371 − 2 × 26 × 26 ) × 26 + 4 × 1 = 498 Hash(ate)=(Hash(cat)-c \times 26 \times 26)*26+e \times 26 \\ =(1371-2 \times 26 \times 26) \times 26 + 4 \times 1 \\ =498 Hash(ate)=(Hash(cat)c×26×26)26+e×26=(13712×26×26)×26+4×1=498
可以看出,这两种方式计算出的哈希值是相同的。但是第二种计算方式不需要再遍历子串,只需要进行一位字符的计算即可得出整个子串的哈希值。这样每次计算子串哈希值的时间复杂度就降到了 O ( 1 ) \Omicron(1) O(1)。然后我们就可以通过滚动哈希算法快速计算出子串的哈希值了。

我们将上面的规律扩展总结一下。

给定的文本串 T T T 与模式串 P P P,求出文本串 T T T 的长度为 n n n,模式串 P P P 的长度为 m m m。字符串字符种类数为 d d d,则:

  • 模式串 P P P 的哈希值计算方式为: H a s h ( P ) = p 0 × d m − 1 + p 1 × d m − 2 + ⋯ + p m − 1 × d 0 Hash(P) = p_0 \times d^{m-1} + p_1 \times d^{m-2} + \cdots + p_{m-1} \times d^{0} Hash(P)=p0×dm1+p1×dm2++pm1×d0
  • 文本串中起始于位置 0 0 0,长度为 m m m 的子串 T [ 0.. m − 1 ] T[0..m-1] T[0..m1] 对应哈希值计算方法为: H a s h ( T [ 0 , m − 1 ] ) = T 0 × d m − 1 + T 1 × d m − 2 + ⋯ + T m − 1 × d 0 Hash(T_{[0, m-1]}) = T_0 \times d^{m-1} + T_1 \times d^{m-2} + \cdots + T_{m-1} \times d^0 Hash(T[0,m1])=T0×dm1+T1×dm2++Tm1×d0
  • 已知子串的哈希值 H a s h ( T [ i , i + m − 1 ] ) Hash(T_{[i, i + m - 1]}) Hash(T[i,i+m1]),将子串向右移动一位的子串对应哈希值计算方法为: H a s h ( T [ i + 1 , i + m ] ) = [ H a s h ( T [ i , i + m − 1 ] ) − T i × d m − 1 ] × d + T i + m × d 0 Hash(T_{[i+1, i+m]}) = [Hash(T_{[i, i + m - 1]}) - T_i \times d^{m-1}] \times d + T_{i+m} \times d^0 Hash(T[i+1,i+m])=[Hash(T[i,i+m1])Ti×dm1]×d+Ti+m×d0

因为哈希值过大会造成溢出,所以我们在计算过程中还要对结果取模。取模的值应该尽可能大,并且应该是质数,这样才能减少哈希碰撞的概率。

Rabin-Karp的伪代码如下:

Rabin-Karp-Matcher

运行过程如下图:

Figure 32.4

这里的计算相比上面多了求模!

32.3 利用有限自动机进行字符串匹配

很多字符串匹配算法都要建立一个有限自动机,它是一个处理信息的简单机器,通过对文本字符串 T T T 进行扫描,找出模式 P P P 的所有出现位置。

该有限自动机非常高效:仅检查每个文本字符一次且每次检查时间为常数,首先通过预处理模式构建自动机,预处理时间为 O ( m ∣ ∑ ∣ ) \Omicron(m|\sum|) O(m) ,然后对文本进行匹配,匹配时间为 Θ ( n ) \Theta(n) Θ(n) 。本节将介绍通过构建有限自动机进行字符串匹配。

有限自动机

一个有限自动机 M M M 是一个 5 元组 ( Q , q 0 , A , ∑ , δ ) (Q, q_0, A, \sum, \delta) (Q,q0,A,,δ),其中:

  • Q Q Q状态的有限集合。
  • q 0 ∈ Q q_0 \in Q q0Q初始状态
  • A ⊆ Q A \subseteq Q AQ 是一个特殊的接受状态的集合。
  • ∑ \sum 是有限输入字母表
  • δ \delta δ 是一个从 Q × ∑ Q \times \sum Q× Q Q Q 的函数,称为 M M M转移函数

有限自动机开始处于状态 q 0 q_0 q0,每次读入输入字符串的一个字符。如果有限自动机在状态 q q q 时读入了字符 a a a,则它从状态 q q q 变为状态 δ ( q , a ) \delta(q, a) δ(q,a)(进行了一次转移)。当前状态 q q q 属于 A A A 时,就说有限自动机 M M M 接受了迄今为止所读入的字符串。没有被接受的输入称为被拒绝的输入。

Figure 32.5

上图是一个简单的两状态自动机。

  • 状态集 Q = { 0 , 1 } Q=\{0, 1\} Q={0,1},表示有两种状态 0 0 0 1 1 1
  • 初始状态 q 0 = 0 q_0=0 q0=0
  • 状态 1 1 1 (橙红色)是唯一的接受状态,有向边代表着转换,对应下面的转移函数。
  • 字母表 ∑ = { a , b } \sum=\{a, b\} ={a,b}
  • 上图中的表格表示转移函数 δ \delta δ,例如, δ ( 0 , a ) = 1 \delta(0, a) = 1 δ(0,a)=1

例如,对于输入 a b a a a abaaa abaaa,包括初始状态,这个自动机的输入状态序列为 < 0 , 1 , 0 , 1 , 0 , 1 > <0, 1, 0, 1, 0, 1> <0,1,0,1,0,1>,因此它接受这个输入。

计算过程:

  1. 初始状态 q 0 = 0 q_0 = 0 q0=0
  2. 查表得 δ ( 0 , a ) = 1 \delta(0, a) = 1 δ(0,a)=1
  3. 继续, δ ( 1 , b ) = 0 \delta(1, b)=0 δ(1,b)=0
  4. 继续, δ ( 0 , a ) = 1 \delta(0, a) = 1 δ(0,a)=1
  5. 继续, δ ( 1 , a ) = 0 \delta(1, a) = 0 δ(1,a)=0
  6. 继续, d e l t a ( 0 , a ) = 1 delta(0, a) = 1 delta(0,a)=1

与自动机得输入状态序列相同,接受!

如果输入是 a b b a a abbaa abbaa,自动机输入状态序列为 < 0 , 1 , 0 , 0 , 1 , 0 > <0, 1, 0, 0, 1, 0> <0,1,0,0,1,0>,因此拒绝这个输入。

有限自动机 M M M 引入了一个终态函数 ϕ ( w ) \phi(w) ϕ(w),满足 ϕ ( w ) \phi(w) ϕ(w) M M M 在扫描字符串 w w w 后终止时得状态。

字符串匹配自动机

Figure 32.6

上图(a)是一个字符串匹配自动机的状态转换图,它可以接受所有以字符串 a b a b a c a ababaca ababaca 结尾的字符串。状态 0 0 0 是初始状态,状态 7 7 7 是仅有的接受状态。从状态 i i i 到状态 j j j,标有 a a a 的有向边 a a a 记为 δ ( i , a ) = j \delta(i, a) = j δ(i,a)=j

上图(b)对应的转移函数 δ \delta δ 和模式字符串 P = a b a b a c a P=ababaca P=ababaca。对应着模式和输入字符串之间成功匹配(蓝色部分)。

上图(c)自动机在文本 T = a b a b a b a c a b a T=abababacaba T=abababacaba 上的操作。在处理了前缀 T i T_i Ti 之后,在每个文本字符 T [ i ] T[i] T[i] 下面,给出了它在自动机内的状态 ϕ ( T i ) \phi(T_i) ϕ(Ti)

以上图中的自动机和文本 T = a b a b a b a c a b a T=abababacaba T=abababacaba 为例,计算过程如下:

  1. 初始状态 q 0 = 0 q_0 = 0 q0=0
  2. 查表得, δ ( 0 , a ) = 1 \delta(0, a) = 1 δ(0,a)=1
  3. 继续, δ ( 1 , b ) = 2 \delta(1, b) = 2 δ(1,b)=2
  4. 继续, δ ( 2 , a ) = 3 \delta(2, a) = 3 δ(2,a)=3
  5. 继续, δ ( 3 , b ) = 4 \delta(3, b) = 4 δ(3,b)=4
  6. 继续, δ ( 4 , a ) = 5 \delta(4, a) = 5 δ(4,a)=5
  7. 继续, δ ( 5 , b ) = 4 \delta(5, b) = 4 δ(5,b)=4
  8. 继续, δ ( 4 , a ) = 5 \delta(4, a) = 5 δ(4,a)=5
  9. 继续, δ ( 5 , c ) = 6 \delta(5, c) = 6 δ(5,c)=6
  10. 继续, δ ( 6 , a ) = 7 \delta(6, a) = 7 δ(6,a)=7
  11. 继续, δ ( 7 , b ) = 2 \delta(7, b) = 2 δ(7,b)=2
  12. 继续, δ ( 2 , a ) = 3 \delta(2, a) = 3 δ(2,a)=3

转移函数 δ \delta δ 计算出得结果即为 ϕ ( T i ) \phi(T_i) ϕ(Ti)

利用生成好的状态机进行字符串匹配,很简单。复杂的是如何生成该状态机?

32.4 Knuth-Morris-Pratt算法(KMP算法)

KMP算法是由Knuth、Morris和Pratt三人设计的线性时间字符串匹配算法。这个算法无需计算转移函数 δ \delta δ,匹配时间为 Θ ( n ) \Theta(n) Θ(n),只用到辅助函数 π \pi π,它在 Θ ( m ) \Theta(m) Θ(m) 时间内根据模式预先计算出来,并且存储在数组 π [ 1.. m ] \pi[1..m] π[1..m] 中。

KMP 算法思想:对于给定文本串 T T T 与模式串 P P P,当发现文本串 T T T 的某个字符与模式串 P P P 不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。

32.4.1 朴素算法的缺陷

在朴素匹配算法的匹配过程中,我们分别用指针 i i i 和指针 j j j 指示文本串 T T T 和模式串 P P P 中当前正在对比的字符。当发现文本串 T T T 的某个字符与模式串 P P P 不匹配的时候, j j j 回退到开始位置, i i i 回退到之前匹配开始位置的下一个位置上,然后开启新一轮的匹配,如图所示。

img

在 Brute Force 算法中,如果从文本串 T [ i ] T[i] T[i] 开始的这一趟字符串比较失败了,算法会直接开始尝试从 T [ i + 1 ] T[i+1] T[i+1]开始比较。如果 i i i 已经比较到了后边位置,则该操作相当于将指针 i i i 进行了回退操作。

那么有没有哪种算法,可以让 i i i 不发生回退,一直向右移动呢?

32.4.2 KMP算法的改进

如果我们可以通过每一次的失配而得到一些「信息」,并且这些「信息」可以帮助我们跳过那些不可能匹配成功的位置,那么我们就能大大减少模式串与文本串的匹配次数,从而达到快速匹配的目的。

每一次失配所告诉我们的信息是:主串的某一个子串等于模式串的某一个前缀

这个信息的意思是:如果文本串 T [ i : i + m ] T[i: i + m] T[i:i+m] 与模式串 P P P 的失配是下标位置 j j j 上发生的,那么文本串 T T T 从下标位置 i i i 开始连续的 j − 1 j - 1 j1 个字符,一定与模式串 p p p 的前 j − 1 j - 1 j1 个字符一模一样,即: T [ i : i + j ] = = p [ 0 : j ] T[i: i + j] == p[0: j] T[i:i+j]==p[0:j]

但是知道这个信息有什么用呢?

以刚才图中的例子来说,文本串的子串 T [ i : i + m ] T[i: i + m] T[i:i+m] 与模式串 P P P 的失配是在第 5 5 5 个位置发生的,那么:

  • 文本串 T T T 从下标位置 i i i 开始连续的 5 5 5 个字符,一定与模式串 P P P 的前 5 5 5 个字符一模一样,即: A B C A B = = A B C A B ABCAB == ABCAB ABCAB==ABCAB
  • 而模式串的前 5 5 5 个字符中,前 2 2 2 位前缀和后 2 2 2 位后缀又是相同的,即 A B = = A B AB == AB AB==AB

所以根据上面的信息,我们可以推出:文本串子串的后 2 2 2 位后缀和模式串子串的前 2 2 2 位是相同的,即 T [ i + 3 : i + 5 ] = = p [ 0 : 2 ] T[i + 3: i + 5] == p[0: 2] T[i+3:i+5]==p[0:2],而这部分(即下图中的蓝色部分)是之前已经比较过的,不需要再比较了,可以直接跳过。

那么我们就可以将文本串中的 T [ i + 5 ] T[i + 5] T[i+5] 对准模式串中的 p [ 2 ] p[2] p[2],继续进行对比。这样 i i i 就不再需要回退了,可以一直向右移动匹配下去。在这个过程中,我们只需要将模式串 j j j 进行回退操作即可。

img

KMP 算法就是使用了这样的思路,对模式串 P P P 进行了预处理,计算出一个 「部分匹配表」,用一个数组 π \pi π 来记录。然后在每次失配发生时,不回退文本串的指针 i i i,而是根据「部分匹配表」中模式串失配位置 j j j 的前一个位置的值,即 π [ j − 1 ] \pi[j - 1] π[j1] 的值来决定模式串可以向右移动的位数。

比如上述示例中模式串 P P P 是在 j = 5 j = 5 j=5 的位置上发生失配的,则说明文本串的子串 T [ i : i + 5 ] T[i: i + 5] T[i:i+5] 和模式串 P [ 0 : 5 ] P[0: 5] P[0:5] 的字符是一致的,即 A B C A B = = A B C A B ABCAB == ABCAB ABCAB==ABCAB。而根据「部分匹配表」中 n e x t [ 4 ] = = 2 next[4] == 2 next[4]==2,所以不用回退 i i i,而是将 j j j 移动到下标为 2 2 2 的位置,让 T [ i + 5 ] T[i + 5] T[i+5] 直接对准 p [ 2 ] p[2] p[2],然后继续进行比对。

32.4.3 π \pi π 数组(next数组)

上文提到的「部分匹配表」,也叫做「前缀表」,在 KMP 算法中使用 π \pi π 数组存储。 π [ j ] \pi[j] π[j] 表示的含义是:记录下标 j j j 之前(包括 j j j)的模式串 P P P 中,最长的相等的前缀和后缀的长度

认真体会这句话,最长的 相等的 前缀和后缀 的长度

  • 前缀后缀:指的是模式串 P P P 的当前子串的前缀和后缀; 例如, P = A B C A B C D P=ABCABCD P=ABCABCD,其中的子串“ABCAB”,一组前缀为“AB”,后缀为“AB”;另一组前缀为“ABC”,后缀为“CAB”。
  • 相等的:要求前缀字符串和后缀字符串相等;还是上面的例子,相等的一组前缀和后缀是“AB”
  • 最长的长度:要求的是前缀==后缀,最长的那组前缀和后缀。还是上面的例子,最大长度为2。

简单而言,就是求:模式串 P P P 的子串 P [ 0 : j + 1 ] P[0: j + 1] P[0:j+1] 中,使得「前 k k k 个字符」恰好等于「后 k k k 个字符」的「最长的 k k k

举个例子来说明一下,以 P = A B C A B C P = ABCABC P=ABCABC 为例。

  • π [ 0 ] = 0 \pi[0] = 0 π[0]=0,因为 “A” 中无有相同前缀后缀,最大长度为 0 0 0
  • π [ 1 ] = 0 \pi[1] = 0 π[1]=0,因为 “AB” 中无相同前缀后缀,最大长度为 0 0 0
  • π [ 2 ] = 0 \pi[2] = 0 π[2]=0,因为 “ABC” 中无相同前缀后缀,最大长度为 0 0 0
  • π [ 3 ] = 1 \pi[3] = 1 π[3]=1,因为 “ABCA” 中有相同的前缀后缀 “a”,最大长度为 1 1 1
  • π [ 4 ] = 2 \pi[4] = 2 π[4]=2,因为 “ABCAB” 中有相同的前缀后缀 “AB”,最大长度为 2 2 2
  • π [ 5 ] = 3 \pi[5] = 3 π[5]=3,因为 “ABCABC” 中有相同的前缀后缀 “ABC”,最大长度为 3 3 3
  • π [ 6 ] = 0 \pi[6] = 0 π[6]=0,因为 “ABCABCD” 中无相同前缀后缀,最大长度为 0 0 0

同理也可以计算出

  • “ABCABDEF” 的前缀表为 [ 0 , 0 , 0 , 1 , 2 , 0 , 0 , 0 ] [0, 0, 0, 1, 2, 0, 0, 0] [0,0,0,1,2,0,0,0]
  • “AABAAAB” 的前缀表为 [ 0 , 1 , 0 , 1 , 2 , 2 , 3 ] [0, 1, 0, 1, 2, 2, 3] [0,1,0,1,2,2,3]
  • “ABCDABD” 的前缀表为 [ 0 , 0 , 0 , 0 , 1 , 2 , 0 ] [0, 0, 0, 0, 1, 2, 0] [0,0,0,0,1,2,0]

在之前的例子中,当 P [ 5 ] P[5] P[5] T [ i + 5 ] T[i + 5] T[i+5] 匹配失败后,根据模式串失配位置 j j j 的前一个位置的值,即 n e x t [ 4 ] = 2 next[4] = 2 next[4]=2,我们直接让 T [ i + 5 ] T[i + 5] T[i+5] 直接对准了 P [ 2 ] P[2] P[2],然后继续进行比对,如下图所示。

img

32.4.4 算法步骤

  1. 根据 n e x t next next 数组的构造步骤生成「前缀表」 n e x t next next

    https://blog.csdn.net/v_JULY_v/article/details/7041827?spm=1001.2014.3001.5502

  2. 使用两个指针 i i i j j j,其中 i i i 指向文本串中当前匹配的位置, j j j 指向模式串中当前匹配的位置。初始时, i = 0 i = 0 i=0 j = 0 j = 0 j=0

  3. 循环判断模式串前缀是否匹配成功,如果模式串前缀匹配不成功,将模式串进行回退,即 j = n e x t [ j − 1 ] j = next[j - 1] j=next[j1],直到 j = = 0 j == 0 j==0 时或前缀匹配成功时停止回退。

  4. 如果当前模式串前缀匹配成功,则令模式串向右移动 1 1 1 位,即 j + = 1 j += 1 j+=1

  5. 如果当前模式串 完全 匹配成功,则返回模式串 P P P 在文本串 T T T 中的开始位置,即 i − j + 1 i - j + 1 ij+1

  6. 如果还未完全匹配成功,则令文本串向右移动 1 1 1 位,即 i + = 1 i += 1 i+=1,然后继续匹配。

  7. 如果直到文本串遍历完也未完全匹配成功,则说明匹配失败,返回 − 1 -1 1

KMP-Matcher

参考

  • https://algo.itcharge.cn/
posted @ 2023-07-02 21:59  gengduc  阅读(26)  评论(0编辑  收藏  举报  来源