数据结构——字符串匹配算法:朴素的匹配算法和KMP算法(超详细解释 新手也能会学!!!)_kmp朴素
目录
一、字符串匹配问题定义
二、朴素匹配算法(Brute Force)
1. 核心思想
2. 伪代码实现
3. 时间复杂度分析
4. 缺陷
三、KMP算法(Knuth-Morris-Pratt)
1. 核心思想
2. 最长公共前后缀(LPS):详细构建放在文章末尾
3. next 数组构建
4. 匹配流程
5. 时间复杂度
四、算法对比与应用场景
五、总结
Fin:LPS究极详解
一、LPS表的核心作用
二、LPS表的构建步骤(动态规划思想)
三、LPS表如何用于匹配?
四、完整代码实现(Python)
五、为什么LPS能避免回溯?关键总结
一、字符串匹配问题定义
给定文本串 T[0..n−1](长度 n)和模式串 P[0..m−1](长度 m,m≤n),目标是找到所有满足 T[s..s+m−1]=P[0..m−1] 的起始位置 s,称为合法移位(valid shift)。
二、朴素匹配算法(Brute Force)
1. 核心思想
- 枚举文本串 T 中所有长度为 m 的子串起点 s(s 范围:0 至 n−m)。
- 逐字符比较子串 T[s..s+m−1] 与模式串 P[0..m−1]。
- 若某字符失配(
),则 s 右移一位,重新比较。
2. 伪代码实现
def naive_match(T, P): n = len(T) m = len(P) for s in range(0, n - m + 1): # 枚举所有起点 j = 0 while j < m and T[s + j] == P[j]: # 逐字符比较 j += 1 if j == m: # 完全匹配 return s return -1 # 无匹配
3. 时间复杂度分析
- 最坏情况:每次需比较 m 次才失配,共 n−m+1 次尝试,复杂度 O(m(n−m))≈O(mn)。
- 例:T=\"AAAAA\",P=\"AAAB\" 。
- 最好情况:首次比较即失配,复杂度 O(n)。
- 空间复杂度:O(1),无需额外存储。
4. 缺陷
- 低效回溯:失配时文本串指针回溯至 s+1,模式串指针归零,重复比较大量字符。
三、KMP算法(Knuth-Morris-Pratt)
1. 核心思想
- 避免回溯:失配时利用已匹配信息,将模式串右移多位,文本串指针不回溯。
- 关键工具:构建
next
数组(部分匹配表),存储模式串前缀的最长公共前后缀长度。
2. 最长公共前后缀(LPS):详细构建放在文章末尾
- 定义:对子串 P[0..q],其 LPS 是满足 P[0..k]=P[q−k..q] 的最大 k(k<q)。
- 示例:P=\"ABABC\" 的 LPS 计算:
子串(P[0..q]) 前缀 后缀 LPS(k) \"A\" [] [] 0 \"AB\" [\"A\"] [\"B\"] 0 \"ABA\" [\"A\",\"AB\"] [\"BA\",\"A\"] 1(\"A\") \"ABAB\" [\"A\",\"AB\",\"ABA\"] [\"BAB\",\"AB\",\"B\"] 2(\"AB\") \"ABABC\" 所有前缀 所有后缀 0
3. next
数组构建
next[q]
表示 P[0..q] 的 LPS 长度。- 动态规划递推:
next[0] = 0
(单字符无真前缀)。- 设指针 i=1, j=0:
- 若 P[i]=P[j],则
next[i] = j + 1
,i,j 右移。 - 若 P[i]=P[j] 且 j>0,则 j=next[j−1](回溯至前一个 LPS)。
- 若 j=0,则
next[i] = 0
,i 右移。
- 若 P[i]=P[j],则
代码实现(Java):
int[] computeNext(String P) { int m = P.length(); int[] next = new int[m]; next[0] = 0; int j = 0; for (int i = 1; i 0) { j = next[j - 1]; // 回溯至前一个 LPS } else { next[i] = 0; i++; } } return next;}
4. 匹配流程
- 初始化文本串指针 i=0,模式串指针 j=0。
- 比较 T[i] 与 P[j]:
- 若相等,i 和 j 右移。
- 若不等:
- j>0:j=next[j−1](模式串右移)。
- j=0:i 右移。
- 当 j=m 时,返回 i−j(匹配成功)。
匹配示例:
T=\"ABABABABC\", P=\"ABABC\",`next = [0, 0, 1, 2, 0] )
- 失配于 T[4](\'A\' vs \'C\'),j=next[3]=2 → 模式串右移 2 位,继续匹配。
5. 时间复杂度
- 预处理:构建
next
数组需 O(m)。 - 匹配:文本串指针 i 单调不减,复杂度 O(n)。
- 总计:O(m+n),远优于朴素算法。
四、算法对比与应用场景
实际应用建议:
- 朴素算法:嵌入式系统、短文本(如
std::string::find
的实现)。 - KMP算法:编译器词法分析、DNA序列比对(避免大量重复字符导致的低效)。
五、总结
朴素匹配算法是字符串匹配的基础,但效率受限;KMP算法通过 next
数组跳过无效匹配,以空间换时间。理解KMP的关键在于掌握LPS的定义与 next
数组的构建逻辑。实际应用中需根据问题规模选择合适算法。进一步学习可参考Boyer-Moore、Sunday等优化算法。
Fin:LPS究极详解
理解KMP算法的难点确实常集中在LPS(Longest Proper Prefix which is also Suffix,最长相同真前后缀)表的构建逻辑上。别担心,这里看完肯定能看懂,我们一起彻底攻克这个核心难点。
一、LPS表的核心作用
在KMP算法中,LPS表(也称为前缀表或next
数组)的作用是:当字符失配时,告诉模式串应该回退到哪个位置继续匹配。
- 为什么需要它? 暴力匹配在失配时只能将模式串右移1位,而KMP利用LPS表跳过已确认匹配的字符,避免重复比较,大幅降低复杂度。
- 核心定义:对模式串位置
i
,LPS[i]
表示子串P[0..i]
的最长相同真前后缀的长度(真前后缀指不包含整个子串本身:也就是这里前缀的时候要排除掉从前往后的最后一位也就是位置i,后缀的时候排除掉从后往前的第一位也就是位置0)。
💡 例:模式串
P = \"ABABC\"
i=3
时(子串\"ABAB\"
):
- 前缀:
\"A\"
,\"AB\"
,\"ABA\"
- 后缀:
\"B\"
,\"AB\"
,\"BAB\"
- 最长相同前后缀:
\"AB\"
→ 长度=2 →LPS[3] = 2
。
二、LPS表的构建步骤(动态规划思想)
通过双指针分别是:j
(前缀尾)和i
(后缀尾)逐步计算,用模式串 P=\"ABABC\"
举例:
i
位置j
变化LPS[i]
LPS[0]=0
, j=0
, i=1
i=1
\"AB\"
前缀:A
后缀:B
j=0
LPS[1]=0
i=2
\"ABA\"
前缀:A,AB
后缀:A,BA
j++ → j=1
LPS[2]=1
\"A\"
相同i=3
\"ABAB\"
前缀:A,AB,ABA
后缀:B,AB,BAB
j++ → j=2
LPS[3]=2
\"AB\"
相同i=4
\"ABABC\"
前缀:A,AB,ABA,ABAB
后缀:
C,BC,ABC,BABC
j=LPS[j-1]=LPS[1]=0
LPS[4]=0
关键回溯逻辑(步骤4):
当P[i] ≠ P[j]
时,j
回溯到LPS[j-1]
(即前一个字符的最长相同前后缀位置),避免重复比较已匹配前缀。
回溯原因:子串
\"ABABC\"
中,j=2
指向\'A\'
,i=4
指向\'C\'
不匹配。但\"ABA\"
的最长相同前后缀是\"A\"
(LPS[2]=1
),所以将j
回退到位置1
(对应字符\'B\'
),继续与i=4
比较。
三、LPS表如何用于匹配?
以下列文本串T=\"ABABABABC\"
和模式串P=\"ABABC\"
为例(LPS = [0,0,1,2,0]
):
- 第一次匹配:
T[0..3] = \"ABAB\"
匹配P[0..3]=\"ABAB\"
- 失配位置:
T[4]=\'A\'
vsP[4]=\'C\'
- 利用LPS跳过重复比较:
- 失配时,模式串指针
j
回退到LPS[j-1] = LPS[3] = 2
- 模式串右移,新匹配位置:
P[2]
对齐T[4]
(相当于跳过2个字符)
- 失配时,模式串指针
- 继续匹配:
- 从
T[4]=\'A\'
vsP[2]=\'A\'
开始 → 后续字符全部匹配成功。
- 从
跳过原理:
子串\"ABAB\"
的LPS[3]=2
表明其后2字符\"AB\"
与前缀\"AB\"
相同,因此失配后可直接将前缀\"AB\"
对齐到已匹配的\"AB\"
上,跳过中间无效位置。
四、完整代码实现(Python)
def build_lps(pattern): m = len(pattern) lps = [0] * m j = 0 # 指针:前缀尾 for i in range(1, m): # 指针:后缀尾 # 字符不匹配时,回溯j到前一个LPS位置 while j > 0 and pattern[i] != pattern[j]: j = lps[j - 1] # 字符匹配,j右移并更新LPS if pattern[i] == pattern[j]: j += 1 lps[i] = j return lpsdef kmp_search(text, pattern): n, m = len(text), len(pattern) lps = build_lps(pattern) j = 0 # 模式串指针 for i in range(n): # 文本串指针 # 不匹配时,j回溯到LPS位置 while j > 0 and text[i] != pattern[j]: j = lps[j - 1] # 匹配时,j右移 if text[i] == pattern[j]: j += 1 if j == m: # 完全匹配 print(f\"匹配位置: {i - m + 1}\") j = lps[j - 1] # 继续搜索其他匹配 return -1 # 无匹配# 测试text = \"ABABABABC\"pattern = \"ABABC\"kmp_search(text, pattern) # 输出:匹配位置: 4
五、为什么LPS能避免回溯?关键总结
- 空间换时间:LPS表存储模式串的自匹配信息,预处理时间
O(m)
,匹配时间O(n)
。 - 跳过的本质:利用已匹配部分的最长相同前后缀,直接将前缀对齐到后缀位置,跳过中间无重叠字符。
- 类比理解:
- 暴力匹配:每次失配,模式串右移1位(如幻灯片逐帧播放)。
- KMP算法:失配时按LPS表跳转(如快进到关键帧)。
检验理解:尝试手动计算
P=\"AABAAB\"
的LPS表:
- 答案:
[0,1,0,1,2,3]
(子串\"AABAA\"
的最长前后缀是\"AA\"
,长度=2)。
彻底掌握LPS表,KMP算法就再无难点!