KMP算法的基础部分不再多说,详细大家都Google过了。这里做一些总结。
对于KMP算法来说,重点就是 next数组 (也有叫覆盖函数,部分匹配表,lps数组等)。
总之就是 对模式串做预处理,而且该预处理只和 模式串(pattern)本身有关!
假设有模式串 pattern = “abababca”; 则有匹配表:
1 |
char: | a | b | a | b | a | b | c | a | |
2 |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
3 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | |
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
1 |
字符串: "bread" |
2 |
3 |
前缀: b , br, bre, brea |
4 |
5 |
后缀: read, ead, ad , d |
关于 next数组 (也有叫覆盖函数,部分匹配表,lps数组) 的
通俗解释:”部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”ABCDABD”为例,
1 |
- "A" 的前缀和后缀都为空集,共有元素的长度为0; |
2 |
- "AB" 的前缀为[A],后缀为[B],共有元素的长度为0; |
3 |
- "ABC" 的前缀为[A, AB],后缀为[BC, C],共有元素的长度0; |
4 |
- "ABCD" 的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0; |
5 |
- "ABCDA" 的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为 "A" ,长度为1; |
6 |
- "ABCDAB" 的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为 "AB" ,长度为2; |
7 |
- "ABCDABD" 的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。 |
1 |
pattern “AABAACAABAA”, next[] is [0, 1, 0, 1, 2, 0, 1, 2, 3, 4, 5] |
2 |
pattern “ABCDE”, next[] is [0, 0, 0, 0, 0] |
3 |
pattern “AAAAA”, next[] is [0, 1, 2, 3, 4] |
4 |
pattern “AAABAAA”, next[] is [0, 1, 2, 0, 1, 2, 3] |
5 |
pattern “AAACAAAAAC”, next[] is [0, 1, 2, 0, 1, 2, 3, 3, 3, 4] |
下面的代码实现中,lps(longest prefix suffix )数组就是我们说的next数组。这个是最原创的实现,和网上很多优化的next数组的算法不同,但是基本原理是一样的:
01 |
#include<stdio.h> |
02 |
#include<string.h> |
03 |
#include<stdlib.h> |
04 |
05 |
void computeLPSArray( char *pat, int M, int *lps); |
06 |
07 |
void KMPSearch( char *pat, char *txt) |
08 |
{ |
09 |
int M = strlen (pat); |
10 |
int N = strlen (txt); |
11 |
12 |
// 预处理pattern,计算出 lps[]数组记录前缀和后缀的最长匹配 |
13 |
int *lps = ( int *) malloc ( sizeof ( int )*M); |
14 |
int j = 0; // index for pat[] |
15 |
16 |
// Preprocess the pattern (calculate lps[] array) |
17 |
computeLPSArray(pat, M, lps); |
18 |
19 |
int i = 0; // index for txt[] |
20 |
while (i < N) |
21 |
{ |
22 |
if (pat[j] == txt[i]) |
23 |
{ |
24 |
j++; |
25 |
i++; |
26 |
} |
27 |
28 |
if (j == M) |
29 |
{ |
30 |
printf ( "Found pattern at index %d \n" , i-j); |
31 |
j = lps[j-1]; |
32 |
} |
33 |
34 |
// mismatch after j matches |
35 |
else if (pat[j] != txt[i]) |
36 |
{ |
37 |
// Do not match lps[0..lps[j-1]] characters, |
38 |
// they will match anyway |
39 |
if (j != 0) |
40 |
j = lps[j-1]; |
41 |
else |
42 |
i = i+1; |
43 |
} |
44 |
} |
45 |
free (lps); // to avoid memory leak |
46 |
} |
47 |
48 |
void computeLPSArray( char *pat, int M, int *lps) |
49 |
{ |
50 |
int len = 0; // 记录前一个[最长匹配的前缀和后缀]的长度 |
51 |
int i; |
52 |
53 |
lps[0] = 0; // lps[0] 必须是 0 |
54 |
i = 1; |
55 |
56 |
// the loop calculates lps[i] for i = 1 to M-1 |
57 |
while (i < M) |
58 |
{ |
59 |
if (pat[i] == pat[len]) |
60 |
{ |
61 |
len++; |
62 |
lps[i] = len; |
63 |
i++; |
64 |
} |
65 |
else // (pat[i] != pat[len]) |
66 |
{ |
67 |
if ( len != 0 ) |
68 |
{ |
69 |
// 这个地方有陷阱. 考虑这个例子 AAACAAAA ,i = 7. |
70 |
len = lps[len-1]; |
71 |
72 |
// 另外, 注意 i 在这个地方并没有增加 |
73 |
} |
74 |
else // 如果 (len == 0) |
75 |
{ |
76 |
lps[i] = 0; //没有一个匹配的 |
77 |
i++; |
78 |
} |
79 |
} |
80 |
} |
81 |
} |
82 |
83 |
// 测试 |
84 |
int main() |
85 |
{ |
86 |
char *txt = "ABABDABACDABABCABAB" ; |
87 |
char *pat = "ABABCABAB" ; |
88 |
KMPSearch(pat, txt); |
89 |
return 0; |
90 |
} |
测试数据如下:
1) Input:
1 |
txt[] = "THIS IS A TEST TEXT" |
2 |
pat[] = "TEST" |
Output:
1 |
Pattern found at index 10 |
2) Input:
1 |
txt[] = "AABAACAADAABAAABAA" |
2 |
pat[] = "AABA" |
Output:
1 |
Pattern found at index 0 |
2 |
Pattern found at index 9 |
3 |
Pattern found at index 13 |
next[0]=0 的初始化在下面的计算中并不方便,大多数算法都是初始化 next[0] = -1; 而且只有next[0]为-1,其他next[i] >= 0.
下面给出更常见的写法:
01 |
//常规写法,求next数组的值 |
02 |
void getNext() |
03 |
{ |
04 |
int i = 0; //pattern串的下标 |
05 |
int j = -1; // |
06 |
next[0] = -1; |
07 |
while (i < pattern_len - 1) |
08 |
{ |
09 |
if (j == -1 || pattern[i] == pattern[j]) |
10 |
{ |
11 |
++i; |
12 |
++j; |
13 |
next[i] = next[j]; |
14 |
} |
15 |
else |
16 |
j = next[j]; |
17 |
} |
18 |
} |
一样的原理,只是next的数组的含义可能稍有偏差。
但总体来说,Next数组意义:当pattern串失配的时候,NEXT数组对应的元素指导应该用patter串的哪个元素进行下一轮的匹配。
上面的算法是有缺陷的。比如我们的模式串 pattern =“AAAAB”,其中很容易得到next数组为01230。
如果目标匹配串为 “AAAACAAAAB” ,大家可以模拟一下,A要回溯多次。
就是说我们的next数组优化并不彻底。
优化算法:next[i]表示匹配串在i处如果匹配失败下次移到的位置.
最终的优化算法代码:
01 |
//优化算法,求next数组的值 |
02 |
void getNext2() |
03 |
{ |
04 |
int i = 0; //pattern串的下标 |
05 |
int j = -1; // |
06 |
next[0] = -1; |
07 |
while (i < pattern_len - 1) |
08 |
{ |
09 |
10 |
if (j == -1 || pattern[i] == pattern[j]) |
11 |
{ |
12 |
++i; |
13 |
++j; |
14 |
if (pattern[i] != pattern[j]) //正常情况 |
15 |
next[i] = j; |
16 |
else //特殊情况,这里即为优化之处。考虑下AAAAB, 防止4个A形成0123在匹配时多次迭代。 |
17 |
next[i] = next[j]; |
18 |
} |
19 |
else |
20 |
j = next[j]; |
21 |
} |
22 |
} |
虽然两种写求得next值不一样,但是kmp函数的写法是一样的。
参考测试代码:
001 |
#include <iostream> |
002 |
#include <stdio.h> |
003 |
#include <string.h> |
004 |
using namespace std; |
005 |
006 |
int next[100], pattern_len, str_len; |
007 |
char * pattern, * str; |
008 |
009 |
//优化算法,求next数组的值 |
010 |
void getNext2() |
011 |
{ |
012 |
int i = 0; //pattern串的下标 |
013 |
int j = -1; // |
014 |
next[0] = -1; |
015 |
while (i < pattern_len - 1) |
016 |
{ |
017 |
018 |
if (j == -1 || pattern[i] == pattern[j]) |
019 |
{ |
020 |
++i; |
021 |
++j; |
022 |
if (pattern[i] != pattern[j]) //正常情况 |
023 |
next[i] = j; |
024 |
else //特殊情况,这里即为优化之处。考虑下AAAAB, 防止4个A形成0123在匹配时多次迭代。 |
025 |
next[i] = next[j]; |
026 |
} |
027 |
else |
028 |
j = next[j]; |
029 |
} |
030 |
} |
031 |
032 |
//求next数组的值 |
033 |
void getNext() |
034 |
{ |
035 |
int i = 0; //pattern串的下标 |
036 |
int j = -1; // |
037 |
next[0] = -1; |
038 |
while (i < pattern_len - 1) |
039 |
{ |
040 |
if (j == -1 || pattern[i] == pattern[j]) |
041 |
{ |
042 |
++i; |
043 |
++j; |
044 |
next[i] = j; |
045 |
} |
046 |
else |
047 |
j = next[j]; |