KMP(Knuth-Morris-Pratt)算法是一种改进的字符串匹配算法,用于解决在一个文本串S内查找一个词串P的出现位置的问题。与朴素的暴力匹配不同,KMP算法可以在匹配失败时利用已经部分匹配的有效信息,避免从头开始匹配,从而提高匹配效率。
原理
KMP算法的核心是利用一个“部分匹配表”(也称为“失败函数表”或“跳转表”),该表记录了词串P中各个位置上的字符,与词串P的某个前缀能够匹配的最长长度。在匹配过程中,当发生不匹配时,可以利用这个表来确定下一步应该从哪里开始匹配,从而避免不必要的比较。
部分匹配表的构造
- 初始化一个空表。
- 对于词串P的每个位置i(从1开始),计算以该位置结尾的子串的前缀和后缀的最长公共元素长度,并将这个值记录在表中对应的位置。
- 如果不存在这样的公共元素,则对应位置的值为0。
匹配过程
- 将词串P与文本串S的第一个字符对齐。
- 从左到右逐个比较P和S的字符。
- 如果出现不匹配,查找部分匹配表中当前位置的值,将P向前滑动这个值的距离,然后从新的位置开始继续匹配。
- 如果全部匹配成功,返回词串P在文本串S中的起始位置。
实例
假设词串P为"ABCDABD",文本串S为"ABCABABDABCDABDABCDABABCAB"。
- 构造部分匹配表:
- A的后缀和前缀无公共元素:0
- AB的后缀B和前缀B相同:1
- ABC的后缀BC和前缀无公共元素:0
- ABCD的后缀CD和前缀无公共元素:0
- ABCDA的后缀DA和前缀A相同:1
- ABCDAB的后缀AB和前缀AB相同:2
- 因此,部分匹配表为[0, 1, 0, 0, 1, 2]。
- 匹配过程:
- 将P与S的第一个字符对齐。
- 比较到P的第四个字符D时,发现不匹配。
- 查找部分匹配表中位置4的值,为0,因此P不动,S向右移动一个字符。
- 继续比较,直到P全部匹配成功,或P移动到S的末尾。
应用场景
KMP算法在字符串匹配、搜索引擎、文本编辑器、生物信息学等领域有广泛应用。例如,在搜索引擎中,KMP算法可以快速定位网页中关键词的出现位置,从而提高搜索效率;在文本编辑器中,KMP算法可以实现高效的查找和替换功能;在生物信息学中,KMP算法可以用于DNA序列的比对和分析等。
总的来说,KMP算法通过充分利用已经匹配的信息,减少了不必要的比较次数,从而提高了字符串匹配的效率
KMP算法C++代码示例
cpp复制代码
#include <iostream> | |
#include <vector> | |
#include <string> | |
using namespace std; | |
// 计算部分匹配表 | |
vector<int> computeLPSArray(const string& pat, int M) { | |
vector<int> lps(M, 0); | |
int len = 0; // length of the previous longest prefix suffix | |
int i = 1; | |
lps[0] = 0; // lps[0] is always 0 | |
// loop to calculate lps[i] for i = 1 to M-1 | |
while (i < M) { | |
if (pat[i] == pat[len]) { | |
len++; | |
lps[i] = len; | |
i++; | |
} else { | |
// (pat[i] != pat[len]) | |
if (len != 0) { | |
// This is tricky. Consider the example. | |
// AAACAAAA and i = 7. The idea is similar | |
// to search step. | |
len = lps[len - 1]; | |
// Also, note that we do not increment i here | |
} else { | |
// if (len == 0) | |
lps[i] = 0; | |
i++; | |
} | |
} | |
} | |
return lps; | |
} | |
// KMP搜索函数 | |
int KMPSearch(const string& pat, const string& txt) { | |
int M = pat.length(); | |
int N = txt.length(); | |
// 创建LPS数组 | |
vector<int> lps = computeLPSArray(pat, M); | |
int i = 0; // index for txt[] | |
int j = 0; // index for pat[] | |
while (i < N) { | |
if (pat[j] == txt[i]) { | |
j++; | |
i++; | |
} | |
if (j == M) { | |
cout << "Found pattern at index " << i - j << endl; | |
j = lps[j - 1]; | |
} else if (i < N && pat[j] != txt[i]) { | |
// Mismatch after j matches | |
if (j != 0) | |
j = lps[j - 1]; | |
else | |
i = i + 1; | |
} | |
} | |
return 0; | |
} | |
int main() { | |
string txt = "ABABDABACDABABCABAB"; | |
string pat = "ABABCABAB"; | |
KMPSearch(pat, txt); | |
return 0; | |
} |
时空复杂度分析
时间复杂度
- 部分匹配表计算 (
computeLPSArray
): 这个函数的时间复杂度是O(M),其中M是模式串的长度。因为我们需要遍历整个模式串一次来计算LPS数组。 - KMP搜索 (
KMPSearch
): 这个函数的时间复杂度是O(N),其中N是文本串的长度。在最坏情况下,我们可能需要遍历整个文本串一次。尽管在每次不匹配时,我们都会使用LPS数组来跳过一些字符,但总体上,我们仍然需要遍历文本串的每个字符至少一次。
因此,总的时间复杂度是O(M + N)。
空间复杂度
- 我们使用了一个大小为M的LPS数组来存储部分匹配表。因此,空间复杂度是O(M)。
请注意,这里的时空复杂度分析是基于理想情况的。在实际应用中,由于缓存、内存访问模式和其他因素,实际性能可能会有所不同。