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]; |
048 | } |
049 | } |
050 |
051 | int kmp() |
052 | { |
053 | int i = 0, j = 0; |
054 | str_len = strlen (str); |
055 | getNext2(); // getNext(); |
056 | while (i < str_len && j < pattern_len) |
057 | { |
058 | if (j == -1 || str[i] == pattern[j]) |
059 | { |
060 | ++i; |
061 | ++j; |
062 | } |
063 | else |
064 | j = next[j]; |
065 | } |
066 | if (j >= pattern_len) |
067 | return i - pattern_len; |
068 | else |
069 | return -1; |
070 | } |
071 |
072 | void printNext() { |
073 | for ( int i = 0; i < pattern_len; i++) |
074 | cout << next[i] << " " ; |
075 | cout << endl; |
076 |
077 | } |
078 |
079 | void getnext3(){ |
080 | int i=0; |
081 | int j= next[0] = -1; |
082 | while ( i < pattern_len){ |
083 | if ( j==-1 || pattern[i] == pattern[j] ){ |
084 | i++; j++; |
085 | next[i] = j; |
086 | } else |
087 | j = next[j]; |
088 | } |
089 | } |
090 |
091 | int main() |
092 | { |
093 | str = "AAAABAAACABAA" ; |
094 | pattern = "AAACAB" ; |
095 | pattern_len = strlen (pattern); |
096 | int res = kmp(); |
097 | cout << res << endl; |
098 | printNext(); |
099 |
100 | } |