用T表示目标串、P表示模式串;KMP要在T中寻找P。
在比较T[i]与P[j]匹配时若发生失配:
KMP算法不会回退i,而是会将模式串右移,从T[i]与P[next[j]]再继续尝试:
注意,next[j]的值要尽量大,以免错失潜在的匹配(注意next[j]的值与右移量呈反比)。例如,若T='00001000'、P='0001',那么算法运行时会发现T[3]≠P[3]。此时,唯一的正确选择是将P右移一格;如果右移两或三格,都无法得出正确结果(见邓俊辉老师的《数据结构(C++语言版)》中KMP算法的相关章节)。实际上,next[j]是被设为了所能允许的最大值。
相比于蛮力算法,这样做可以避免许多不必要的比较。但问题是,怎么确定P[next[j]]呢?
本文以下用区间表示法来表示一个字串。例如,半开区间[a,b)表示从a到b而不包括b的连贯整数,因此P[a,b)表示的就是由P[a]、P[a+1]、P[a+2]……P[b-1]按顺序组成的子串。闭区间[a,b]不过是给[a,b)加入了b,因此P[a,b]与P[a,b)的不同只在于前者多出来一个P[b]。
但是将右移后的模式串P[0, next[j]]与右移之前的P[0, j]放在一起比一比,我们会发现:P[0, next[j])是P[0, j)的一个真后缀,如下图:
本文以下用P[0, next[j])∝P[0, j)来表示这种“真后缀”关系。next表满足P[0, next[j])∝P[0, j)这种性质是理解它构建方式的一个关键,稍后会用到它;暂且将其称为真后缀性质。现在我们要用另一种方式来审视这个问题。
假如已经确定了next[0,j]={next[0], next[1], … , next[j]}(回忆我前面提到的区间表示法),该如何确定next[j+1]呢?为此,我们先将目标串放在一边,而集中精力于模式串本身。既然已假定next[j]已知,那么由真后缀性质可知P[0, next[j])∝P[0, j),现在的问题就是,P[next[j]]与P[j]相等吗?这个局面如下:
假如它俩相等,那么就有P[0, next[j]+1)∝P[0, j+1)。由真后缀性质可知,next[j+1]就等于next[j]+1。因此,P[next[j]]=P[j]的话,就可以轻松确定出next[j+1]。
若不等于呢?
不等于的话,是因为next[j]的值过大(为什么?答案见文末,但推荐你自主思考),以至于next[j]+1不能作为next[j+1]的值。显然,需要一个比next[j]更小的值。我们可以从next[j]-1开始一个一个往后试,然而这种蛮力破解有悖KMP算法的本色。
让我们像“假如已经确定了next[0,j]={next[0], next[1], … , next[j]}”那样,假定我们已经确定了next[j+1]。由真后缀关系可知,P[0, next[j+1])∝P[0, j+1),这意味着P[j]=P[next[j+1]-1],如下图:
但是从图中我们可以看到一个更重要的事实,那就是P[0, next[j+1]-1)∝P[0, next[j])。这意味着完全没必要从next[j]-1开始一个一个往后试,而只需要从P[0, next[j])的一个最长真后缀开始(从最长的后缀开始,逐渐向更短的试探,就可以避免遗漏)。如果我们将next[j]作为下标,去查next表,得到next[next[j]],那么由真后缀性质知,P[0, next[next[j]])∝P[0, next[j]),而且P[0, next[next[j]])是满足这一关系的最长后缀(回忆开头所说“next[j]的值要尽量大”那里;另注意a∝b只是说a是b的后缀,而没有对a是否最长做出断言)。
既然查next表的效果是令模式串右移,那么我们就知道next[j]<j,又因为在讨论之初就已假定已经确定了next[0,j]={next[0], next[1], … , next[j]},那么我们知道next[next[j]]也是早已被一起假定为已知的。因此上述递归地查表完全是可行的。
现在的局面如下图所示:
若P[next[next[j]]]等于P[j],那么令next[j+1]为next[next[j]]+1后,就有P[0, next[j+1])∝P[0, j+1)。这样,就再次成功确定了next[j+1]的值。
可是如果还P[next[next[j]]]不等于P[j]呢?那只需要再试探next[next[next[j]]]是否与P[j]相等就行了,道理是一样的。由于每一次查表都会得到一个更小的值,因此这一递归查表的过程必然终止,而不会无穷进行下去。例如,若P[0]≠P[j],那么会去查next[0];它的值是-1,即那个标记特殊情况的值;这时构建过程就该令next[j+1]=next[0]+1=0了,同时将j加一,准备计算next表中下一项的值。
“假如已经确定了next[0,j]={next[0], next[1], … , next[j]}”的做法是一种数学技巧,可以称之为“白日做梦”或“打如意算盘”。我最初是在波利亚的《数学的发现》的§1.4读到了它,后来在求解数学证明的过程中,发现这一技巧果然屡试不爽。
为什么P[next[j]]≠P[j]时,需要一个比next[j]更小的j'来得出next[j+1]=j'+1?
首先,显然
next[j]不能毫无变化,因此它就剩下了减少或增大两个选择。假如说有一个j''>next[j],且next[j+1]=j''+1,那这就意味着P[0, j''+1)=P[0, next[j+1])∝P[0, j+1)。把这两个字符串的最后一个字符拿掉,就得P[0, j'')∝P[0, j)。这样一来,P[0, j'')就是一个比P[0, next[j])更长的P[0, j)的真后缀,与开头提到的“实际上,next[j]是被设为了所能允许的最大值”这一点相悖。
4140



被折叠的 条评论
为什么被折叠?



