【备注】本文为KMP算法的学习笔记,重点说明KMP算法中最关键的回溯表的构造思路。若对KMP算法涉及到的基础概念(如串的proper suffix, proper prefix等名词)不熟悉,建议先参照阅读本文最后部分给出的参考文献。
Knuth–Morris–Pratt string searching algorithm (简称KMP算法)是一种高效的子串查找算法,
提到子串查找,一种最简单易实现的方法是从源串第1个字符开始,与目标串进行逐次匹配,匹配思路如下所示(串S为长度为n的源串,W为长度为k的目标串,我们要实现的是在S中查找W,若查找成功,则返回其首次匹配成功的子串的首字符位置):
for(i = 0; S[i] != '\0'; ++i)
{
for(j = 0; S[i+j] != '\0' && W[j] != '\0' && S[i+j] == W[j]; j++);
if (W[j] == '\0') {
// found a match
return i;
}
else {
// match failed
return -1;
}
}
上述方法优点是实现起来简单,缺点是时间复杂度高,最坏情况下的复杂度为O(nm)。
不过在实际使用中,通常在逐次匹配(上述代码第2个for循环所示的过程)的早期就会出现dismatch,从而可以及时回溯进行下次匹配,故这种实现方法效果通常也不会很差,因此较为常用,c的库函数strstr()就是采用这种思路实现的(strstr()的一种实现方法)。
为方便描述,本文约定:S[0, n-1]表示长度为n的源串,W[0, k-1]表示长度为k的目标串。
假设串W的第j个字符W[j-1]与串S的第(i+j-1)个字符S[i+j-1]不相等引起本次匹配失败,这意味着在匹配失败的这个字符之前,串S与串W的对应字符均相等,即S[i, i+j-2]与W[0, j-2]逐字匹配成功。每一次的匹配过程均可用得到类似的、比较隐晦的“历史信息”。
上面的代码中,每次出现dismatch后,外循环的index+1,内循环的index清零,重新开始匹配。可见,该方法并没有充分利用上次匹配失败过程中得到”历史信息“,这是导致其复杂度较高的根本原因。
KMP查找算法正是充分利用了这些匹配失败时的“额外信息”,对回溯过程进行了优化,从而将最坏情况下的时间复杂度降低到了O(k+n),其中k为目标串W的长度,n为源串S的长度。若k远小于n,则KMP算法几乎是在线性时间内完成查找,效率提高的相当明显。
为尽快引入本文的重点(回溯表的构造方法),我们将wikipedia中给出的KMP算法伪码(文末参考文献里有链接)摘出如下:
algorithm kmp_search:
input:
an array of characters, S (the text to be searched)
an array of characters, W (the word sought)
output:
an integer (the zero-based position in S at which W is found)
define variables:
an integer, m ← 0 (the beginning of the current match in S)
an integer, i ← 0 (the position of the current character in W)
an array of integers, T (the table, computed elsewhere)
while m+i is less than the length of S, do:
if W[i] = S[m + i],
if i equals the (length of W)-1,
return m
let i ← i + 1
otherwise,
let m ← m + i - T[i],
if T[i] is greater than -1,
let i ← T[i]
else
let i ← 0
(if we reach here, we have searched all of S unsuccessfully)
return the length of S
上述伪码中,标红且加粗的部分是KMP算法的关键,其对匹配失败时的回溯过程做了优化:不是简单的将外循环变量m+1且对内循环变量清零( m = m + 1, i = 0,这是传统查找过程的做法),而是借助回溯表T,将外循环变量m更新为(m + i - T[i]),其中m+i为引起dismatch的字符在源串S中的index,同时,根据T[i]的值对内循环变量i做更新。
对这两个循环变量的优化是KMP算法的精髓,优化过程中用到的回溯表T则是关键中的关键。
假设目标串W长度为k,则回溯表T是由k个元素构成的数组,其中T[i]决定了源串S的第(m+i)个字符与目标串W的第i个字符匹配失败时,两个串的回溯步骤(从伪码中两个循环变量m, i的更新均用到T[i]可以得到这一结论)。
我们从很多介绍性文章中都可以看到类似的描述:只要给出目标串W,就可以构造出回溯表T,与源串S无关。
相信不少初学KMP算法的童鞋会有疑问:既然T的构造与S无关,那为啥S可以根据T做回溯?为神马这么神奇?反正我当时就有这样的疑问。。。