我所理解的KMP算法
作者:goal00001111(高粱)
始发于goal00001111的专栏;允许自由转载,但必须注明作者和出处
一。简单字符串模式匹配算法的缺陷
设有目标串T(target)和模式串P(pattern),模式匹配是指在目标串T中找到一个与模式串P相等的子串。模式匹配成功是指在目标串T中找到了一个模式串P。
简单字符串模式匹配算法(也就是BF算法)的基本思想是:从目标串T的起始比较位置pos开始(在后面的案例中,我们默认pos = 0),和模式串P的第一个字符比较之,若匹配,则继续逐个比较后继字符,否则从串T的下一个字符起再重新和串P的字符比较之。依此类推,直至串P中的每个字符依次和串T中的一个连续的字符序列(即匹配子串)相等,则称匹配成功,返回该匹配子串的首字符下标;否则成匹配不成功,返回-1。
BF算法的思想很直接,也很容易理解,其时间复杂度为O(lenT*lenP),其中lenT和lenP分别为串T和串P的长度。
我们先给出代码,再做简要分析:
/*
函数名称:BFIndex
函数功能:简单字符串模式匹配算法,若目标串T中从下标pos起存在和模式串P相同的子串,则称匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。
输入参数:const string & T :目标串T
const string & P :模式串P
int pos :模式匹配起始位置
输出参数:int :匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。
*/
int BFIndex(const string & T, const string & P, int pos)
{
int i = pos;
int j = 0;
while (i < T.size() && j < P.size())
{
if (T[i] == P[j]) //如果当前字符匹配,继续比较后继字符
{
++i;
++j;
}
else //否则i,j回溯,重新开始新的一轮比较
{
i = i - j + 1;
j = 0;
//if (i > T.size() - P.size()) //一旦目标串剩余部分子串比模式串短,则无需再比较
// break;
}
}
if (j == P.size()) //匹配成功,返回第一个匹配子串首字符的下标
return i - j;
else
return -1;
}
我们发现,在某一轮比较中,一旦出现字符失配(即T[i] != P[j]),则需将i和j回溯,其中i回溯至i = i - j + 1,j回溯至j = 0。
这样产生了很多不必要的比较,例如(例1):
string T = "aababaabaabc";
string P = "abaabc";
在第4轮比较中,T3T4T5T6T7 == P0P1P2P3P4,我将其简写为T[3…7] == P[0…4](后面的都这样表示),但T[8] != P[5],出现字符失配,需要将i和j回溯,使得i = 4, j = 0。
而在第4轮比较中,我们已经得到了T[6…7] == P[3…4],又P[0…1] == P[3…4],相当于T[6…7]和P[0…1]已经间接地比较过,而且字符匹配了,我们无需进行从i =6, j = 0开始的重复比较。
实际上,当T[8] != P[5],即在i = 8, j = 5处出现字符失配时,我们无需将i回溯,只需将j回溯至failure[j](此时failure[5] = 2)处即可。即当T[8] != P[5]时,我们可以跳过比较T[6…7]和P[0…1](因为它们已经间接地比较过了),直接比较字符T[8]和P[2],这样可以省去很多不必要的回溯和比较,时间复杂度达到O(lenT+lenP)。这就是KMP算法的核心思想。
二.高效的KMP算法
现在继续剖析KMP算法。
我在上文提到当T[i] != P[j]时,我们无需将i回溯,只需将j回溯至failure[j]处即可。我们称failure[j]为模式串P下标j的失效函数。
失效函数的值failure[j]是指当T[i] != P[j]时,接下来与T[i]进行比较的模式串P的元素下标。如上面的例子,当T[8] != P[5]时,因为T[6…7] == P[3…4] == P[0…1],我们可以跳过比较T[6…7]和P[0…1],直接比较字符T[8]和P[2],所以failure[5] = 2。
如果你对失效函数还不太理解,我再举一些例子。
仍然以上面提供的目标串T和模式串S为例,当出现T[1…3] == P[0…2],但T[4] != P[3]时,若采用BF算法,则需要将i和j回溯,使得i = 2, j = 0。
而采用KMP算法,则无需将i回溯,j也不需要回溯至j = 0,而只需回溯至j = failure[j]。那如何得知failure[j]的值呢?观察模式串P,我们发现P[2] == P[0],因为T[3] == P[2],所以T[3] == P[0],相当于我们已经间接地比较过T[3] 和P[0]了,无需重复比较,接下来可以直接比较T[4]和P[1],所以failure[3] = 1。
再看一个简单的例子(例2):
string T = "aabcabaababc";
string P = "ababc";
当出现T[0] == P[0],但T[1] != P[1]时,要保证i不变,必须将j回溯至j = 0,然后比较T[1] 和P[0],所以failure[1] = 0。
同样的,当出现T[4…6] == P[0…2],但T[7] != P[3]时,要保证i不变,必须将j回溯至j = 0(为什么?好好想想!),然后比较T[7] 和P[0],所以failure[3] = 0。
那么,如果模式串P的第一个元素就不匹配,即T[i] != P[0]又该怎么办呢?j已经最小,没办法再往前回溯了,下一次比较必须使i自增1。这是一种特殊的情况,考虑到C语言中的数组下标从0开始,为了表示区别,我们设failure[0] = -1。很明显当failure[j] != -1时,在进行下一次比较之前,我们无需改变i的值;而当failure[j] == -1时,在进行下一次比较之前,必须先使i自增1。
我们继续分析例2,当出现T[1…2] == P[0…1],但T[3] != P[2]时,要保证i不变,必须将j回溯至j = 0,然后比较T[1] 和P[0]——看上去好像一切都顺理成章,但是请等等! 经比较T[1] == P[0],经观察P[2] == P[0],我们还有必要再去比较T[1] 和P[0]吗?当然不需要,我们应该直接比较T[2] 和P[0]才对!所以failure[2] = -1。
举了一大堆例子,苦于没有图象对照,想必各位看官已经看得头都大了!到底如何求模式串P的失效函数failure[j],可能很多人还是一头雾水(PS:我计划做一个教学视频,到时候图文声并茂,一定会帮助你理解的,记得随时关注博客,等待观看哦)。据考证,失效函数failure[j]是模式串P本身的属性,与目标串T无关,而且从不同的角度分析模式串P可以得到失效函数的不同表示方法。网络上此类文章可谓汗牛充栋,我的关于失效函数failure[j]的理解,与网友A_B_C_ABC 在其博文《KMP字符串模式匹配详解》(http://blog.csdn.net/A_B_C_ABC/archive/2005/11/25/536925.aspx)中所论述的“第一种表示方法”极为相似,如果你不想读我的文字,可以先去看A_B_C_ABC贴的图片,回过头再看我的文章,也许会明白我的意思——不会作图的下场啊!555
现在去A_B_C_ABC的博客看图!
。。。。。。
现在明白失效函数failure[j]的意义了吧?也应该知道如何求解failure[j]了吧?
总结一下吧:
先看失效函数的意义。
设在目标串T中查找模式串P,若T[i] != P[j],则将j回溯至失效函数值failure[j]处,那failure[j]可以取到哪些值呢?
① failure[0]= -1,表示T[i]和P[0]间接比较过了,且T[i] != P[0],接下来比较T[i+1]和P[0];
② failure[j] = 0,表示比较过程中产生了不相等,接下来比较T[i]和P[0];
③ failure[j] = k,其中0 < k < j,表示T[i]之前的k个字符与P中的前k个字符已经间接比较过了,且P[0…k-1] == P[j-k…j-1] == T[i-k…i-1],接下来比较T[i]和P[k]。
除了上述三种情况,failure[j]不可能取到其他值。
那么如何求解失效函数failure[j]的值呢?
从上述讨论可见,失效函数failure[j]的值仅取决于模式串P本身,与目标串T无关。
① failure[0]= -1:考虑到C语言中的数组下标从0开始,模式串P的首字符的失效函数值规定为-1;
② failure[j] = -1:若P[j] == P[0],且P[0…k-1] != P[j-k…j-1],或P[0…k] == P[j-k…j],其中0 < k < j。
如:P = "abcaabcab"。
因为P[3] == P[0],且P[0] != P[1],P[0…1] != P[1…3],则failure[3] = -1;
又因为P[7] == P[0],且P[0…3] == P[4…7],则failure[7] = -1;
③ failure[j] = k:若P[0…k-1] == P[j-k…j-1],且P[k] != P[j],其中0 < k < j。
如:P = "abcaabcab"。
因为P[0] == P[3],且P[1] != P[4],则failure[4] = 1;
又因为P[0…3] == P[4…7],且P[4] != P[8],则failure[8] = 4;
④ failure[j] = 0:除(1)(2)(3)的其他情况。
如:P = "abcaabcab"中,failure[1] = failure[2] = failure[5] = failure[6] = 0;
算法思路:
KMPIndex函数:
KMP算法在形式上和BF算法即为相似。不同之处仅在于:当匹配过程中产生“失配”时,目标串T指示标志i不变,模式串P指示标志j回溯至failure[j]所指示的位置,并且当j回溯至最左端(即failure[j] == -1)时,使j = 0,i自增1,。
GetFailure函数:
根据failure[j]的定义,我们先规定failure[0] = -1;然后遍历模式串P,依次计算各个元素的失配函数值。
设已有k == 0;或0 < k < j,且P[0…k-1] == P[j-k…j-1];我们比较P[k]和 P[j]:
若P[k] == P[j],则由failure[j]的定义可知failure[j] = failure[k],之后k和j均自增1,继续比较后继字符;
若P[k] != P[j],则failure[j] = k。很明显之后不能直接比较后继字符;而需要将k回溯,直至找到使得P[0…k] == P[j-k…j]的最大k值,才可以让k和j均自增1,继续比较后继字符。
那么如何将k快速回溯到适当的位置呢?
我们设h = failure[k],很明显有:P[0…h-1] == P[k-h…k-1] == P[j-h…j-1]。
若P[h] == P[j],那h就是满足条件的最大k值。
若P[h] != P[j],则再在串P[0…h]中寻找更小的failure[h]。如此递推,有可能还需要以同样的方式再缩小寻找范围,直到failure[h] == -1才算失败。
若failure[h] == -1,则相当于k已经回溯到了模式串P的最左端,可以让k和j均自增1,继续比较后继字符。
实现代码如下:
/*
函数名称:KMPIndex
函数功能:Knuth-Morris-Pratt算法,若目标串T中从下标pos起存在和模式串P相同的子串,则称匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。
输入参数:const string & T :目标串T
const string & P :模式串P
int pos :模式匹配起始位置
输出参数:无
返回值:int :匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。
*/
int KMPIndex(const string & T, const string & P, int pos)
{
int *failure = new int[P.size()];
Getfailure(P, failure); //计算模式串P的失配函数failure[]
int i = pos;
int j = 0;
while (i < T.size() && j < P.size())
{
if (T[i] == P[j]) //如果当前字符匹配,继续比较后继字符
{
++i;
++j;
}
else //否则保持i不变,将j回溯至failure[j],开始新的一轮比较
{
j = failure[j];
if (j == -1) //若j回溯至最左端,则使j = 0,i自增1
{
j = 0;
++i;
}
}
}
delete []failure;
if (j == P.size()) //匹配成功,返回第一个匹配子串首字符的下标
return i - j;
else
return -1;
}
/*
函数名称:Getfailure
函数功能:计算模式串P的失配函数,并存入数组failure[]
输入参数:const string & P :模式串P
int failure[]:模式串P的失配函数
输出参数:int failure[]:模式串P的失配函数
返回值:无
*/
void Getfailure(const string & P, int failure[])
{
failure[0] = -1; //模式串P的首字符的失配函数值规定为-1
for (int j=1, k=0; j
{
//若P[0…k-1] == P[j-k…j-1],且P[k] == P[j],则failure[j] = failure[k],并继续比较后继字符
if (P[k] == P[j])
{
failure[j] = failure[k];
}
else //否则保持j不变,将k回溯至failure[k]
{
failure[j] = k; //若P[0…k-1] == P[j-k…j-1],且P[k] != P[j],则failure[j] = k
//寻找使得P[0…k] == P[j-k…j]的最大k值,才可以继续比较后继字符
while (k >= 0 && P[k] != P[j])//将k回溯至P[k] == P[j]或最左端,以进行下一轮比较
k = failure[k];
}
}
//以下代码输出failure[i]
for (int i=0; i
cout << failure[i] << " ";
cout << endl;
}
我们刚才讨论的失效函数中有一个很巧妙的地方,那就是除了failure[0] = -1以外,模式串P中还有多处failure[j]可以等于-1,这就避免了重复比较T[i]和P[0],可以直接比较T[i+1]和P[0]。但是最初接触到这个算法的时候,我被这个“巧妙之处”足足折腾了半天——只因为效率上的一点点提高,却带来了理解上的巨大困难——真是得不偿失啊!
那么有没有更好理解的失效函数呢?当然有!
三.更好理解的失效函数
接下来我们看看另一些常见的失效函数表示方法。
在严蔚敏和吴伟民编著的《数据结构(C语言版)》(清华大学出版社)一书中,采用了一种比较简单的失效函数表示方法。它的定义与前面讲的失效函数差不多,只是把上述的四种情况简化为三种情况,将②和③合并为同一种类型,即若P[0…k-1] == P[j-k…j-1],其中0 < k < j,则failure[j] = k,而不论P[k] 是否等于 P[j]。这样模式串P中就只有failure[0] = -1了,失效函数表示方法得到了简化——当然效率稍微有所降低。
采用这种失效函数表示方法,在求解失效函数时,可以利用简单的递推,根据failure[j]来得到failure[j+1]。
原理如下:
先给出两个概念:若存在0 <= k < j,且使得P[0…k] == P[j-k…j]的最大整数k,我们称P[0…k]为串P[0…j]的前缀子串,P[j-k…j]为串P[0…j]的后缀子串。
从failure[j]的定义出发,计算failure[j]就是要在串P[0…j]中找出最长的相等的前缀子串P[0…k]和后缀子串P[j-k…j],这个查找的过程实际上仍是一个模式匹配的过程,只是目标和模式现在是同一个串P。
我们可以用递推的方法求failure[j]的值。
设已有failure[j] = k,则有0 < k < j,且P[0…k-1] == P[j-k…j-1]。接下来:
若P[k] == P[j],则由failure[j]的定义可知failure[j+1] = k + 1 = failure[j] + 1;
若P[k] != P[j],则可以在前缀子串P[0…k]中寻找使得P[0…h-1] == P[k-h…k-1]的h,这时存在两种情况:
① 找到h,则由failure[j]的定义可知failure[k] = h,故P[0…h-1] == P[k-h…k-1] == P[j-h…j-1],即在串P[0…j]中找到了长度为h的相等的前缀子串和后缀子串。
这时若P[h] == P[j],则由failure[j]的定义可知failure[j+1] = h + 1 = failure[k] + 1 = failure[failure[j]] + 1;
若P[h] != P[j],则再在串P[0…h]中寻找更小的failure[h]。如此递推,有可能还需要以同样的方式再缩小寻找范围,直到failure[h] == -1才算失败。
② 找不到h,这时failure[k] == -1,即k已经回溯到k = failure[k] = -1,所以failure[j+1] = k + 1 = 0。
依据以上分析,仿照KMP算法,可以得到计算failure[j]的算法,其对应的KMPIndex函数不变。
代码如下:
/*
函数名称:Getfailure
函数功能:用递推的方法计算模式串P的失配函数,并存入数组failure[]
输入参数:const string & P :模式串P
int failure[]:模式串P的失配函数
输出参数:int failure[]:模式串P的失配函数
返回值:无
*/
void Getfailure(const string & P, int failure[])
{
failure[0] = -1; //模式串P的首字符的失配函数值规定为-1
for (int j=1; j
{
int k = failure[j-1]; //利用failure[j-1]递推failure[j],k指向failure[j-1]
while (k >= 0 && P[k] != P[j-1])//将k回溯至P[k] == P[j-1]或k == -1,以进行下一轮比较
k = failure[k];
//现在可以确保P[0…k] == P[j-k-1…j-1],则failure[j] = k + 1(若k == -1,则failure[j] = 0)
failure[j] = k + 1;
}
//以下代码输出failure[i]
for (int i=0; i
cout << failure[i] << " ";
cout << endl;
}
前面定义的失效函数在某些情况下尚有缺陷。例如当模式串P = "aaaaaaaaaab"时,若T[i] != P[9],因为failure[9] = 8,所以下一步要将T[i] 和 P[8]比较;依此类推还要比较P[7],P[6],。。。,P[0]。实际上,因为它们都相等,所以当T[i] != P[9]时,可以直接比较T[i] 和 P[0]。也就是说,若按上述定义得到failure[j] = k,且P[j] == P[k]时,则当T[i] != P[j]时,不需要再比较T[i] 和 P[k],可以直接比较T[i] 和 P[failure[k]],即此时的failure[j]应该等于failure[k]。由此我们可以在原来计算失效函数算法的基础上加上一条语句,对失效函数值进行修正,以得到更高效的KMP算法。而且我们可以检验修正后的失效函数值与用第一种方法得到的失效函数值是一样的。
计算失效函数修正值的代码如下:
void Getfailure2(const string & P, int failure[])
{
failure[0] = -1; //模式串P的首字符的失效函数值规定为-1
for (int j=1; j
{
int k = failure[j-1]; //利用failure[j-1]递推failure[j],k指向failure[j-1]
while (k >= 0 && P[k] != P[j-1])//将k回溯至P[k] == P[j-1]或k == -1,以进行下一轮比较
k = failure[k];
//现在可以确保P[0…k] == P[j-k-1…j-1],则failure[j] = k + 1(若k == -1,则failure[j] = 0)
failure[j] = k + 1;
}
//对失效函数值进行修正,可以得到更高效的KMP算法
for (int j=1; j
{
if (P[j] == P[failure[j]])
failure[j] = failure[failure[j]];
}
//以下代码输出failure[i]
for (int i=0; i
cout << failure[i] << " ";
cout << endl;
}
四.另类的KMP算法
在殷人昆等人编著的《数据结构(用面向对象方法与C++描述)》(清华大学出版社)一书中,用到了另外一种表示失效函数的方法。该方法与前述两种方法的区别在于,当T[i] != P[j]时,模式串P的下标j不是回溯至failure[j],而是回溯至failure[j-1]+1,所以它的KMPIndex函数和GetFailure函数都与前面的有所不同。
该书对失效函数failure[j]的定义如下:
① failure[j] = k,其中0 <= k < j,且使得P[0…k] == P[j-k…j]的最大整数;
② failure[j] = -1,其他情况。
如:P = "abcaabcab"。
j = 0时,没有满足0 <= k < j的k存在,故failure[0] = -1;
j = 1时,可取k = 0,但P[0] != P[1],k不符合要求,故failure[1] = -1;
j = 2时,可取k = 0或1,但P[0] != P[2],且P[0…1] != P[1…2],k不符合要求,故failure[2] = -1;
j = 3时,可取k = 0,1或2:P[0] == P[3],P[0…1] != P[2…3],P[0…2] != P[1…3],故failure[3] = k = 0;
j = 4时,可取k = 0,1,2或3:P[0] == P[4],P[0…1] != P[3…4],P[0…2] != P[2…4],P[0…3] != P[1…4],故failure[4] = k = 0;
j = 5时,可取k = 0。。4:P[0] != P[5],P[0…1] == P[4…5],P[0…2] != P[3…5],P[0…3] != P[2…5],P[0…4] != P[1…5],故failure[5] = k = 1;
其他的以此类推可以得到failure[6] = 2;failure[7] = 3;failure[8] = 1。
设若在进行某一趟匹配比较时在模式串P的j位失配,即T[i] != P[j],如果j > 0,因为P[failure[j-1]] == P[j-1] == T[i-1],即已经间接地知道了P[0…failure[j-1]]是匹配的,那么我们只需将串P的下标j回溯至failure[j-1]+1,串T的下标i不回溯,仍指向上一趟失配的字符;如果j == 0,则让串T的下标i前进一位,串P的起始比较位置回溯到P[0],继续做匹配比较。
如何正确地计算出失效函数failure[j],是实现KMP算法的关键。
从failure[j]的定义出发,计算failure[j]就是要在串P[0…j]中找出最长的相等的前缀子串P[0…k]和后缀子串P[j-k…j],这个查找的过程实际上仍是一个模式匹配的过程,只是目标和模式现在是同一个串P。
我们可以用递推的方法求failure[j]的值(此方法与上文介绍的严蔚敏教授书中的方法极为相似,只有一处不同,请注意区别)。
设已有failure[j] = k,则有0 <= k < j,且P[0…k] == P[j-k…j]。
若P[k+1] == P[j+1],则由failure[j]的定义可知failure[j+1] = k + 1 = failure[j] + 1;
若P[k+1] != P[j+1],则可以在前缀子串P[0…k]中寻找使得P[0…h] == P[k-h…k]的h,这时存在两种情况:
① 找到h,则由failure[j]的定义可知failure[k] = h,故P[0…h] == P[k-h…k] == P[j-h…j],即在串P[0…j]中找到了长度为h + 1的相等的前缀子串和后缀子串。
这时若P[h+1] == P[j+1],则由failure[j]的定义可知failure[j+1] = h + 1 = failure[k] + 1 = failure[failure[j]] + 1;
若P[h+1] != P[j+1],则再在串P[0…h]中寻找更小的failure[h]。如此递推,有可能还需要以同样的方式再缩小寻找范围,直到failure[h] == -1才算失败。
② 找不到h,这时failure[k] == -1。
依据以上分析,仿照KMP算法,可以得到计算failure[j]的算法。
/*
函数名称:KMPIndex
函数功能:Knuth-Morris-Pratt算法,若目标串T中从下标pos起存在和模式串P相同的子串,
则称匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。
输入参数:const string & T :目标串T
const string & P :模式串P
int pos :模式匹配起始位置
输出参数:无
返回值:int :匹配成功,返回第一个匹配子串首字符的下标;否则返回-1。
*/
int KMPIndex(const string & T, const string & P, int pos)
{
int *failure = new int[P.size()];
Getfailure(P, failure); //计算模式串P的失配函数failure[]
int i = pos;
int j = 0;
while (i < T.size() && j < P.size())
{
if (T[i] == P[j]) //如果当前字符匹配,继续比较后继字符
{
++i;
++j;
}
else if (j == 0) //如果j == 0,则让目标串T的下标i前进一位
++i;
else //否则下一趟比较时模式串P的起始比较位置是P[failure[j-1]+1],目标串T的下标i不回溯
j = failure[j-1] + 1;
}
delete []failure;
if (j == P.size()) //匹配成功,返回第一个匹配子串首字符的下标
return i - j;
else
return -1;
}
/*
函数名称:Getfailure
函数功能:用递推的方法计算模式串P的失配函数,并存入数组failure[]
输入参数:const string & P :模式串P
int failure[]:模式串P的失配函数
输出参数:int failure[]:模式串P的失配函数
返回值:无
*/
void Getfailure(const string & P, int failure[])
{
failure[0] = -1; //模式串P的首字符的失配函数值规定为-1
for (int j=1; j
{
int k = failure[j-1]; //利用failure[j-1]递推failure[j],k指向failure[j-1]
while (k >= 0 && P[k+1] != P[j])//将k回溯至P[k+1] == P[j]或k == -1,以进行下一轮比较
k = failure[k];
if (P[k+1] == P[j]) //若P[0…k] == P[j-k…j-1],且P[k+1] == P[j],则failure[j] = k + 1
failure[j] = k + 1;
else //没有找到满足条件的k
failure[j] = -1;
}
//以下代码输出failure[i]
for (int i=0; i
cout << failure[i] << " ";
cout << endl;
}
这样我们就学习了三种失效函数的表示方法,虽然它们对应的KMP算法代码略有不同,但其本质是一样的,就是避免回溯目标串T的下标i,并使得模式串P的下标j回溯到正确位置。同样的,不管你用什么代码来实现求解失效函数的算法,其本质都是模式串内部的模式匹配,采用递推的方式,寻找最大的相同子串。
参考文献:
1.《数据结构(C语言版)》(清华大学出版社)严蔚敏,吴伟民编著
2.《数据结构(用面向对象方法与C++描述)》(清华大学出版社)殷人昆等人编著
3.《KMP字符串模式匹配详解》来自网友A_B_C_ABC的博客
(http://blog.csdn.net/A_B_C_ABC/archive/2005/11/25/536925.aspx)
4.《KMP算法中Next[]数组求法》作者:剑心通明
(http://www.bsdlover.cn/html/21/n-3021.html)