惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - ChuckLu

英语背单词 专八词汇 中英对照 2026年05月 背单词 纯英文 2026年05月 - ChuckLu 英语背单词 专八词汇 中英对照 2026年04月 背单词 纯英文 2026年04月 英语背单词 专八词汇 中英对照 2026年03月 背单词 纯英文 2026年03月 背单词 纯英文 2026年02月 英语背单词 专八词汇 中英对照 2026年02月 英语背单词 专八词汇 中英对照 2026年01月 背单词 纯英文 2026年01月 英语背单词 专八词汇 中英对照 2025年12月 背单词 纯英文 2025年12月 二叉树 节点的个数关系 二叉树 遍历 补码加减法 英语背单词 专八词汇 中英对照 2025年11月 背单词 纯英文 2025年11月 图 生成树 图论 Walks Trails and Paths 英语背单词 专八词汇 中英对照 2025年10月 背单词 纯英文 2025年10月
KMP算法
ChuckLu · 2025-10-17 · via 博客园 - ChuckLu

字符串匹配的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]

jT[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" 只满足第一个条件,不满足第二个,因此它不是周期性字符串。