#字符串
基本概念
- 串:特殊的线性表,数据元素为字符
- 空串:长度为零的串
"" - 空格串:仅由空格组成的串
" "或" " - 子串:串中任意连续字符组成的序列
- 主串:包含子串的串
- 模式匹配:在主串中定位子串(模式串)的操作
一、朴素模式匹配算法
朴素模式匹配算法是最直观的字符串匹配方法,其核心思想是:将模式串在主串中逐个位置进行比对,如果当前字符匹配成功,则两个指针同时向后移动一位;否则,将模式串的指针移回起始位置,主串的指针向后移动一位,再次尝试匹配。
时间复杂度:O(m*n),其中m是模式串长度,n是主串长度。
朴素算法匹配过程(主串S: “ABABABABCABAAB”,模式串P: “ABABC”)
从S[0]开始匹配:
- S[0]=‘A’ vs P[0]=‘A’ → 匹配
- S[1]=‘B’ vs P[1]=‘B’ → 匹配
- S[2]=‘A’ vs P[2]=‘A’ → 匹配
- S[3]=‘B’ vs P[3]=‘B’ → 匹配
- S[4]=‘A’ vs P[4]=‘C’ → 不匹配
- 不匹配,主串指针后移1位到S[1],模式串指针回退到P[0]
从S[1]开始匹配:
- S[1]=‘B’ vs P[0]=‘A’ → 不匹配
- 不匹配,主串指针后移1位到S[2],模式串指针回退到P[0]
从S[2]开始匹配:
- S[2]=‘A’ vs P[0]=‘A’ → 匹配
- S[3]=‘B’ vs P[1]=‘B’ → 匹配
- S[4]=‘A’ vs P[2]=‘A’ → 匹配
- S[5]=‘B’ vs P[3]=‘B’ → 匹配
- S[6]=‘A’ vs P[4]=‘C’ → 不匹配
- 不匹配,主串指针后移1位到S[3],模式串指针回退到P[0]
从S[3]开始匹配:
- S[3]=‘B’ vs P[0]=‘A’ → 不匹配
- 不匹配,主串指针后移1位到S[4],模式串指针回退到P[0]
从S[4]开始匹配:
- S[4]=‘A’ vs P[0]=‘A’ → 匹配
- S[5]=‘B’ vs P[1]=‘B’ → 匹配
- S[6]=‘A’ vs P[2]=‘A’ → 匹配
- S[7]=‘B’ vs P[3]=‘B’ → 匹配
- S[8]=‘C’ vs P[4]=‘C’ → 匹配
- 匹配成功!起始位置为4
朴素算法总结:共进行了5次匹配尝试,匹配成功时起始位置为4。
二、KMP算法
KMP算法通过预处理模式串,构建一个next数组(部分匹配表),记录模式串中每个位置之前的子串中,最长相同前缀后缀的长度。当发生不匹配时,利用next数组决定模式串的移动位置,避免了主串指针的回溯。
时间复杂度:O(m+n),其中m是模式串长度,n是主串长度。
1. 构建next数组(模式串P: “ABABC”)
模式串:A B A B C
| 位置 | 字符 | 前缀 | 后缀 | 最长相同前缀后缀长度 | next值 |
|---|---|---|---|---|---|
| 0 | A | 无 | -1 | ||
| 1 | B | A | B | 无 | 0 |
| 2 | A | A, AB | A, BA | A (长度1) | 1 |
| 3 | B | A, AB, ABA | B, AB, BAB | AB (长度2) | 2 |
| 4 | C | A, AB, ABA, ABAB | C, BC, ABC, BABC | 无 | 0 |
next数组:[-1, 0, 1, 2, 0]
通常实现时,next[0] = -1,用于特殊处理第一个字符就不匹配的情况。
2. KMP算法匹配过程(主串S: “ABABABABCABAAB”,模式串P: “ABABC”)
KMP 的核心思想
- 匹配失败时,主串指针 i 不回溯,只调整模式串指针 j 的位置
- 利用已匹配的部分字符的前缀和后缀的公共信息,让 j 移动到「能复用已匹配内容」的位置,避免重复比对(模式串指针 j 在失配时根据 next[j-1] 跳转)
关键:前缀函数(next 数组)
- next 数组本质是模式串的自相关性。它记录了:如果在位置 j 失配,那么模式串可以“安全地”滑动到 next[j-1] 位置继续匹配,因为前面的 next[j-1] 个字符已经和主串对齐。
- 前缀:模式串中,除最后一个字符外,从开头到任意位置的子串(比如 “ABC” 的前缀是 “A”、“AB”)
- 后缀:模式串中,除第一个字符外,从任意位置到结尾的子串(比如 “ABC” 的后缀是 “C”、“BC”)
- next数组的定义是:next[j]表示模式串中长度为j+1的子串(即从索引0到j的子串)的最长公共前后缀长度。当匹配失败时,我们已经匹配了前j个字符(索引0到j-1),所以我们需要使用next[j-1]来决定j应该更新为多少。
KMP 匹配过程(核心逻辑)
- 先计算模式串 P 的 next 数组
- 用指针 i 遍历主串 S(不回溯),指针 j 遍历模式串 P
- 若 S[i] == P[j],则 i++、j++,继续比对
- 若 S[i] != P[j]:
- 若 j > 0,则 j = next[j-1](利用 next 数组回退 j,复用已匹配的内容)
- 若 j == 0,则 i++(模式串第一个字符就不匹配,主串指针后移)
- 当 j ==
len(P)时,匹配成功,返回起始位置 i-j;否则返回 -1
初始化:
- i = 0(主串指针),j = 0(模式串指针)
- next数组:[-1, 0, 1, 2, 0]
匹配过程:
i=0, j=0:S[0]=‘A’ vs P[0]=‘A’ → 匹配 → i=1, j=1
i=1, j=1:S[1]=‘B’ vs P[1]=‘B’ → 匹配 → i=2, j=2
i=2, j=2:S[2]=‘A’ vs P[2]=‘A’ → 匹配 → i=3, j=3
i=3, j=3:S[3]=‘B’ vs P[3]=‘B’ → 匹配 → i=4, j=4
i=4, j=4:S[4]=‘A’ vs P[4]=‘C’ →不匹配
- j = next[j-1] = next[3] = 2
- 现在比较S[4]=‘A’ vs P[2]=‘A’ → 匹配 → i=5, j=3
i=5, j=3:S[5]=‘B’ vs P[3]=‘B’ → 匹配 → i=6, j=4
i=6, j=4:S[6]=‘A’ vs P[4]=‘C’ →不匹配
- j = next[j-1] = next[3] = 2
- 现在比较S[6]=‘A’ vs P[2]=‘A’ → 匹配 → i=7, j=3
i=7, j=3:S[7]=‘B’ vs P[3]=‘B’ → 匹配 → i=8, j=4
i=8, j=4:S[8]=‘C’ vs P[4]=‘C’ →匹配成功
- j = 5 == len§,匹配成功
- 起始位置 = i - j + 1 = 8 - 5 + 1 = 4
KMP算法总结:共进行了3次匹配尝试(而非5次),匹配成功时起始位置为4。
解释:
为什么是next[j-1]而不是next[j]?
- 当匹配失败时(即S[i] != P[j]),我们需要将j更新为一个值,使得模式串能够"跳过"一些已经匹配的字符,避免重复比较。*
- j=4表示我们已经匹配了"ABAB"(索引0-3),现在匹配失败
- 我们需要找到"ABAB"的最长公共前后缀长度,这个长度由next[3]给出(因为"ABAB"是长度为4的子串,对应next[3])
- next[3] = 2表示"ABAB"的最长公共前后缀长度为2(即"AB")
- 我们应该使用next[j-1] = next[3] = 2,而不是next[j] = next[4] = 0
- 所以,我们应该将j更新为2,表示模式串可以向右移动2位,让"AB"与主串的当前字符对齐
为什么可以跳过前两个字符?
我们已知主串的这部分是"ABAB"
后缀"AB" = 前缀"AB"
所以主串的后两个字符"AB"一定等于模式串的前两个字符"AB"
不需要重新比较,直接从模式串的第3个字符开始
KMP 算法匹配过程的核心逻辑
next = [-1, 0, 1, 2, 0]- 匹配到
i=4, j=4时:S[4]='A' ≠ P[4]='C'j > 0→j = next[3] = 2- 下一轮比较
S[4]与P[2](即'A' == 'A',继续)
- 最终在
i=9, j=5时:j == len(P)→ 匹配成功,起始位置 =9 - 5 = 4
三、两种算法对比
| 特性 | 朴素匹配算法 | KMP算法 |
|---|---|---|
| 匹配过程 | 每次不匹配都回溯主串指针 | 不回溯主串指针,利用next数组跳过无效匹配 |
| 匹配次数 | 5次 | 3次 |
| 时间复杂度 | O(m*n) | O(m+n) |
| 实现复杂度 | 简单 | 较复杂(需构建next数组) |
| 优势 | 代码简单直观 | 高效,适用于大规模数据匹配 |
四、KMP算法的核心思想
KMP算法的核心在于:当匹配失败时,我们不把文本串的比较指针回溯,而是根据已经匹配过的内容,将模式串的指针"智能地"向前跳转。
在本例中,当在S[4]处匹配失败(S[4]=‘A’ vs P[4]=‘C’)时,KMP算法利用next数组得知P[0…2] = "ABA"与S[2…4] = "ABA"匹配,因此可以直接将模式串的指针移动到j=2的位置,继续比较S[4]和P[2],避免了主串指针的回溯。
这种"智能跳转"使得KMP算法在最坏情况下也能保持线性时间复杂度,而朴素算法在最坏情况下需要O(m*n)时间。
五、总结
- 朴素匹配算法思路简单,但效率低下,尤其在模式串有重复字符时
- KMP算法通过预处理模式串构建next数组,避免了不必要的比较,大大提高了匹配效率
- 在实际应用中,特别是处理大规模数据时,KMP算法是更优的选择
通过这个例子,我们可以清晰地看到KMP算法如何利用已匹配的信息,避免了朴素算法中重复的比较工作,从而提高了匹配效率。
你总结得非常准确!这是KMP 算法匹配过程的核心逻辑,完全正确。下面我将你的总结稍作整理,并补充一些细节说明,使其更清晰、严谨,便于理解和实现。
✅ KMP 匹配过程(核心逻辑)
步骤 1:预处理 —— 构建next数组
- 对模式串
P预处理,生成next数组。 next[j]表示:子串P[0..j]的最长相等真前缀与真后缀的长度。- 注意:不同教材对
next数组的定义略有差异(有的从 0 开始,有的从 -1 开始),但核心思想一致。
常见两种定义:
- 定义 A(常用):
next[0] = -1,next[j]表示当P[j]失配时,j应跳转到的位置。- 定义 B:
next[j]表示P[0..j-1]的最长公共前后缀长度(此时next[0] = 0)。
我们以下采用定义 A(带 -1),因为它在代码中更简洁。
步骤 2:匹配主串 S 与模式串 P
初始化:
i = 0:主串 S 的指针(永不回溯)j = 0:模式串 P 的指针
循环直到i >= len(S):
情况 1:字符匹配
if S[i] == P[j]: i += 1 j += 1- 双指针同步前进。
情况 2:字符不匹配
else: if j > 0: j = next[j - 1] // 利用已匹配信息,j 回退,i 不动 else: // j == 0,即模式串首字符就不匹配 i += 1 // 主串指针后移,j 保持为 0🔍 关键点:只有当 j == 0 且失配时,才移动 i;否则只调整 j。
情况 3:匹配成功
if j == len(P): 找到匹配!起始位置 = i - j (可选择继续查找:j = next[j - 1] 继续匹配)📌 示例回顾(S = “ABABABABCABAAB”, P = “ABABC”)
next = [-1, 0, 1, 2, 0]- 匹配到
i=4, j=4时:S[4]='A' ≠ P[4]='C'j > 0→j = next[3] = 2- 下一轮比较
S[4]与P[2](即'A' == 'A',继续)
最终在i=9, j=5时:j == len(P)→ 匹配成功,起始位置 =9 - 5 = 4
✅ 为什么这个逻辑正确?
- 主串指针 i 永不回溯→ 时间复杂度 O(n)
- next 数组保证了“最大可能复用”已匹配部分→ 避免重复比较
- j = next[j-1]是因为:当前已匹配的是
P[0..j-1],其最长公共前后缀长度决定了 j 能跳到哪里
🧠 小贴士:如何记忆?
“匹配就一起走,不匹配就看 next;j 能退就退,退不了就 i 走”
✅ 最终结论
你给出的总结完全正确,是 KMP 匹配阶段的标准流程。只要配合正确的next数组构建方法,就能实现高效的字符串匹配。
如果需要,我也可以提供完整的 Python/C++ 实现代码!