KMP算法是一种用于在字符串中查找子串的高效算法。它的核心思想是利用之前已经部分匹配的结果来减少不必要的比较,从而提高查找速度。
核心概念
- 前缀函数(也称为部分匹配表):对于给定的模式字符串 P,前缀函数存储了模式字符串的每个位置的最长相同前缀和后缀的长度。
- 匹配过程:使用前缀函数来跳过已经匹配的部分,以减少比较次数。
前缀函数的计算
前缀函数 π 表示对于字符串 P 中的每个位置 i,最长的使得 𝑃[0..𝑘−1]等于 𝑃[𝑖−𝑘+1..𝑖]的 k 值。
// 计算模式字符串的前缀函数
void computePrefixFunction(const string& pattern, vector<int>& pi) {
int m = pattern.size();
pi[0] = 0; // 初始状态,第一个字符的前缀函数值为0
int k = 0; // k表示前一个字符的前缀长度
for (int i = 1; i < m; i++) {
// 如果当前字符不匹配,并且k>0,调整k的值为前一个字符的前缀函数值
while (k > 0 && pattern[k] != pattern[i]) {
k = pi[k - 1];
}
// 如果当前字符匹配,k值加1
if (pattern[k] == pattern[i]) {
k++;
}
// 更新当前字符的前缀函数值
pi[i] = k;
}
}
KMP匹配过程
使用计算好的前缀函数,在目标字符串中查找模式字符串。
// 使用KMP算法在目标字符串中查找模式字符串
vector<int> KMP(const string& text, const string& pattern) {
int n = text.size();
int m = pattern.size();
vector<int> pi(m); // 创建前缀函数数组
computePrefixFunction(pattern, pi); // 计算前缀函数
vector<int> result; // 存储匹配的位置
int k = 0; // 模式字符串中的位置
// 遍历目标字符串
for (int i = 0; i < n; i++) {
// 如果字符不匹配,并且k>0,调整k的值为前一个字符的前缀函数值
while (k > 0 && pattern[k] != text[i]) {
k = pi[k - 1];
}
// 如果字符匹配,k值加1
if (pattern[k] == text[i]) {
k++;
}
// 如果k值等于模式字符串长度,表示找到一个匹配
if (k == m) {
result.push_back(i - m + 1); // 记录匹配的位置
k = pi[k - 1]; // 重置k值为前一个字符的前缀函数值
}
}
return result; // 返回所有匹配的位置
}
完整代码
结合前缀函数和KMP匹配过程的完整代码如下:
#include <vector>
#include <string>
#include <iostream>
using namespace std;
// 计算模式字符串的前缀函数
void computePrefixFunction(const string& pattern, vector<int>& pi) {
int m = pattern.size();
pi[0] = 0; // 初始状态,第一个字符的前缀函数值为0
int k = 0; // k表示前一个字符的前缀长度
for (int i = 1; i < m; i++) {
// 如果当前字符不匹配,并且k>0,调整k的值为前一个字符的前缀函数值
while (k > 0 && pattern[k] != pattern[i]) {
k = pi[k - 1];
}
// 如果当前字符匹配,k值加1
if (pattern[k] == pattern[i]) {
k++;
}
// 更新当前字符的前缀函数值
pi[i] = k;
}
}
// 使用KMP算法在目标字符串中查找模式字符串
vector<int> KMP(const string& text, const string& pattern) {
int n = text.size();
int m = pattern.size();
vector<int> pi(m); // 创建前缀函数数组
computePrefixFunction(pattern, pi); // 计算前缀函数
vector<int> result; // 存储匹配的位置
int k = 0; // 模式字符串中的位置
// 遍历目标字符串
for (int i = 0; i < n; i++) {
// 如果字符不匹配,并且k>0,调整k的值为前一个字符的前缀函数值
while (k > 0 && pattern[k] != text[i]) {
k = pi[k - 1];
}
// 如果字符匹配,k值加1
if (pattern[k] == text[i]) {
k++;
}
// 如果k值等于模式字符串长度,表示找到一个匹配
if (k == m) {
result.push_back(i - m + 1); // 记录匹配的位置
k = pi[k - 1]; // 重置k值为前一个字符的前缀函数值
}
}
return result; // 返回所有匹配的位置
}
int main() {
string text = "ababcabcababcabc"; // 目标字符串
string pattern = "abc"; // 模式字符串
vector<int> matches = KMP(text, pattern); // 使用KMP算法查找匹配
// 输出所有匹配的位置
for (int pos : matches) {
cout << "Pattern found at position " << pos << endl;
}
return 0; // 程序结束
}
-
computePrefixFunction:
- 计算模式字符串的前缀函数值,前缀函数值表示模式字符串的每个位置的最长相同前缀和后缀的长度。
- 通过调整
k
的值来跳过不匹配的部分,减少不必要的比较次数。
-
KMP:
- 使用计算好的前缀函数在目标字符串中查找模式字符串。
- 通过调整
k
的值来跳过已经匹配的部分,以提高查找效率。 - 当找到一个匹配时,记录匹配的位置并继续查找下一个可能的匹配。
-
main:
- 测试 KMP 算法,输出模式字符串在目标字符串中的所有匹配位置。
对于模式字符串 "abc"
:
pattern[0] = 'a'
,前缀函数值为0。pattern[1] = 'b'
,前缀函数值为0(没有匹配的前缀)。pattern[2] = 'c'
,前缀函数值为0(没有匹配的前缀)。
因此,前缀函数数组 pi = [0, 0, 0]
。
详细过程
-
初始化:
n = 16
(目标字符串的长度)m = 3
(模式字符串的长度)pi = [0, 0, 0]
(前缀函数数组)k = 0
(模式字符串中的位置)
-
遍历目标字符串:
i = 0
:text[0] = 'a'
与pattern[k] = 'a'
匹配,k = 1
i = 1
:text[1] = 'b'
与pattern[k] = 'b'
匹配,k = 2
i = 2
:text[2] = 'a'
不匹配pattern[k] = 'c'
,k
重置为pi[k-1] = 0
i = 3
:text[3] = 'b'
与pattern[k] = 'a'
不匹配,k
仍为 0i = 4
:text[4] = 'c'
与pattern[k] = 'a'
不匹配,k
仍为 0i = 5
:text[5] = 'a'
与pattern[k] = 'a'
匹配,k = 1
i = 6
:text[6] = 'b'
与pattern[k] = 'b'
匹配,k = 2
i = 7
:text[7] = 'c'
与pattern[k] = 'c'
匹配,k = 3
k
达到模式字符串长度m
,匹配成功,记录匹配位置7 - 3 + 1 = 5
k
重置为pi[k-1] = 0
重复上述过程,找到所有匹配的位置。
- 匹配过程:
- 继续从
i = 8
开始重复上述过程:- 找到匹配位置
i = 8 + 3 - 1 = 8
- 重置
k
为0
- 找到匹配位置
- 最终匹配位置为
[2, 5, 8, 11]
- 继续从
最终,模式字符串 "abc"
在目标字符串 "ababcabcababcabc"
中的匹配位置为 [2, 5, 8, 11]
。