原理
在日常实践中,我们常常有这样的需求:给定一段文本,从中找到某个指定的关键字。
一般来说,我们完成这个目标,会直接去调用 C 语言提供的strstr()
函数。但是这个函数的算法是暴力穷举去匹配,时间复杂度很高:
在串的模式匹配中,已经有人帮我们设计出了高效的算法,这就是KMP:
它的关键是用动态规划的知识构造一个关于 Pattern 的 Prefix Table (前缀表),然后根据 Prefix Table 来快速匹配字符串。
最长公共前后缀
给定一个字符串ababc
,它的子串前缀、子串后缀和最长公共前后缀的长度如下:
子串 | 字串前缀 | 字串后缀 | 最长公共前后缀 的 长度 |
---|---|---|---|
a | a | NULL | 0 |
ab | a | b | 0 |
aba | a、ab | a、ba | 1 |
abab | a、ab、aba | b、ab、bab | 2 |
ababc | a、ab、aba、abab | c、bc、abc、babc | 0 |
把最长公共前后缀的长度专门组成一个数组,这就是 Prefix Table。在大部分教科书中,会把 Prefix Table 的第一个元素置为-1
。如上述例子的 Prefix Table 会是[-1, 0, 0, 1, 2]
。
Prefix Table 的构建
算法步骤如下:
- 定义一个一维数组,名为
prefix_table
,其长度和 Pattern 相同(prefix_table[size]
); - 定义变量
i
和j
,设置i = 0
、j = 1
、prefix_table[0] = 0
; - 比较
pattern[i]
和pattern[j]
; - 如果
pattern[i] == pattern[j]
,则设置prefix_table[j] = i + 1;
并且i += 1; j += 1;
,之后返回步骤三; - 如果
pattern[i] != pattern[j]
,则检查i
的值。如果i == 0
,则设置prefix_table[j] = 0;
并j += 1;
。如果i != 0
,则设置i = prefix_table[i-1];
。返回步骤三; - 重复上述步骤直到
prefix_table
被填满;
Rust代码实现如下:
// src/lib.rs
pub fn build_prefix_table(pattern: &str) -> Vec<usize> {
let mut prefix_table = vec![0];
let pattern = pattern.as_bytes();
let mut i = 0;
let mut j = 1;
while j < pattern.len() {
if pattern[i] == pattern[j] {
prefix_table.push(i + 1);
i += 1;
j += 1;
} else {
if i == 0 {
prefix_table.push(0);
j += 1;
} else {
i = prefix_table[i - 1];
}
}
}
return prefix_table;
}
使用 Prefix Table 匹配字符串
假设有 Text:abaacababcac
,Pattern:abab
。我们已知前缀表prefix = [0, 0, 1, 2]
,匹配过程如下:
第一步:开始匹配,到第三个字符匹配失败。我们从匹配失败的地方把 Pattern 指针由2
拉到prefix[2] = 1
。
第二步:匹配依旧失败,我们从匹配失败的地方把 Pattern 指针由1
拉到prefix[1] = 0
。
第三步:匹配到第二个字符失败,重复前面的步骤,从匹配失败的地方,把 Pattern 指针由1
拉到prefix[1] = 0
处。
第四步:匹配失败,把 Pattern 指针由1
拉到0
:
第五步:依旧失败,此时 Pattern 指针已经是0
了,我们把 Text 指针向后移动一格,继续匹配后面的字符。
第六步:此时出现了一个匹配成功的序列。我们记录下当前的位置。把 Pattern 指针置为prefix[i-1]
,并把 Text 指针后移一格,寻找后面有没有更多能匹配上的字符串。
此时 Pattern 的尾巴已经碰到 Text 的尾巴了,没有找到更多能匹配上的字符。匹配过程结束。
Rust代码实现如下:
// src/lib.rs
pub fn kmp(text: &str, pattern: &str) -> Option<Vec<usize>> {
if text.is_empty() || pattern.is_empty() {
return None;
}
let prefix_table = build_prefix_table(pattern);
let text = text.as_bytes();
let pattern = pattern.as_bytes();
let mut index_pattern = 0;
let mut ret = vec![];
for (index_text, &item) in text.iter().enumerate() {
while index_pattern > 0 && item != pattern[index_pattern] {
index_pattern = prefix_table[index_pattern - 1];
}
if item == pattern[index_pattern] {
index_pattern += 1; // next char matched, increment position
if index_pattern == pattern.len() {
ret.push(index_text - index_pattern + 1);
index_pattern = prefix_table[index_pattern - 1];
}
}
}
if ret.is_empty() {
return None;
}
Some(ret)
}
测试代码如下:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefix_table_works() {
assert_eq!(build_prefix_table("ababc"), vec![0, 0, 1, 2, 0]);
assert_eq!(build_prefix_table("ABCDABD"), vec![0, 0, 0, 0, 1, 2, 0]);
assert_eq!(build_prefix_table("abaabc"), vec![0, 0, 1, 1, 2, 0]);
assert_eq!(build_prefix_table("aaaaa"), vec![0, 1, 2, 3, 4]);
assert_eq!(build_prefix_table("ababab"), vec![0, 0, 1, 2, 3, 4]);
assert_eq!(build_prefix_table("abacabab"), vec![0, 0, 1, 0, 1, 2, 3, 2]);
assert_eq!(
build_prefix_table("aaabaaaaab"),
vec![0, 1, 2, 0, 1, 2, 3, 3, 3, 4]
);
}
#[test]
fn kmp_works() {
assert_eq!(kmp("", ""), None);
assert_eq!(kmp("", "hello"), None);
assert_eq!(kmp("hello", ""), None);
assert_eq!(kmp("abaacababcac", "abab"), Some(vec![5]));
assert_eq!(kmp("cbabcababcac", "ab"), Some(vec![2, 5, 7]));
assert_eq!(kmp("cbabcababcac", "apple"), None);
}
}
运行结果如下:
WSL my_kmp on 🌱 main [?] is 📦 v0.1.0 via 🦀 v1.55.0
❯ cargo t
Finished test [unoptimized + debuginfo] target(s) in 0.01s
Running unittests (target/debug/deps/my_kmp-f74484c2e47d7d4c)
running 2 tests
test tests::kmp_works ... ok
test tests::prefix_table_works ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests my_kmp
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s