KMP演算法的主過程比較容易理解,正確性也比較容易證明,演算法導論中給出的證明也完備。但是對於首碼Function Compute過程的正確性證明,演算法導論給出的過於晦澀,尤其是大量地使用數學符號,導致數學符號的晦澀性超過了證明本身的晦澀性。想了一段時間,現在想給出一個關於首碼函數的較易理解的自然語言證明。
其實KMP演算法的首碼函數寥寥數行,本不該用過多的語言和符號去表述,演算法導論的證明方法有些買櫝還珠的意味。為了方便起見,先對證明中必須用到的一些符號做一個定義:
pi[1...n] 首碼數組,演算法所求
P 模式,需要匹配的字串
P(k) P的k長度首碼
KMP演算法的首碼計算函數虛擬碼如下:
pi[1] = 0for i = 2 to length(P) temp = pi[i - 1] while temp > 0 and P[temp] != P[i - 1] temp = pi[temp] end while pi[i] = temp + 1end for
首先,證明在已知pi[1]~pi[i-1]的情況下,演算法能正確求出pi[i]。
pi[i]的定義為,P(i - 1)的尾碼中,是P(i - 1)的首碼且最長的。
如果已知P(i - 2)的所有尾碼,那麼只需要判斷1其某個尾碼是否也是首碼2作為首碼的下一個字元是否等於P[i - 1],如果滿足條件,且這個尾碼最長,那麼加上1即是pi[i]。
接下來證明,迴圈
while temp > 0 temp = pi[temp]
遍曆出的序列,是所有滿足條件1的P(i - 2)的尾碼按長度從大到小排列。因為對於P(i - 2)來說,其符合條件1且最長的尾碼為P(pi[i - 2]),而P(pi[pi[i - 2]])作為P(pi[i - 2])的尾碼同時也是P(i - 2)的尾碼(尾碼操作符滿足傳遞性),那麼一路遍曆過來得到的全部序列就是滿足條件1的P(i - 2)的尾碼且長度從大到小排列。現在假設遍曆出的尾碼pf1, pf2, pf3 ... pfn(這裡,為了易於理解長度按從小到大排列)不包括所有,即,某個pf'可以插入到序列中pfi和pfi+1兩個元素的中間,那麼pfi+1的最長滿足條件的尾碼就不是pfi而是pf'了,顯然length(pf')大於length(pfi),這與pi函數的定義衝突。所以迴圈得出的尾碼pf1, pf2, pf3 ... pfn是所有的尾碼。
那麼只要在這個序列中從後往前遍曆(實際上虛擬碼中也是這麼做的)並判斷其作為首碼的下一個字元等於P[i - 1](虛擬碼中while判斷語句的第二個條件做了這件事情),即能證的演算法的一次迴圈等夠得到P(i - 1)的滿足同時為P(i - 1)首碼的最長尾碼。
由於起始條件為pi[i] = 0,滿足條件,所以整個演算法正確性得證。
參考資料:
[1] 演算法導論 32.4
[2] Jeffrey J. McConnell, Analysis of Algorithm - An Active Learning Approach, 5.1.2
另外關於KMP和普通暴力法的效率比較,[2]中說實際上KMP僅僅比暴力法好一點,而[1]中也給出了暴力法的執行複雜度的期望,為線性。貌似暴力法是可以忍受的。