最近一段時間,我一直在看 KMP 字串模式匹配演算法的各種不同解釋。因為各種原因,沒有找到一種我覺得好的解釋。當我讀到“……的字首的字尾的字首”時,我會不停地拍自己的腦袋。
最後,花了大約30分鐘將《演算法導論》裡相同的部分反反覆覆讀了以後,我決定坐下來做一些例子和圖解。現在,我已經搞清楚了這個演算法並能對它解釋。對於那些和我有一樣想法的人,下面是我自己的理解。一方面,我不打算解釋為什麼它比樸素的字串匹配效率更高;這些在很多地方都已經解釋得非常好了。我要說明的是,它究竟是如何工作的。
部分匹配表
毫無疑問,KMP演算法的精髓是部分匹配表。我理解KMP演算法時,最大的障礙就在於是否充分明白部分匹配表裡的值所代表的意義。下面我會盡可能簡單地來解釋這些。
下面這個是“abababca”這個模板的部分匹配表:
char: | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
如果我有一個8個字元的模板(這裡我們就用“abababca”來舉例子),我的部分匹配表將會有8格。如果此時此刻我正匹配模板的第8格即最後一格,那意味著我匹配了整個模板(“abababca”);如果我正匹配模板的第7格,則意味著當前僅匹配了整個模板的前7位(“abababc”),此時第8位(“a”)是無關的,不用去管它;如果我此時此刻正匹配模板的第6格,那意味著……看到這裡你應該已經明白我的意思了。目前我還沒有提到部分匹配表每格資料的含義,在這裡僅僅是交代了大概。
現在,為了說明剛剛提到的每格資料的含義,我們首先要明白什麼是最優字首什麼是最優字尾。
最優字首:一個字串中,去除一個或多個尾部的字元所得的新字串就是最優字首。例如 “S”、 “Sn”、 “Sna”、 “Snap”都是“Snape”的最優字首。
最優字尾:一個字串中,去除一個或多個首部的字元所得的新字串就是最優字尾。例如“agrid”、 “grid”、“rid”、 “id”、“d”都是 “Hagrid”的最優字尾。
有了兩個概念,我現在可以用一句話來概括部分匹配表裡每列資料的含義了:
模板(子模板)中,既是最優字首也是最優字尾的最長字串的長度。
下面我舉例說明一下這句話。我們來看部分匹配表的第3格資料,如果你還記得我在前面提到的,這意味著我們目前僅僅關心前3個字母(“aba”)。在“aba”這個子模板中,有兩個最優字首(“a”和“ab”)和兩個最優字尾(“a”和“ba”)。其中,最優字首“ab”並不是最優字尾。因此,最優字首與最優字尾中,相同的只有“a”。那麼,此時此刻既是最優字首也是最優字尾的最長字串的長度就是1了。
我們再來試試第4格,我們應該是關注於前4個字母(“abab”)。可以看出,有3個最優字首(“a”、“ab”、 “aba”)和3個最優字尾(“b”、“ab”、“bab”)。這一次 “ab” 既是最優字首也是最優字尾,並且長度為2,因此,部分匹配表的第4格值為2。
這是很有趣的例子,我們再看看第5格的情況,也就是考慮“ababa”。我們有4個最優字首(“a”、 “ab”、“aba”,和“abab”)和4個最優字尾(“a”、 “ba”、“aba”,和“baba”)。現在,有兩個匹配“a”和“aba” 既是最優字首也是最優字尾,而“aba”比“a”要長,所以部分匹配表的第5格值為3。
跳過中間的直接來看第7格,此時只考慮字母“abababc”。即使不一一列舉出所有的最優字首與最優字尾也不難看出,這兩個集合之間不會有任何的交集。因為,所有最優後綴都以“c”結尾,但沒有任何最優字首是以“c”結尾的,所以沒有相匹配的,因此第7格值為0。
最後,讓我們看看第8格,也就是考慮整個模板(abababca)。它的最優字首與最有字尾都以“a”開頭以“a”結尾,所以第8列的值至少是1。然而1就是最終結果了,所有長度大於等於2的最優字尾都包含“c”,但只有“abababc”這一個最優字首包含“c”,這個7位的最優字尾“bababca”並不匹配,所以第8列最終賦值為1。
如何使用部分匹配表
當我們找到了部分匹配的字串時,可以用部分匹配表裡的值來跳過前面一些字元(而不是重複進行沒有必要的比較)。具體是這樣工作的:
如果已經匹配到的部分字串的長度為partial_match_length且 table[partial_match_length] > 1,那麼我們可以跳過partial_match_length- table[partial_match_length – 1]個字元。
比如,我們拿“abababca”來這個模板來匹配文字“ bacbababaabcbab”的話,我們的部分匹配表應該是這樣的:
1 2 3 |
char: | a | b | a | b | a | b | c | a | index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | |
第一次匹配的時候是在這裡
1 2 3 |
bacbababaabcbab | abababca |
partial_match_length值為1,對應的table[partial_match_length – 1] (即table[0])值為0。所以,這種情況下我們不能跳過任何字元。下一次的匹配是這裡:
1 2 3 |
bacbababaabcbab ||||| abababca |
partial_match_length值為5,對應的 table[partial_match_length – 1] (即 table[4])值為3。這意味著我們可以跳過 partial_match_length- table[partial_match_length – 1] (即 5 – table[4] 或5 – 3 亦即 2)個字元:
1 2 3 4 5 |
// x 表示一個跳過 bacbababaabcbab xx||| abababca |
partial_match_length值為3,對應的 table[partial_match_length – 1] (即 table[2])值為1,這意味著我們可以跳過 partial_match_length- table[partial_match_length – 1] (即 3- table[2] 或3 – 1亦即 2)個字元:
1 2 3 4 5 |
// x 表示一個跳過 bacbababaabcbab xx| abababca |
現在,模板長度大於所剩餘的目標字串長度,所以我們知道不會再有匹配了。
結語
那麼你應該搞明白了吧。就像我一開始說的,這篇文章沒有關於KMP多餘的解釋或者或枯燥的證明;而是我自己的理解,以及我發現的容易讓人感到迷惑部分的詳盡解釋。如果你有任何疑問或者發現我這篇文章哪裡寫錯了,請給我留言;也許我們都會有所收穫。