如果您喜欢这篇文章,欢迎访问我的个人博客查看原文并参与讨论与互动 【原文链接】
字符串匹配算法
字符串匹配算法旨在找到字符串 s
中与搜索词 w
匹配的起始索引 m
。[ ^wiki ] 这种过程是文本处理和计算机科学应用中的基础,例如文本编辑和数据检索。
例子:
s = "ABCABABCABDA"
w = "ABCABD"
暴力解法
暴力方法采用了一个简单的方案,使用两个指针:一个遍历主字符串 s
,另一个用于检查目标单词 w
。对于 s
中的每个字符,它会比较接下来的字符与 w
的对应字符,直到比较长度达到 w
的长度。如果出现不匹配,s
中的指针将移动到下一个字符,并重新开始比较。
然而,这种方法的时间复杂度为 O(mn)
,其中 m = len(s)
,n = len(w)
。当不匹配经常发生在 w
的末尾并且 w
出现在 s
中较晚时,这种方法的效率极其低下。例如,考虑 s = "000000000000000000001"
和 w = "00000001"
。在这种情况下,几乎每次尝试都将处理 w
的几乎整个长度,然后才遇到不匹配,导致搜索过程中大量的冗余。
KMP 的优势
Knuth-Morris-Pratt (KMP) 算法通过使用 前缀表(又称 “部分匹配” 表1) next
来提高效率。该表允许算法跳过已与 w
部分匹配的 s
的部分,从而避免重新评估根据前缀表将匹配的字符。
KMP 工作原理
前缀和后缀
- 前缀表:
- 这个表是由搜索词
w
构造的,记录了每个位置i
结束的子字符串的最长前缀与后缀匹配的长度。 - 前缀:
- 一个字符串的前缀是任何以字符串开头的子串,但不包括最后一个字符。这些是字符串可能的前导段。
- 后缀:
- 一个字符串的后缀是任何以字符串末尾结束的子串,但不包括第一个字符。这些是字符串可能的末尾段2。
- 这个表是由搜索词
对于每个 next[i]
,它记录了 w
的最长前缀与 w[i]
之前结束的后缀匹配的长度。前缀表 next
允许我们跳到 w[next[i]]
,有效地跳过之前已保证匹配的字符,避免了冗余检查。
预处理:创建前缀表
这个预处理步骤包括以下三个主要阶段:
- 初始化
- 处理不匹配
- 处理匹配
1. 初始化
首先,创建一个与模式 w
长度相同的数组 next
,并将所有条目初始化为 0
。
get_next(w, next):
next[0] = 0
然后,初始化指针 i = 1
,j = 0
,其中:
i
是后缀的结尾,也是下一个将更新的next
元素。j
是前缀的结尾,也表示最长匹配的前缀和后缀的长度。
2. 处理不匹配
当前缀的最后一个元素 s[j]
与后缀 w[i]
不匹配时,我们更新指针 j
,寻找下一个可能的前缀长度,然后将 j
前移一步,查看该位置的 next
值。
while j > 0 and w[i] != w[j]:
j = next[j - 1]
在某些情况下,即使回退到先前的前缀长度(j = next[j-1]
),j
的新位置仍然不匹配当前字符 w[i]
。因此,while
循环(while j > 0 and w[i] != w[j]
)确保算法持续调整被考虑的前缀长度,直到找到匹配的前缀或耗尽所有可能的前缀。
3. 处理匹配
当找到 w[i]
与 w[j]
的匹配时,表示前缀长度为 j
,可以通过一个字符延伸(w[i]
)。
if w[i] == w[j]:
j += 1
完整代码
get_next(w, next):
next[0] = 0
for i in range(1, len(w)):
while j > 0 and w[i] != w[j]:
j = next[j - 1]
if w[i] == w[j]:
j += 1
next[i] = j
搜索:对齐到下一个潜在匹配
前缀表的使用取决于具体的问题。以下列出了一些实现 KMP 的示例问题:
力扣问题 | 说明 |
---|---|
28 在字符串中找到第一个匹配的位置 | 专注于在长文本中找到第一个匹配,直接应用 KMP 快速搜索能力。 |
459 重复的子字符串模式 | 使用前缀表确定字符串是否可以由重复的子字符串构成,展示了 KMP 预处理的灵活应用。 |
预处理的复杂度
由于构建前缀表的预处理阶段和随后的搜索阶段分别以 O(n)
和 O(m)
时间运行,其中 n
是模式 w
的长度,m
是文本 s
的长度,KMP 算法的总复杂度为 O(n+m)
。
如果您喜欢这篇文章,欢迎访问我的个人博客查看原文并参与讨论与互动 【原文链接】
参考
Wikipedia - Knuth–Morris–Pratt algorithm:https://en.wikipedia.org/wiki/Knuth–Morris–Pratt_algorithm. ↩︎