问题背景
在我们平时使用计算机的过程中,有一项使用非常频繁的功能就是字符串查找。这个问题可以描述成:给定一个字符串文本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]]。这里递归地利用了前面的求解结果,这也是整个问题最难理解的部分。如果这里理解了,剩下的问题就简单了。
理解上述过程的核心点是:当比较到第q+1个字符不匹配时,我们无论将P右移多少位进行后续比较,必然要先比较不匹配位置之前的子串是否匹配。只有 那些右移后子串仍然匹配的移动才可能是有效的。如下图,假设一开始已匹配子串为P的前5个字符,而第6个字符不匹配(一个是&,一个是#)。这时,无论我们将P右移多少位,假设右移3位,必然要先比较&前面的两个字符(即已匹配子串的最后两个字符)和P的前两个字符(也是已匹配子串的前两个字符)(图中红色标出的x)是否匹配,只有这两个字符先匹配了,我们才需要再比较&和P的第三个字符是否相同。
T: ................x x x x x & .......................P: x x x x x # ... P右移3位: x x x x x # ..
这里我们给个例子说明以上求解过程。假设 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] = 03 k = 04 for q = 2 to m5 while k > 0 and P[k + 1] != P[q]6 k = π[k]7 if P[k + 1] == P[q]8 k = k + 19 π[q] = k10 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行)。