KMP算法

字符串匹配的KMP算法 - 阮一峰的网络日志

The Knuth-Morris-Pratt Algorithm in my own words - jBoxer

image

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

 - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

 

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

image

 

9.1 Knuth-Morris-Pratt KMP String Matching Algorithm

s=aaaaaaab

t=aaab

用基础的逐字符暴力比较算法,假设s的字符串长度是n, t的字符串长度是m。算法的时间复杂度是n*m

 

t=abcdabc

prefix:
suffix:

KMP 算法 中,LPSLongest Prefix Suffix(最长前缀后缀)的缩写。它是 KMP 算法中用于优化匹配过程的核心概念之一。

 

在计算机科学中,特别是字符串匹配算法(如KMP算法)中,LPS通常指的是**最长公共前后缀(Longest Proper Prefix which is also a Suffix)**数组。

对于字符串 $S$,LPS 数组中的第 $i$ 个元素 $LPS[i]$ 表示子字符串 $S[0 \dots i]$ 最长的那个真前缀,同时它也是 $S[0 \dots i]$后缀的长度。

对于字符串 $S = \text{"abaabc"}$,我们来计算它的 LPS 数组:

i 字符 S[i] 子字符串 S[0…i] 最长公共前后缀(长度) LPS[i] 解释
0 a "a" 0 真前缀和后缀都为空。
1 b "ab" 0 真前缀有:"a";后缀有:"b"。没有公共。
2 a "aba" 1 真前缀有:"a", "ab";后缀有:"a", "ba"。最长公共是 "a",长度为 1。
3 a "abaa" 1 真前缀有:"a", "ab", "aba";后缀有:"a", "aa", "baa"。最长公共是 "a",长度为 1。
4 b "abaab" 2 真前缀有:"a", "ab", "aba", "abaa";后缀有:"b", "ab", "aab", "baab"。最长公共是 "ab",长度为 2。
5 c "abaabc" 0 真前缀有:"a", "ab", "aba", "abaa", "abaab";后缀有:"c", "bc", "abc", "aabc", "baabc"。没有公共。

 

字符串s=abaabaabaca

t=abaabc

第一次匹配失败的时候,i和j分别是多少?

下一次匹配的时候,i和j分别是多少

这个问题考察的是KMP(Knuth-Morris-Pratt)字符串匹配算法的核心机制,特别是匹配失败时的回溯(next/LPS 数组的应用)

我们使用以下标记:

  • $S$: 主字符串($\text{"abaabaabaca"}$

  • $T$: 模式字符串($\text{"abaabc"}$

  • $i$: $S$ 字符串的当前匹配位置(指针)

  • $j$: $T$ 字符串的当前匹配位置(指针)

  • $LPS$ 数组: $T$ 模式字符串的最长公共前后缀数组。

这个问题考察的是KMP(Knuth-Morris-Pratt)字符串匹配算法的核心机制,特别是匹配失败时的回溯(next/LPS 数组的应用)

我们使用以下标记:

  • $S$: 主字符串($\text{"abaabaabaca"}$

  • $T$: 模式字符串($\text{"abaabc"}$

  • $i$: $S$ 字符串的当前匹配位置(指针)

  • $j$: $T$ 字符串的当前匹配位置(指针)

  • $LPS$ 数组: $T$ 模式字符串的最长公共前后缀数组。

 

1. 计算 $T$ 的 LPS 数组

 

首先,我们需要计算模式字符串 $T = \text{"abaabc"}$LPS 数组(或称 $next$ 数组):

索引 j 字符 T[j] 前缀 T[0…j] 最长公共前后缀(长度) LPS[j]
0 a "a" 0
1 b "ab" 0
2 a "aba" 1
3 a "abaa" 1
4 b "abaab" 2
5 c "abaabc" 0

$LPS = [0, 0, 1, 1, 2, 0]$


 

第一次匹配失败时:

  • $i$ 停留在 $S$ 中不匹配的位置:$i = 5$

  • $j$ 停留在 $T$ 中不匹配的位置:$j = 5$


 

3. 下一次匹配时的 $i$$j$

 

当匹配失败时(在 $j=5$ 处失败),KMP 算法利用 $LPS$ 数组来确定 $T$ 模式字符串应该从哪个位置开始新的匹配,而主串指针 $i$ 不回溯

  1. 计算 j 的新值:

    新的 j 值由失败位置的前一个位置 j−1 的 LPS 值决定。

     

    $$\text{新的 } j = LPS[j-1]$$
    • 失败时的 $j = 5$

    • $j-1 = 4$

    • $LPS[4] = 2$

    • 新的 $j = 2$

    这意味着在 $T[0 \dots 4] = \text{"abaab"}$ 中,最长公共前后缀是 $\text{"ab"}$,长度为 2。我们已经知道 $S[i-j \dots i-1]$$\text{"abaab"}$ 匹配,所以现在我们知道 $S[i-2 \dots i-1]$ 已经和 $T[0 \dots 1]$ 匹配。因此,我们从 $T[2]$ ($\text{'a'}$) 处开始尝试匹配 $S[5]$ ($\text{'a'}$)。

  2. 确定 i 的值:

    在 KMP 算法中,主串指针 i 在匹配失败时保持不变,继续指向 S 中发生不匹配的位置。

    • 新的 $i = 5$

下一次匹配时:

  • $i = 5$

  • $j = 2$

 

对于字符串 "aaab",让我逐步计算其LPS数组:

LPS数组的定义:LPS[i] 表示子串 s[0...i] 的最长相等前后缀的长度(前缀和后缀不能是整个字符串本身)。

计算过程

  • i=0 ('a'):子串 "a"
    • 没有真前后缀
    • LPS[0] = 0
  • i=1 ('a'):子串 "aa"
    • 前缀:"a",后缀:"a" ✓
    • LPS[1] = 1
  • i=2 ('a'):子串 "aaa"
    • 前缀:"aa",后缀:"aa" ✓
    • LPS[2] = 2
  • i=3 ('b'):子串 "aaab"
    • 前缀:"a", "aa", "aaa"
    • 后缀:"b", "ab", "aab"
    • 没有匹配
    • LPS[3] = 0

答案

 
 
字符串: a  a  a  b
索引:   0  1  2  3
LPS:    0  1  2  0

所以 "aaab" 的LPS数组是 [0, 1, 2, 0]

 

ababaaababaa
0 a 0
1 ab 0 
2 aba 1
3 abab 2
4 ababa 3
5 ababaa 1
6 ababaaa 1
7 ababaaab 2
8 ababaaaba 3
9 ababaaabab 4
10 ababaaababa 5
11 ababaaababaa 1

 

 

LPS 的优化数组”通常指的是 KMP 算法中的 $next$ 数组的 改进/优化版本,也常被称为 $nextval$ 数组(或 $next'$ 数组)。

 

1. 为什么需要优化?(LPS 数组的缺陷)

 

标准的 LPS 数组(或 $next$ 数组)虽然能防止主串回溯,但在某些极端情况下,模式串的回溯仍然可能发生多次无效比较

缺陷示例:

假设模式串 T="aaaaab",其 LPS 数组为 [0,1,2,3,4,0]。

现在主串 $S$ 正在匹配,在 $T[5]=\text{'b'}$ 处失败(假设 $S[i]=\text{'c'}$)。

  1. 匹配失败于 $j=5$.

  2. 回溯到 $j = LPS[4] = 4$. ($T[4]=\text{'a'}$)

  3. 比较 $S[i]=\text{'c'}$$T[4]=\text{'a'}$失败! (无效比较:$S[i]$ 仍然是一个新的不匹配字符,它不可能与 $T[4]$ 匹配,因为 $T[4]$$T[5]$ 字符相同都是 'a',而 $T[5]$ 已经失败了)

  4. 回溯到 $j = LPS[3] = 3$. ($T[3]=\text{'a'}$)

  5. 比较 $S[i]=\text{'c'}$$T[3]=\text{'a'}$失败!

  6. ...直到 $j=0$ 失败。

这种情况下,每次回溯到的字符都与原失败字符 $T[5]$ 相同(都是 'a'),导致一系列冗余且注定失败的比较。

 

2. LPS 的优化数组($nextval$ 数组)

 

优化数组($nextval$ 数组) 的目标是消除这种冗余的比较。

定义:

对于 T 中的任何位置 j,nextval[j] 存储的是:

  1. 如果 $T[j] \ne T[nextval[j]]$,则 $nextval[j] = LPS[j]$ (即标准回溯位置)。

  2. 如果 $T[j] = T[nextval[j]]$,则 $nextval[j] = nextval[nextval[j]]$ (继续回溯,直到找到一个与 $T[j]$ 不同的字符位置 $k$)。

优化后的回溯逻辑:

当匹配在 T[j] 处失败时,模式串直接回溯到 T[nextval[j]] 处继续匹配。由于 T[nextval[j]]=T[j],可以确保回溯后的第一次比较不是冗余的。

 

3. 示例:$T = \text{"aaaaab"}$$nextval$

 

T=a a a a a b

LPS=[0,1,2,3,4,0]

j T[j] LPS[j] nextval[j] 计算 nextval[j]
0 a 0 - 0
1 a 1 $T[1] (\text{'a'}) = T[LPS[1]=1] (\text{'a'})$? Yes. $\rightarrow nextval[1] = nextval[LPS[1]] = nextval[1]$. (循环定义,通常 $nextval[1]=0$) 0
2 a 2 $T[2] (\text{'a'}) = T[LPS[2]=2] (\text{'a'})$? Yes. $\rightarrow nextval[2] = nextval[LPS[2]] = nextval[2]$. (循环定义,通常 $nextval[2]=0$) 0
3 a 3 $T[3] (\text{'a'}) = T[LPS[3]=3] (\text{'a'})$? Yes. $\rightarrow nextval[3] = nextval[LPS[3]] = nextval[3]$. (循环定义,通常 $nextval[3]=0$) 0
4 a 4 $T[4] (\text{'a'}) = T[LPS[4]=4] (\text{'a'})$? Yes. $\rightarrow nextval[4] = nextval[LPS[4]] = nextval[4]$. (循环定义,通常 $nextval[4]=0$) 0
5 b 0 $T[5] (\text{'b'}) = T[LPS[5]=0] (\text{'a'})$? No. $\rightarrow nextval[5] = LPS[5]$. 0

最终 $nextval$ 数组 (为简化,通常 $nextval[0]$ 设为 0): $[0, 0, 0, 0, 0, 0]$

优化后的回溯:

在 T[5]=’b’ 处失败,回溯到 j=nextval[5]=0.

直接比较 S[i]=’c’ 和 T[0]=’a’。只有一次比较,大大减少了冗余操作。

简而言之,LPS 的优化数组$nextval$ 数组)就是通过跳过那些与失败字符相同的公共前后缀,来提高 KMP 算法的实际执行效率

 

 

!从KMP算法的实际使用角度来看,LPS数组的最后一位在模式匹配过程中确实不会被用到

原因分析

在KMP算法中,当我们在位置 j 匹配失败时:

  • 我们会跳转到 j = lps[j-1]
  • 也就是说,我们用的是 lps[j-1] 而不是 lps[j]

由于 j 的最大值是 m-1(m是模式串长度),所以:

  • j = m-1 时,匹配失败会跳转到 lps[m-2]
  • 永远不会访问 lps[m-1]

但是 LPS 数组的最后一位仍然有其他用途

  1. 构建LPS数组本身:在计算LPS数组时,需要计算每一位(包括最后一位)
  2. 判断周期性:LPS的最后一位可以用来判断字符串的周期性
    • 如果 lps[m-1] > 0m % (m - lps[m-1]) == 0,说明字符串有周期
    • 例如:"abababab" 的 LPS 最后一位是 6,可以判断出它有周期"ab"
  3. 其他字符串算法:在某些扩展应用中可能会用到

总结:你的理解是正确的!在标准的KMP模式匹配中,LPS数组的最后一位不会在匹配过程中被访问到。

 

让我检查一下 "ababaaababaa" 是否满足周期性条件:

检查条件

  • 字符串长度 m = 12
  • LPS[11] = 1
  • 条件1:lps[m-1] > 0 ✓ (1 > 0)
  • 条件2:m % (m - lps[m-1]) == 0
    • 即:12 % (12 - 1) == 0
    • 即:12 % 11 == 0 ✗ (结果是1,不是0)

结论不适用!"ababaaababaa" 不满足周期性条件。

为什么? 这个字符串确实不是周期性的。让我们看看:

  • 如果有周期,周期长度应该是 m - lps[m-1] = 12 - 1 = 11
  • 但 11 不能整除 12,所以这个字符串不是由某个子串重复构成的

周期性的例子

  • "ababab":m=6, lps[5]=4, 6%(6-4)=0 ✓,周期是"ab"
  • "abcabcabc":m=9, lps[8]=6, 9%(9-6)=0 ✓,周期是"abc"

所以你的质疑是对的!我之前举的判断周期性的方法需要同时满足两个条件,而 "ababaaababaa" 只满足第一个条件,不满足第二个,因此它不是周期性字符串。

 

 

 

posted @ 2025-10-17 18:12  ChuckLu  阅读(5)  评论(0)    收藏  举报