概述
KMP(由Knuth,Morris,Pratt三个人发明)算法,时间复杂度为:
T
=
O
(
n
+
m
)
T=O(n+m)
T=O(n+m)
相比于暴力匹配的O(mn)
有一定提高。
KMP算法的核心思想就是当发生失配时,则在前面已经匹配的部分中,找到最长的相同前缀,如下图的紫色和绿色部分,那么下次移位时直接将前面的前缀和后面对齐即可,从而不必每次只移动一位。这样string中的指针不会回溯。
构造match数组
为了得知当发生失配时我们要在子串中回退到哪里,我们需要针对pattern
进行如下分析处理:
定义match
函数构造next
表:
m a t c h ( j ) = { 满 足 p 0 . . . p i = p j − i . . . p j 的 最 大 i ( < j ) − 1 , 如 果 这 样 的 i 不 存 在 match(j)= \begin{cases} 满足p_0...p_i=p_{j-i}...p_j的最大i(<j)\\ -1,如果这样的i不存在 \end{cases} match(j)={满足p0...pi=pj−i...pj的最大i(<j)−1,如果这样的i不存在
例如:
match(j)
只与比较小的 pattern
子串有关,保存了子串中一首一尾能够匹配的最长子串的信息,我们
将其保存在match[]
数组里。
递归地,当计算match[j]
时,如果pattern[match[j-1] + 1] == pattern[j]
,则可以直接得到match[j]=match[j-1]+1
。
如果没那么幸运pattern[match[j-1] + 1] != pattern[j]
,也不至于回到起点重开,可以根据match[j-1]
的match值,向前找到匹配的部分(下面第一块绿色部分),则对于相同的后半段,最后的紫色部分和最开始的绿色部分一定是匹配的,再利用上面的步骤即可。
第一个问号处是match[match[j-1]+1]
,第二个问号处当然是j
。
KMP算法
有了match[]
数组,KMP算法的实现很简单:
定义s
和p
两个指针分别从头开始遍历主串和模式串,当二者都未到达结尾时执行如下循环:
if (str[s] == pattern[p])
,说明当前字符匹配,s和p同时向后移动;
else if (p > 0)
,当前字符失配,且不是模式串的第0个字符,就是下图的平凡情况,则需要将p指针回溯到绿色部分末尾,也就是p-1
的match值:p = march[p - 1] + 1;
;
否则说明模式串的第一个字符就不匹配,++s
;
代码实现
#include <stdlib.h>
#include <string.h>
void buildMatch(char const* pattern, int* match) {
int prev = 0, j = 0;
int m = strlen(pattern);
match[0] = -1;
for (j = 1; j < m; ++j) {
prev = match[j - 1];
while ((prev >= 0) && (pattern[prev + 1] != match[j])) {
//i不断回退
prev = match[prev];
}
if (pattern[j] == pattern[prev + 1]) {
//pattern[j]==pattern[match[j-1]+1]
match[j] = prev + 1;
} else {
//没有真子串可以匹配
match[j] = -1;
}
}
return;
}
int KMP(char const* str, char const* pattern) {
//获取str的长度O(n)
int n = strlen(str);
//获取pattern的长度O(m)
int m = strlen(pattern);
int s = 0, p = 0;
if (m > n)
return -1;
int* match = (int*)malloc(sizeof(int) * m);
//构造next表
buildMatch(pattern, match);
//都没有触达结尾O(n),s从来没有回退过
while (s < n && p < m) {
if (str[s] == pattern[p]) {
//当前字符匹配
++s;
++p;
} else if (p > 0) {
//当前字符失配,跳转到上一个小子串末尾
p = match[p - 1] + 1;
} else {
//第一个字符就失配
++s;
}
}
return (p == m) ? s - m : -1;
}
测试代码
void test_KMP() {
printf("\n%s\n", __func__);
char string[] = "This is a simple example";
char pattern0[] = "simple";
char pattern1[] = " isa";
char pattern2[] = "Th";
char pattern3[] = "e";
char pattern4[] = "exam";
char pattern5[] = "sample";
char* patterns[6] = {pattern0, pattern1, pattern2, pattern3, pattern4, pattern5};
int p;
printf("\nstring:%s\n", string);
for (int i = 0; i < 6; ++i) {
printf("\npattern%d:%s\n", i, patterns[i]);
p = KMP(string, patterns[i]);
if (p != -1) {
printf("%s\n", string + p);
} else {
printf("Not found\n");
}
}
return;
}