kmp演算法是非常經典的字串匹配演算法,而且有可能是最經典的乙個。同時它也是非常典型的一種優化演算法,它把原本暴力法o(mn)的最壞複雜度降低到了o(m+n)(雖然實際上暴力法的執行複雜度期望依然是線性的),其思想非常具有典型性和可借鑑性,值得好好學習。
1 基本思想
kmp演算法的基本思想是,借助乙個預先計算好的陣列pi,在匹配了一定數量的模式的情況下,遇到不匹配的字元時,不像暴力法那樣將待匹配的文字下標從上一次匹配的地方向後移動乙個位置,並將已匹配的模式個數清零,而是利用已經匹配部分的資訊,迅速跳過那些不可能匹配成功的文字開頭,減少了暴力法中一些不必要的盲目搜尋。
比如,對於模式
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
a b a b a b c a b c a b c d b c d b c d
如果從文字中的某乙個字元開始,匹配到6號字元c時,出現了不匹配,此時已經匹配了6個字元。如果再從文字中下乙個字元b開始,從模式的第0號字元開始繼續嘗試,就是暴力法的思想。而kmp演算法比暴力法先進的地方就在於,它利用模式已匹配部分的資訊,跳過那些已遍歷的文字中不可能匹配的開頭,直接進入有可能會產生匹配的文字開頭,並更新相應的已匹配字元長度。比如,匹配到6號字元時,出現不匹配,此時已匹配的部分為ababab。kmp演算法會跳到原文本中開頭的後面第二個字元開始匹配,因為abab也匹配了模式開頭的一部分。同時演算法將已匹配長度更新為4,而文字的待匹配字元保持不變不後退。
為了方便起見,先對必須用到的一些符號做乙個定義:
pi[1..n] 字首陣列
p 模式,需要匹配的字串
p(k) p的k長度字首
理解kmp演算法的關鍵在於如何理解字首函式pi。pi中的某個元素pi[k]是
長度為k的p的字首p(k)的同時為它自己字首的最長真字尾的長度。這個說法有點拗口,細說起來,
k是p的乙個字首的長度,k定義了乙個p的字首p(k)。pi[k
]是p(k)乙個真字尾的長度值,它定義了p
(k)的乙個真字尾。真字尾的意思是不等於它自己的字尾(即長度小於它本身)。這個p
(k)的真字尾必須滿足以下條件:
(1) 它必須是p
(k)的字首(這意味它也是p的字首)
(2) 它必須是滿足條件1的所有真字尾中最長的
p(k)代表在演算法出現不匹配時已匹配的那部分。由於每次匹配從模式的頭開始,故從匹配的開頭到最後乙個成功匹配的位置所組成的子串為模式p的字首。求取真字尾的意思是說,從當前匹配過程的開頭後面的某乙個位置開始,到最後乙個成功匹配的字元所組成的字串,是當前已匹配模式的乙個真字尾(因為當出現不匹配的時候,下一次匹配過程的開頭必須從當前的開頭向後退)。同時為字首的意思是說,所要跳到的開頭和最後乙個成功匹配的字元所組成的子串(即這個真字尾),必須是當前已匹配模式的字首才有意義,因為如果它不是模式的字首,那麼就說明這段子串無論後面的字元是什麼都不可能匹配模式。最長的意思是說,不要跳過那些滿足條件1的字尾。
這裡給出乙個kmp演算法實現:
kmp.h
#ifndef _kmp_template_#define _kmp_template_#include
#include
static
const
int null_char = 0
;using
std::vector;
//陣列p下標從0開始
//pi[k]代表長度為k的p的字首p[0..k-1]的最長同時為它自己字首的真字尾的長度
//在這裡,將pi[0]定義為無意義(-1),方便指示迴圈終止條件
//pi[1]為0,因為p[0..0]的真字尾為空串
templatevoid kmp_pibuild(c *p, int *pi, int
p_len)
}}template
void kmp_stringmatch(c *text, c *p, int p_len, vector &match)
else
if (m ==p_len)
}}#endif
//_kmp_template_
main.cpp
#include #include#include
#include
#include
"kmp.h
"using
namespace
std;
intmain()
char text[5000], p[1000
]; fin.getline(text,
5000
); fin.getline(p,
1000
); vector
match;
int len =strlen(p);
kmp_stringmatch(text, p, len, match);
if (match.size() == 0
) else
}
2 字首函式計算的正確性
對於字首函式計算過程的正確性證明,演算法導論給出的過程由於大量地使用數學符號而顯得過於晦澀。想了一段時間,現在給出乙個關於字首函式的較易理解的證明。
首先證明,迴圈
for (int k = pi[i - 1]; k != -1; k =pi[k])遍歷出的序列,是所有同時是其字首的p(i-1)的真字尾按長度從大到小排列。因為p(pi[pi[i-1]])作為p(pi[i-1])的字尾同時也是p(i-1)的字尾(字尾操作符滿足傳遞性),那麼一路遍歷過來得到的全部序列就是滿足條件的p(i-1)的字尾且長度從大到小排列。用反證法,假設遍歷出的字尾pf1, pf2, pf3 ... pfn(這裡,為了易於理解長度按從小到大排列)不包括所有。那麼,某個pf'可以插入到序列中pfi和pfi+1兩個元素的中間。由此可推出pfi+1的最長滿足條件的真字尾就不是pfi而是pf'了,顯然length(pf')大於length(pfi)。這與pi函式的定義衝突。所以迴圈得出的字尾pf
1, pf
2, pf
3... pf
n是所有滿足條件的真字尾。
接下來證明,迴圈
for (int k = pi[i - 1]; k != -1; k =pi[k])能在已知pi[1]..pi[i-1]的情況下,正確求出pi[i]。if (p[k] == p[i - 1
])
上一段迴圈做的事情是:對p(i-1)的滿足條件的真字尾,按長度從大到小遍歷,並判斷每個真字尾作為p的字首其下乙個字元與p[i-1]是否相等,並將第乙個滿足條件的這樣的p(i-1)的真字尾prf1與p[i-1]相連視為pi[i]對應的真字尾,其長度為prf
1的長度加1(這段雖然說起來拗口但是找個例子列一下其實很清晰)。
要證明其正確性,也可以使用反證法,假設某個p(i)滿足條件的最長真字尾沒有被列舉,從而推出矛盾,即可得證。需要說明的是,在開頭,將pi[0]設為乙個無意義的值-1,是為了k=0作為迴圈的最後乙個元素。k=0,代表當前字首為空串,說明當前列舉的p(i)的真字尾為由p[i-1]這個元素單獨組成的串。這樣能保證列舉出所有情況。
由於起始條件為pi[1] = 0,初始條件成立,所以字首函式計算的正確性得證。
3 匹配過程的正確性
其實第1節介紹匹配的基本思想時已經基本上證明了演算法的正確性。證明其正確性的關鍵在於,每次出現不匹配或者匹配成功時,跳過的那些後面的開頭是否真的是應該被忽略的。這裡使用反證法,假設某個跳過的開頭最終匹配了模式,那從這個開頭到當前最後乙個成功匹配的位置所組成的真字尾必定也是乙個字首。由第2節的證明可知,回退的過程窮舉出了所有的符合條件的真字尾,而假設又推出符合條件的真字尾乙個真字尾未被列舉,這與事實矛盾,因此假設不成立。即,演算法實際上窮舉出了所有可能匹配的開頭,正確性得證。
關於KMP演算法
複習的時候隨便寫寫的,用git太麻煩,就用csdn儲存下。kmp演算法寫起來很短,但是精髓是它的思想不太好理解,主串不用回溯,是因為模式串自己與自己比較匹配可以得出相應的next值,然後模式串向右滑動,例如,模式串abcd xxabci x在第i位失配,只需要模式串滑到d與主串繼續比較。哎呀表達得不...
關於KMP演算法的筆記
以前一直沒有搞懂kmp演算法,突然心血來潮,特意去網上搜尋資料下定決心弄懂,終於在一篇文章的幫助下,對kmp演算法有了自己的理解。這篇文章的出處是 the knuth morris pratt algorithm in my own words 一 什麼是kmp演算法?kmp演算法是一種改進的字串匹...
關於KMP演算法的理解
上次因為haipz組織的比賽中有道題必須用到kmp演算法,因此賽後便了解了下它,在仔細拜讀了孤 影神牛的文章之後有種茅塞頓開的感覺,再次orz。附上鏈結 對於整個kmp演算法,最精髓的部分便是關於next陣列的生成。一開始ruijia liu的書上貼上的 感覺完全不能理解,但是看神犇的分析覺得似乎明...