KMP算法,是由Knuth,Morris,Pratt共同提出的模式匹配算法,其對於任何模式和目標序列,均可以在線性時間內完成匹配查找,而不會發生退化,是一個很是優秀的模式匹配算法。可是相較於其餘模式匹配算法,該算法晦澀難懂,第一次接觸該算法的讀者每每會看得一頭霧水,主要緣由是KMP算法在構造跳轉表next過程當中進行了多個層面的優化和抽象,使得KMP算法進行模式匹配的原理顯得不那麼直白。本文但願可以深刻KMP算法,將該算法的各個細節完全講透,掃除讀者對該算法的困擾。算法
KMP算法對於樸素匹配算法的改進是引入了一個跳轉表next[]。以模式字符串abcabcacab爲例,其跳轉表爲:數組
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
pattern[j] | a | b | c | a | b | c | a | c | a | b |
next[j] | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 5 | 0 | 1 |
跳轉表的用途是,當目標串target中的某個子部target[m...m+(i-1)]與pattern串的前i個字符pattern[1...i]相匹配時,若是target[m+i]與pattern[i+1]匹配失敗,程序不會像樸素匹配算法那樣,將pattern[1]與target[m+1]對其,而後由target[m+1]向後逐一進行匹配,而是會將模式串向後移動i+1 - next[i+1]個字符,使得pattern[next[i+1]]與target[m+i]對齊,而後再由target[m+i]向後與依次執行匹配。數據結構
舉例說明,以下是使用上例的模式串對目標串執行匹配的步驟性能
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
b | a | b | c | b | a | b | c | a | b | c | a | a | b | c | a | b | c | a | b | c | a | c | a | b | c |
a | b | c | a | b | c | a | c | a | b | ||||||||||||||||
a | b | c | a | b | c | a | c | a | b | ||||||||||||||||
a | b | c | a | b | c | a | c | a | b | ||||||||||||||||
a | b | c | a | b | c | a | c | a | b | ||||||||||||||||
a | b | c | a | b | c | a | c | a | b | ||||||||||||||||
a | b | c | a | b | c | a | c | a | b |
經過模式串的5次移動,完成了對目標串的模式匹配。這裏以匹配的第3步爲例,此時pattern串的第1個字母與target[6]對齊,從6向後依次匹配目標串,到target[13]時發現target[13]='a',而pattern[8]='c',匹配失敗,此時next[8]=5,因此將模式串向後移動8-next[8] = 3個字符,將pattern[5]與target[13]對齊,而後由target[13]依次向後執行匹配操做。在整個匹配過程當中,不管模式串如何向後滑動,目標串的輸入字符都在不會回溯,直到找到模式串,或者遍歷整個目標串都沒有發現匹配模式爲止。學習
next跳轉表,在進行模式匹配,實現模式串向後移動的過程當中,發揮了重要做用。這個表看似神奇,實際從原理上講並不複雜,對於模式串而言,其前綴字符串,有可能也是模式串中的非前綴子串,這個問題我稱之爲前綴包含問題。以模式串abcabcacab爲例,其前綴4 abca,正好也是模式串的一個子串abc(abca)cab,因此當目標串與模式串執行匹配的過程當中,若是直到第8個字符才匹配失敗,同時也意味着目標串當前字符以前的4個字符,與模式串的前4個字符是相同的,因此當模式串向後移動的時候,能夠直接將模式串的第5個字符與當前字符對齊,執行比較,這樣就實現了模式串一次性向前跳躍多個字符。因此next表的關鍵就是解決模式串的前綴包含。固然爲了保證程序的正確性,對於next表的值,還有一些限制條件,後面會逐一說明。優化
如何以較小的代價計算KMP算法中所用到的跳轉表next,是算法的核心問題。這裏咱們引入一個概念f(j),其含義是,對於模式串的第j個字符pattern[j],f(j)是全部知足使pattern[1...k-1] = pattern[j-(k-1)...j - 1](k < j)成立的k的最大值。仍是以模式串abcabcacab爲例,當處理到pattern[8] = 'c'時,咱們想找到'c'前面的k-1個字符,使得pattern[1...k-1] = pattern[8-(k-1)...7],這裏咱們可使用一個笨法,讓k-1從1到6遞增,而後依次比較,直到找到最大值的k爲止,比較過程以下ui
k-1 | 前綴 | 關係 | 子串 |
1 | a | == | a |
2 | ab | != | ca |
3 | abc | != | bca |
4 | abca | == | abca |
5 | abcab | != | cabca |
6 | abcabc | != | bcabca |
由於要取最大的k,因此k-1=1不是咱們要找的結果,最後求出k的最大值爲4+1=5。可是這樣的方法比較低效,並且沒有充分利用到以前的計算結果。在咱們處理pattern[8] = 'c'以前,pattern[7] = 'a'的最大前綴包含問題已經解決,f(7) = 4,也就是說,pattern[4...6] = pattern[1...3],此時咱們能夠比較pattern[7]與pattern[4],若是pattern[4]=pattern[7],對於pattern[8]而言,說明pattern[1...4]=pattern[4...7],此時,f(8) = f(7) + 1 = 5。再以pattern[9]爲例,f(8) = 5,pattern[1...4]=pattern[4...7],可是pattern[8] != pattern[5],因此pattern[1...5]!=pattern[4...8],此時沒法利用f(8)的值直接計算出f(9)。spa
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
pattern[j] | a | b | c | a | b | c | a | c | a | b |
next[j] | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 5 | 0 | 1 |
f(j) | 0 | 1 | 1 | 1 | 2 | 3 | 4 | 5 | 1 | 2 |
咱們可能考慮仍是使用以前的笨方法來求出f(9),可是且慢,利用以前的結果,咱們還能夠獲得更多的信息。仍是以pattern[8]爲例。f(8) = 5,pattern[1...4]=pattern[4...7],此時咱們須要關注pattern[8],若是pattern[8] != pattern[5],那麼在匹配算法若是匹配到pattern[8]才失敗,此時就能夠將輸入字符target[n]與pattern[f(8)] = pattern[5]對齊,再向後依次執行匹配,因此此時的next[8] = f(8)(此平移的正確性,後面會做出說明)。而若是pattern[8] = pattern[5],那麼pattern[1...5]=pattern[4...8],若是target[n]與pattern[8]匹配失敗,那麼同時也意味着target[n-5...n]!=pattern[4...8],那麼將target[n]與pattern[5]對齊,target[n-5...n]也必然不等於pattern[1...5],此時咱們須要關注f(5) = 2,這意味着pattern[1] = pattern[4],由於pattern[1...4]=pattern[4...7],因此pattern[4]=pattern[7]=pattern[1],此時咱們再來比較pattern[8]與pattern[2],若是pattern[8] != pattern[2],就能夠將target[n]與pattern[2],而後比較兩者是否相等,此時next[8] = next[5] = f(2)。若是pattern[8] = pattern[2],那麼還須要考察pattern[f(2)],直到回溯到模式串頭部爲止。下面給出根據f(j)值求next[j]的遞推公式:.net
若是 pattern[j] != pattern[f(j)],next[j] = f(j);blog
若是 pattern[j] = pattern[f(j)],next[j] = next[f(j)];
當要求f(9)時,f(8)和next[8]已經能夠獲得,此時咱們能夠考察pattern[next[8]],根據前面對於next值的計算方式,咱們知道pattern[8] != pattern[next[8]]。咱們的目的是要找到pattern[9]的包含前綴,而pattern[8] != pattern[5],pattern[1...5]!=pattern[4...8]。咱們繼續考察pattern[next[5]]。若是pattern[8] = pattern[next[5]],假設next[5] = 3,說明pattern[1...2] = pattern[6...7],且pattern[3] = pattern[8],此時對於pattern[9]而言,就有pattern[1...3]=pattern[6...8],咱們就找到了f(9) = 4。這裏咱們考察的是pattern[next[j]],而不是pattern[f(j)],這是由於對於next[]而言,pattern[j] != pattern[next[j]],而對於f()而言,pattern[j]與pattern[f(j)]不必定不相等,而咱們的目的就是要在pattern[j] != pattern[f(j)]的狀況下,解決f(j+1)的問題,因此使用next[j]向前回溯,是正確的。
如今,咱們來總結一下next[j]和f(j)的關係,next[j]是全部知足pattern[1...k - 1] = pattern[(j - (k - 1))...j -1](k < j),且pattern[k] != pattern[j]的k中,k的最大值。而f(j)是知足pattern[1...k - 1] = pattern[(j - (k - 1))...j -1](k < j)的k中,k的最大值。仍是以上例的模式來講,對於第7個元素,其f(j) = 4, 說明pattern[7]的前3個字符與模式的前綴3相同,可是因爲pattern[7] = pattern[4], 因此next[7] != 4。
經過以上這些,讀者可能會有疑問,爲何不用f(j)直接做爲KMP算法的跳轉表呢?實際從程序正確性的角度講是能夠的,可是使用next[j]做爲跳轉表更加高效。仍是以上面的模式爲例,當target[n]與pattern[7]發生匹配失敗時,根據f(j),target[n]要繼續與pattern[4]進行比較。可是在計算f(8)的時候,咱們會得出pattern[7] = pattern[4],因此target[n]與pattern[4]的比較也必然失敗,因此target[n]與pattern[4]的比較是多餘的,咱們須要target[n]與更小的pattern進行比較。固然使用f(j)做爲跳轉表也能得到不錯的性能,可是KMP三人將問題作到了極致。
咱們能夠利用f(j)做爲媒介,來遞推模式的跳轉表next。算法以下:
程序中,9到27行的循環須要特別說明一下,咱們發如今循環開始以後,就沒有再爲t賦新值,也就是說,對於計算next[j]時的t值,在計算next[j+1]時,還會用得着。實際這時的t的就等於f(j)。仍是以上例的目標串爲例,當j等於1,咱們能夠得出t = f(2) = 1。使用概括法,當計算完next[j]後,咱們假設此時t=f(j),此時第11~14行的循環就是要找到知足pattern[k] = pattern[j]的最大k值。若是這樣的k存在,對於pattern[j+1]而言,其前k個元素,與模式的前綴k相同。此時的t+1就是f(j+1)。這時咱們就要判斷pattern[j+1]和pattern[t](t = t+1)的關係,而後求出next[j+1]。這裏須要初始條件next[1] = 0。
利用跳轉表實現字符串匹配的算法以下:
該算法在原有基礎上進行了擴展,在原模式串末尾加入了一個「空字符」,「空字符」不等於任何的可輸入字符,當目標串匹配至「空字符」時,說明已經在目標字符串中發現了模式,將模式串在目標串中的位置,加入matchs[]數組中,同時斷定爲匹配失敗,並根據「空字符」的next值,跳轉到適當位置,這樣算法就能夠識別出字符串中全部的匹配子串。
最後,對KMP算法的正確性作一簡要說明,仍是以上文的模式串pattern和目標串target爲例,假設已經匹配到第3部的位置,且在target[13]處發現匹配失敗,咱們如何決定模式串的滑動步數,來保證既要忽略沒必要要的多餘比較,又不漏過可能的匹配呢?
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | |
target | b | a | b | c | b | a | b | c | a | b | c | a | a | b | c | a | b | c | a | b | c | a | c | a | b | c |
pattern | a | b | c | a | b | c | a | c | a | b |
對於例子中的狀況,顯然向後移動多於3個字符有可能會漏過target[9...18]這樣的的可能匹配。可是爲何向後移動1個或者2個字符是沒必要要的多餘比較呢?當target[13]與pattern[8]匹配失敗時,同時也意味着,target[6...12] = pattern[1...7],而next[8]=5,意味着,pattern[1...4] = pattern[4...7],pattern[1...5] != pattern[3...7],pattern[1...6] != pattern[2...7]。若是咱們將模式串後移1個字符,使pattern[7]與target[13]對齊,此時target[7...12]至關於pattern[2...7],且target[7...12]與pattern[1..6]逐個對應,而咱們已經知道pattern[1...6] != pattern[2...7]。因此無論target[13]是否等於pattern[7],這次比較都必然失敗。同理向前移動2個字符也是多餘的比較。由此咱們知道當在pattern[j]處發生匹配失敗時,將當前輸入字符與pattern[j]和pattern[next[j]]之間的任何一個字符對齊執行的匹配嘗試都是必然失敗的。這就說明,在模式串從目標串頭移動到目標串末尾的過程當中,除了跳過了必然失敗的狀況以外,沒有漏掉任何一個可能匹配,因此KMP算法的正確性是有保證的。
後記: