昨天做了Twitter的OA,最后一题是一个KMP算法的变形,之前看过KMP算法,但是看的不甚仔细,最后还是边学边写才写出来,运气甚好,一次AC。这种OA的环境下,如果不是AC,真是连bug都难看出来。这次整理一遍,下次遇到的时候可不能在靠运气了。
1. brute-force 解决 KMP算法是用来解决字符串匹配问题的。heystack长度为m, needle长度为n,那么最简单的方法是暴力搜索,算法复杂度为O(m * n)
1
2
3
4
5
6
7
8
9
10
11
12
int Strstr (string heystack, string needle) {
int heystack_size = heystack.size(), needle_size = needle.size();
for (int i = 0 , j; i <= heystack_size - needle_size; i++) {
for (j = 0 ; j < needle_size; j++) {
if (heystack[i+j] != needle[j]) break ;
}
if (j == needle_size) return i;
}
return -1 ;
}
2. 两个case 这种O(m * n)的算法并不是最优的,为什么呢?因为有很多子问题被重复计算了。什么是重复子问题,我们想象这样一种序列,用”XABCDE”匹配”AAAAAAAAXABCDE”,这个用暴力算法好吗?从结果来看,是好的,为什么?因为每次几乎没有进入到第二个循环里面就往下走了 ,needle中”X”字符守门守的好。什么时候暴力算法会失效呢?考虑一下用”AAAAA”匹配”AAAACAAAACAAAAA”,每次第二个循环几乎都会走满,每次都功亏一篑。而这第二种情况中就隐藏着重复的子问题。
2.1 case 1 先考虑一个needle字符串”AAABC”,以及一个heystack字符串”AAABDAABC”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
匹配第1步:
heystack: AAABDAABC
|
needle: AAABC
匹配第4步:
heystack: AAABDAABC
|
needle: AAABC
注意:以上的递增是以needle为轴的递增
Brute-Force: 失效,heystack为轴递增1步
heystack: AAABDAABC
|
needle: AAABC
KMP: 失效,根据needle的信息,以heystack为轴递增多步
heystack: AAABDAABC
|
needle: AAABC
对于我们已经匹配成功的pattern(pattern == “AAAB”,这是needle的一个substring), 我们观察pattern中以左边为开始的substring:A AA AAA,和以B为结束的substring B AB AAB,我们注意到两两并不匹配,那么意味着,即便我们将needle左移任意长度(这部分其实是pattern的substring prefix),也不会匹配到pattern的任何前缀(substring prefix) ,在这种情况下我们已经没必要左移needle,不妨将needle归零,heystack步进一位。
值得注意的是,以上的信息是由needle得到的,与heystack无关,这意味着这个撤回的信息可以提前由needle的信息进行计算。
2.2 case 2 再先考虑另一个needle字符串”AABAAC”,以及一个heystack字符串”AABAABAAC”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
匹配第1步:
heystack: AABAABAAC
|
needle: AABAAC
匹配第4步:
heystack: AABAABAAC
|
needle: AABAAC
注意:以上的递增是以needle为轴的递增
Brute-Force: 失效,heystack为轴递增1步
heystack: AABAABAAC
|
needle: AABAAC
KMP: 失效,根据needle的信息,以heystack为轴递增多步
heystack: AABAABAAC
|
needle: AABAAC
这个案例与之前不同的地方在于,pattern(我们称已经匹配的substring为pattern, pattern等于”AABAA”)中有以左边为开始的substring: A AA AAB AABA,和右边为开始的substring A AA BAA ABAA有重复的部分AA(AA is a substring that is prefix and also a suffix, 一个既是前缀也是后缀的子串 ),这意味着我们应该将needle左移两个位置,重新开始匹配,此时heystack保持原有位置不变。
3. KMP算法 现在问题化为连个问题,1. 如何求最长的共同前缀后缀(LPS:longest prefix is also suffix); 2. 有了最长前缀后缀后,又该如何进行比较。
3.1 求解LPS(longest prefix which also is suffix) 先解决如何求lps的问题,这个其实也是一个比较trick的部分,但是在很多对于KMP的解释中,都会掠过这个部分。 我们考虑一个字符串S(n),假设lps[n-1] = c,会有一下几种case:
case 0: s中的第n个字符(新增的那一个)和s[c]相等,则lps[n] = c+1, 考虑一个字符串”AAACAAA” + ‘C’,当前有 lps(“AAACAAA”) == 3, 添加了’C’后,lps(“AAACAAAC”) = 3+1;
case 1: 若新增字符不等,且c == 0,这意味着前n-1个字符没有substring既是前缀又是后缀,那么新增了一个和s[0]不等的字符,lps[n] = 0;
case 2: 若新增字符不等,且c != 0,虽说新增的首字母和lps[c]不匹配,考虑一个字符串”AAACAAA” + ‘A’,当前有 lps(“AAACAAA”) == 3, 但是如果添加的是’A’,lps(“AAACAAAA”)一定不为3,但是有可能会是3或者是2,1,需要一步步递减搜索,直到子串为空串,或找到了一个子串;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void computeLPS (string &needle, vector <int > &lps, int pos) {
lps[pos] = 0 ;
if (pos == 0 ) return ;
computeLPS(needle, lps, pos-1 );
for (int length = lps[pos-1 ]; length >= 0 ; length--) {
if (needle[pos] == needle[length]) {
lps[pos] = length + 1 ;
break ;
}
}
}
3.2 字符串匹配 最后我们解决字符串匹配的问题,从章节2的两个例子中,我们可以看到一下几种情况:
case 0: needle和heystack字符相匹配,needle和heystack的指针均向后移动
case 1: needle和heystack字符不匹配,若当前的已经匹配的pattern的LPS为零,heystack指针向后移动,needle归零,如我们的第一例
case 2: neelde和heystack字符不匹配,若当前的已经匹配的pattern的LPS不为零,heystack指针维持不变,needle向前回撤,如同我们的第二例,值得注意的是,needle回撤的操作是递归的
代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
vector <int > lps;
int KMP_search (string heystack, string needle) {
int heystack_size = heystack.size(), needle_size = needle.size();
int hey_pos = 0 , ned_pos = 0 ;
while (hey_pos < heystack_size) {
if (heystack[hey_pos] == needle[ned_pos]) {
hey_pos++, ned_pos++;
if (ned_pos == needle_size) {
return hey_pos - ned_pos;
}
}
else {
if (ned_pos == 0 ) {
hey_pos++;
}
else {
ned_pos = lps[ned_pos-1 ];
}
}
}
return -1 ;
}
4. 完整实现 可直接编译的版本如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <vector>
#include <string>
using namespace std ;
void computeLPS (string &needle, vector <int > &lps, int pos) {
lps[pos] = 0 ;
if (pos == 0 ) return ;
computeLPS(needle, lps, pos-1 );
for (int length = lps[pos-1 ]; length >= 0 ; length--) {
if (needle[pos] == needle[length]) {
lps[pos] = length + 1 ;
break ;
}
}
}
int KMP_search (string heystack, string needle) {
int heystack_size = heystack.size(), needle_size = needle.size();
int hey_pos = 0 , ned_pos = 0 ;
vector <int > lps(needle.size(), 0 );
computeLPS(needle, lps, needle.size()-1 );
while (hey_pos < heystack_size) {
if (heystack[hey_pos] == needle[ned_pos]) {
hey_pos++, ned_pos++;
if (ned_pos == needle_size)
return hey_pos - ned_pos;
}
else {
if (ned_pos == 0 )
hey_pos++;
else
ned_pos = lps[ned_pos-1 ];
}
}
return -1 ;
}
int main () {
string heystack ("ABABABCABABABCABABABAC" ) ;
string needle ("ABABAC" ) ;
int ret = KMP_search(heystack, needle);
return 0 ;
}