问题背景
问题可以描述成:给定一个字符串文本T,要从中找出是否含有某个子串P。我们把P叫做模式字符串。这个问题最直接的解法就是逐个匹配:先将T和P左对齐,从头开始依次比较P中的每个字符是否和T中对应的字符相同。例如,T 为“a a b a c a b a b c a b a c a b”,P为“a b a b c a b”,一开始将T和P左对齐并依次比较各个字符是否相同,
(1) T: a a b a c a b a b c a b a c a b P: a b a b c a b
当我们比较到第二个字符的时候,发现不一样,于是将P右移一位,再从头开始匹配:
(2) T: a a b a c a b a b c a b a c a b P: a b a b c a b
这时候我们发现前三个字符是相同的,匹配到第四个字符的时候不同,匹配失败。于是继续将P右移一位,再次进行以上的匹配过程,直至P的所有字符和T中的某个子串匹配。对于上面这个例子,当我们进行到如下所示的比较时,即匹配成功:
(3) T: a a b a c a b a b c a b a c a b P: a b a b c a b
如果我们不是这么走运,当比较到T的最后一个字符时,仍然没有完全匹配,则匹配失败。
如何更快地匹配
在上述的匹配过程中,我们每次只将P右移一位,因此匹配过程很缓慢。通过观察我们发现,有时候仅仅将P右移一位的匹配是明显无效的,从而可以一次右移多位,提高匹配效率。具体来说,我们看上面的第二次匹配,如果仅仅将P右移一位,因为P的第一个字符a和T中已匹配子串(a b a)的第二个字符,也就是P的第二个字符b不同,所以右移一位是明显无效的。我们可以直接将P右移两位进行匹配,这样提高了效率。为什么只右移两位、而不多移几位呢?因为当右移两位的时候,P的第一个字符和T中已匹配子串的最后一个字符相同,这时候我们需要继续比较后面的字符才能确定是否匹配成功。
更一般化的描述是,假设当前P的前q个字符已经匹配,而第q+1个字符不匹配,这时候我们的目标是确定最多能将P右移多少位、从而跳过那些明显无效的匹配。从上面的例子可以发现,要完成这个目标,我们只需要知道P的前q个字符的信息即可。这种利用模式字符串本身的局部特征提高匹配效率的方法,就是KMP算法的精髓所在。KMP算法的命名来自它的三个发现者D.E.Knuth,J.H.Morris和V.R.Pratt的名字的首字母,这种方法可以大大提高字符串匹配的效率,从而将求解这一问题降至线性复杂度。
回到前面的问题,如何利用模式字符串本身的局部特征呢?回顾前面的过程,当我们每次决定能右移多少位的时候,实际是在拿已匹配部分的前缀和它本身的后缀相比,我们最多只能右移到已匹配子串的前缀和后缀刚好匹配的位置。前缀和后缀相匹配的部分越长,我们能右移的步数就越少;前缀和后缀相匹配的部分越短,我们能右移的步数就越多。例如,假设当前已匹配部分是a b c a b,它的前缀有四个a、a b、a b c、a b c a,对应的后缀有四个b、a b、c a b、b c a b。我们依次比较每一对前缀后缀,可能有多个相匹配的前缀后缀,我们只需要找到最长的那个就OK啦,如下:
a b c a b | a b c a b | a b c a b a b c a b | a b c a b | a b c a b
当执行第三次比较时,前缀后缀匹配(都是a b),最大匹配长度是2。已匹配子串总长度是5,所以这时可以直接将P右移5 - 2 = 3位(因为前两次右移已经明显无效啦)。可以设想,对于q = 1, 2, ... , m,其中m为模式字符串P的长度,如果我们已经找到P的每个子串P[1...q]的最大前缀后缀匹配长度,记为π[q],那么当P的第q+1个字符和T中对应字符不匹配时(意味着前q个字符已匹配),我们可以直接将P右移q-π[q]位、从而跳过那些明显无效的匹配。这里的π[q]称为前缀函数,他的元素定义为:
定义一:π[q]的值为P的子串P[1...q]的最大前缀后缀匹配长度。
有了π[q],我们就可以执行KMP快速匹配了。
如何计算前缀函数
在实际操作中,给定模式字符串P,我们先要求出它的前缀函数π[q], 然后执行快速匹配操作。本文到此为止,都很好理解。而如何计算前缀函数,才是比较难理解的部分,《算法导论》上关于这一部分,又是符号又是公式,又是定理 又是引理的,看的让人云里雾里(可能是为了完整的描述和证明这个问题)。当然,你依然可以用蛮力的方法计算前缀函数,这里介绍一种更高效的方法。
因为显然π[1] = 0,所以考虑用递归方法。假设我们已经知道k=π[q](设k > 0(条件1))的值,要求π[q+1],即子串P[1...(q+1)]的最大前缀后缀匹配长度。根据定义,P[1...q]的最大前缀后缀匹配长度为π[q],如下图,用红色x表示,这时再增加一个字符P[q+1]时,前缀后缀是否依然匹配呢?我们先比较P[q+1]与P的第π[q] + 1 = k + 1个字符,即两个*表示的字符:
Index : 1 2 ... π[q] π[q]+1 ... @ @ ... q q+1
P : x x ... x * ... x x ... x *
(注:上图中两个@符号表示的下标分别为q - π[q] + 1和q - π[q] + 2。)
如果P[q+1] == P[k+1](条件2),那么可以直接增加第q+1个字符、且此时前缀后缀刚好匹配,所以π[q+1] = π[q] + 1 = k + 1。然而,大多数时候我们没有这么“走运”,即P[q+1] != P[k+1](条件3),然而这时我们不能直接得到π[q+1] = 0。因为虽然P[q+1] != P[k+1],但是如果前缀子串P[1...π[q]](也是子串P[1...q]的后缀,上图中的红色部分)本身又有相匹配的前缀后缀,假设长度为L,0 < L< π[q],也即P[1...q]的前L个字符和它的后L个字符匹配(令k = L,这时又回到了条件1),那么我们还要将字符P[q+1]和P[l+1]比较,如果P[q+1] == P[L+1](回到条件2),那么π[q+1] =L + 1;否则(回到条件3)我们要继续查看子串P[1...k]中是否有相匹配的前缀后缀……如此往复,直至匹配子串中找不到相匹配的前缀后缀(条件4)--就拿上图来说,假设子串P[1...π[q]]没有相匹配的前缀后缀,此时,我们直接比较P[q+1]和P[1],如果P[q+1] == P[1],则π[q+1] = 1;否则π[q+1] = 0。这样,就可以求出π[q+1]的值;利用递归就可以求出π的所有元素。
那么问题来了,子串P[1...π[q]]本身的最大前缀后缀匹配长度是多少?没错,这就是我们前面给出的定义一,答案是π[π[q]]。这里递归地利用了前面的求解结果,这也是整个问题最难理解的部分。如果这里理解了,剩下的问题就简单了。
这里我们给个例子说明以上求解过程。假设 P = “a b a b a a c”,要求π[q]。
- 显然π[1] = 0。
- 求π[2],q = 1,因为π[1] = 0,满足(条件4),所以比较P[2]和P[1],不相等,所以π[2] = 0。
- 求π[3],q = 2,满足(条件4),所以比较P[3]和P[1],相等,所以π[3] = 1。
- 求π[4],q = 3,满足(条件1),所以比较P[4]和P[π[3]+1] = P[2],相等,满足(条件2),所以π[4] = π[3] + 1 = 2。
- 求π[5],q = 4,满足(条件1),所以比较P[5]和P[π[4]+1] = P[3],相等,满足(条件2),所以π[5] = π[4] + 1 = 3。
- 求π[6],q = 5,满足(条件1),所以比较P[6]和P[π[5]+1] = P[4],如下图
Index: 1 2 3 4 5 | 6 7
P : a b a b a | a c
P : a b a b a | a c
图中红色部分是子串P[1...5]相匹配的最大前缀后缀。虽然P[6] != P[4]满足(条件3),但是我们不能直接得到π[6] = 0;这时还要看P[1...π[5]]是否有相匹配的前缀后缀及其最大长度是多少,答案是l = π[π[5]] = π[3] = 1 > 0,回到(条件1),所以继续比较P[6] 和 P[l + 1] = P[2],不相等,满足(条件3),因此我们继续查看P[1...l] = P[1...1]是否有相匹配的前缀后缀及其最大长度是多少,答案是l = π[1] = 0,满足(条件4),因此比较P[6]和P[1],相等,所以 π[6] = 1。
求π[7],q = 6,满足(条件1),所以比较P[7]和P[π[6]+1] = P[2],不相等,满足(条件3),因为l = π[π[6]] = π[1] = 0,满足(条件4),因此比较P[7]和P[1],不相等,所以 π[7] = 0。
计算前缀函数的伪代码。最后给出计算前缀函数的伪代码,转自《算法导论》:
COMPUTE-PREFIX-FUNCTION(P) 1 m = length(P) 2 π[1] = 0 3 k = 0 4 for q = 2 to m 5 while k > 0 and P[k + 1] != P[q] 6 k = π[k] 7 if P[k + 1] == P[q] 8 k = k + 1 9 π[q] = k 10 return π
首先可以保证第5行的k一定等于π[q - 1]:当q = 2时,第一次进入第5行,第2行和第3行初始化保证了k = π[2 - 1] = π[1] = 0;其它情况下代码第9行对此做了保证。所以在计算π[q]时,我们首先判断是否满足条件1(第5行:k > 0),同时判断是否满足条件3(第5行:P[k + 1] != P[q]),如果都满足,则更新k的值(第6行)并继续判断;否则,如果满足条件2(第7行:P[k + 1] == P[q]),则将k值加1(第8行);如果不满足条件2,即满足条件4,此时k = 0,所以直接将新的k值赋给π[q](第9行)。最后,对所有的q都计算完成后,返回前缀函数(第10行)。
kmp-matcher(T, P) n<-length[T] m<-length[P] pi<-compiter-prefix-function(P) q<-0 for i<-1 to n do while q>0 and P[q+1]!=T[i] do q<-pi[q] //前缀的前缀... if P[q+1]==T[i] then q<-q+1 if q==m then print “Pattern occurs with shift”i-m q<-pi[q]
KMP代码:
public class KMP { private String text; private String pattern; private int[] n; public KMP(String text, String pattern) { super(); this.text = text; this.pattern = pattern; this.n = new int[pattern.length()]; computePrifix(pattern); } private void computePrifix(String pattern) { int m = pattern.length(); n[0] = 0; int k = 0; for (int q= 2; q <= pattern.length(); q++) { while (k > 0 && pattern.charAt(k) != pattern.charAt(q-1)) { k = n[k-1]; } if (pattern.charAt(k) == pattern.charAt(q-1)) k = k + 1; n[q-1] = k; } } public int kmp() { int length = text.length(); int m = pattern.length(); if (m > length) return -1; int q = 0; for (int i = 0; i < length; i++) { while (q > 0 && pattern.charAt(q) != text.charAt(i)) { q = n[q-1]; } if (pattern.charAt(q)== text.charAt(i)) q++; if (q == m) return i-m; } return -1; } public static void main(String[] args) { KMP kmp=new KMP("aabacababcabacab", "ababcab"); System.out.println(kmp.n); for (int i = 0; i <kmp.pattern.length(); i++) { System.out.println(kmp.n[i]); } System.out.println(kmp.kmp()); } }