KMP 匹配算法
在开发中,经常会遇到在一个字节数组中,查找一个子数组的问题。如果不是字节数组,而是字符串的话,直接通过 string.IndexOf 就可以解决,对于字节数组还是需要做一点功课。
因为字符串比较容易观察,所以,我们首先通过字符串来分析,然后,再在字节数组上实现。
问题:
对于一个源字符串 source = "abababaababacb" 来说,查找其中包含子串 pattern = "ababacb" 出现的位置下标。
首先,我们通过最基本的方法来进行查找。
i 表示当前用来匹配的 source 中字符的下标,j 表示当前用来匹配的模板的下标。
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
b |
j |
0 |
1 |
2 |
3 |
4 |
5 |
|
|
|
|
|
|
|
|
pattern |
a |
b |
a |
b |
a |
c |
|
|
|
|
|
|
|
|
当 j = 5 的时候,我们可以发现,source[ 5 ] != pattern[ 5 ] , 问题是下一次使用 pattern 的哪个位置来重新开始匹配?
最简单的处理就是让 j = 0 , i 增加一个位置 i = 1, 然后,重新开始,下一次,i = 2,再重新开始,直到完成。
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
b |
j |
|
0 |
|
|
|
|
|
|
|
|
|
|
|
|
pattern |
|
a |
|
|
|
|
|
|
|
|
|
|
|
|
但是,我们对于我们的 pattern 来说,比较容易看到,pattern 下标为 2,3,4 的三个字符正好与 pattern 开始的三个字符相同,都是 "aba"
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
pattern |
a |
b |
a |
b |
a |
c |
b |
|
|
|
|
|
a |
b |
a |
b |
a |
c |
b |
|
|
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
因此,并不需要让 j=0, 可以直接让 j=3, 从 3 开始比较,而 i 不变,这样可以减少 3 次以上的比较。
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
b |
j |
|
|
0 |
1 |
2 |
3 |
|
|
|
|
|
|
|
|
pattern |
|
|
a |
b |
a |
b |
|
|
|
|
|
|
|
|
继续进行比较,直到 i=7, j= 5 的时候,又不匹配了。
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
b |
j |
|
|
0 |
1 |
2 |
3 |
4 |
5 |
|
|
|
|
|
|
pattern |
|
|
a |
b |
a |
b |
a |
c |
|
|
|
|
|
|
根据上一次的经验,我们又可以让 j =3, i 不变,然后进行匹配。但是还是不能匹配。
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
B |
j |
|
|
|
|
0 |
1 |
2 |
3 |
|
|
|
|
|
|
pattern |
|
|
|
|
a |
b |
a |
b |
|
|
|
|
|
|
由于,我们还可以再调整一次 j = 1, 然而,还是不能匹配。
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
B |
j |
|
|
|
|
|
|
0 |
1 |
|
|
|
|
|
|
pattern |
|
|
|
|
|
|
a |
b |
|
|
|
|
|
|
没有办法,只能让 j = 0 再次从头开始了。这次完全匹配。
I |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Source |
a |
b |
a |
b |
a |
b |
a |
a |
b |
a |
b |
a |
c |
b |
J |
|
|
|
|
|
|
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
|
|
|
|
|
|
|
|
a |
b |
a |
b |
a |
c |
b |
通过上面的分析,我们可以看到,由于模式串 pattern 内在的规律性,使得我们不必在每次匹配失败的时候,回溯到起始位置重新开始,而是可以利用已经匹配的部分来减少匹配的次数。如果我们提前通过预处理,构建出一个回溯的函数,就可以通过这个函数获取回溯的位置。
对于 pattern = "ababacb" 来说,我们可以构建一个回溯函数表 next ,列出在匹配失败的时候,应该回溯的位置。
对于 next[0] 来说,显然为 0.
对于 next[1] 来说,也只能从 0 再开始匹配。
以后的匹配值,需要参考上一次的匹配,如果继续匹配了,那么这个值应该增加 1,否则,应为最大匹配值。
对于 pattern = "ababacb" 的匹配函数如下:
pattern |
a |
b |
a |
b |
a |
c |
b |
j |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
next |
0 |
0 |
0 |
1 |
2 |
3 |
0 |
但是,在这个匹配函数,对于 j 为 0,1,2 的情况来说,0, 1 是没有匹配,2 实际上匹配了第一次,这样的话,对于不匹配或者第一次匹配,next 函数都是 0, 不方便计算。
我们可以将 next 函数定义为当前是第几个匹配,这样的话,如果匹配了,就是大于 0 , 如果没有匹配就是 0 ,比较方便计算。
在进行匹配的过程中,匹配失败则取得上一个 next 函数的值,就可以取得当前位置之前的匹配。
pattern |
a |
b |
a |
b |
a |
c |
b |
j |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
next |
0 |
0 |
1 |
2 |
3 |
0 |
0 |
对于字节来说,匹配过程完全相同,构造一个 KMP 匹配函数的方法如下
2 {
3 int[] next = new int[pattern.Length];
4
5 next[0] = 0; // 第一个位置一定为 0
6
7 int j = 0; // 匹配的起始位置
8 for (int i = 1; i < pattern.Length; i++)
9 {
10 // 如果已经匹配上,但是现在不能匹配,回溯寻找
11 while( j>0 && pattern[j] != pattern[i] )
12 {
13 j = next[j-1];
14 }
15
16 // 如果能够匹配上,向下推进一个位置
17 // 注意 i 在 for 循环中自动推进
18 if (pattern[j] == pattern[i])
19 j++;
20
21 // 保存
22 next[i] = j;
23 }
24 return next;
25 }
用于进行匹配的函数
2 {
3 int length = pattern.Length;
4 int[] next = BuildKMP(pattern);
5 int j = 0;
6 for (int i = pos; i < source.Length; i++)
7 {
8 while (j > 0 && source[i] != pattern[j])
9 j = next[j-1]; // 调整下一个匹配的位置
10 if (source[i] == pattern[j])
11 j++;
12 if (j == length)
13 return i-length +1;
14 }
15 return -1;
16 }