【數據結構與算法】KMP算法解讀與實現

考研中,複習KMP算法的時候,以爲KMP雖然寫起來很簡單,可是理解起來有些難度,屬於那種看一遍以爲本身懂了,過一下子再去想的時候又想不清楚了。因此寫篇總結性的文章,以本身的理解解讀一下KMP算法,加深理解的同時,也但願讀者有所收穫。javascript

名稱來由

KMP算法在網上有個俗稱叫「看毛片」算法,爲了給KMP正名,我特意查了一下,KMP之因此叫KMP,是由於這種算法是D.E. Knuth 與 V.R. Pratt 和 J.H. Morris 三人同時發現的,人們便以三人名字的首字母KMP命名了這個算法。😄java

KMP算法是爲了解決什麼問題

KMP算法是一種改進的串模式匹配算法,就是在一個字符串中找到子串的位置。傳統的作法很簡單,相似於暴力求解。好比 主串是ababc , 模式串是abc的傳統作法中,是這樣匹配的算法

能夠看到,傳統作法的方式是模式串與主串一一比較,若是其中一個匹配失敗,則從主串上次開始比較的下一個位置在來一遍。若是用i表示主串的位置,用j表示模式串的位置,則不難看出在傳統作法下,ij都有回溯,因此KMP改進傳統算法的關鍵點就是如何減小ij的回溯數組

KMP算法的核心思想

緊接上文,KMP是如何減小ij的回溯的呢?咱們一點點來分析性能

關鍵狀態

這裏,設主串爲S1S2S3...Sn ,模式串爲P1P2P3...Pm ,在上節傳統算法的匹配過程當中有一個關鍵狀態ui

table-1spa

爲何這裏是關鍵狀態?由於咱們能夠看到,傳統算法中每次遇到這個狀態時,就會開始回溯ij進行下一輪的匹配了,因此改進算法的關鍵就從這裏開始。設計

減小i的回溯

傳統作法中下一個狀態i會回溯到i-j+1,而j會回溯到1。如今咱們不要這麼作,替代的作法是,不要回溯i,把P1P2...Pm向後移動,來試圖消除Si處的不匹配,進而開始S(i+1)及其之後字符的比較,使得整個過程繼續推動下去。code

咱們假設上面這個狀態爲 Status(k) ,而在P1P2...Pm向後移動過程當中某處到達Status(k+1)cdn

table-2

到達Status(k+1)狀態時,有兩種狀況可能發生:

  • 其一,Si=Pt , 則Si處的不匹配問題解決了,此時ij都加1,向後繼續比較
  • 其二,Si≠Pt , 則Si處的不匹配問題未解決,此時模式串繼續後移,爲Si的匹配尋找解決狀態

減小j的回溯

咱們能夠看到,P1P2...P(t-1)與P(j-t+1)..P(j-1)重合,這是很常見的,模式串中不免會有一些重複子串,好比abcabcb中,abc就出現了2次。上面說,經過移動模式串而不變主串的方式避免了i的回溯,那這裏的重複項就是有效減小j回溯的關鍵所在。

在傳統作法中,當Si≠Pj時,j就回溯至1,但其實全部中間的比較都是多餘的,由於只有在Status(k+1)狀態時,P1...P(t-1)才能與主串中字符相對應,而此時只須要比較Si與Pt是否相等,也就是說,j只須要回溯到t,前面t-1項是重複的無需比較【由於P1...P(t-1)與P(j-t+1)...P(j-1)是相等的,而P(j-t+1)...P(j-1)在Status(k)狀態已經比較過了】。從這裏能夠看出,j回溯多少,回溯到哪裏,取決於模式串自己的重複項

由於P1...P(j-1)與S(i-j+1)...S(i-1)是相等的,咱們大可去掉,只關注於模式串自己的移動

table-3

咱們假設P1...P(j-1)爲F,P(j-t+1)...P(j-1)爲FR , P1...P(t-1)爲FL ,則邊框內部就是F串中先後相互重合的部分。當Pj處發生不匹配時,咱們下一個j的位置就剛好是FL.length+1或者FR.length+1,也即Pt的位置。因此咱們只要求出模式串從P1到Pm位置發生不匹配時下一個j的位置,就能夠知道下一個狀態Status應該從哪裏開始比較。

求解Next

在KMP算法中,把模式串中每個字符的下一個回溯位置用next[j]表示,咱們接下來聚焦於如何求解next數組

以上面的table-3爲例,此狀態下,next[j]已經求得,注意next數組求解的是當前字符匹配失敗時的下一個j的位置,因此這裏當Pj匹配失敗時,next[j]=t 。 那麼求解next[j+1]分爲兩種狀況:

  • 1)Pj=Pt時,至關於j=j+1/t=t+1時的狀況,因此 next[j+1]=t+1
  • 2)Pj≠Pt時,這裏能夠把Status(k)對應的串看做主串,把Status(k+1)對應的串看做模式串,這樣至關因而回到了由Status(k)找Status(k+1)的過程 , 因此只須要將t賦值爲next[t]繼續比較Pj與Pt

這裏有一個特殊狀況須要另外說明,就是next[0]=-1,什麼意思呢?就是模式串第一個字符發生不匹配時下一個位置爲 -1 。這裏你或許奇怪,爲啥不是 0 ,而是 -1 ,-1是不存在的位置啊?這是由於若是next[0] = 0 ,則一旦第一個字符真的不匹配就會無限死循環。根據咱們上面說的 2)的處理,t=next[t],0位置的下一個回溯位置永遠是 0 ,會死循環的。因此這裏用 -1 做爲一個標識,表示是第一個字符不匹配的狀況。

KMP串匹配

Next數組求解出來後,就比較簡單了,只要對照着Next數組來「跳」值,就能夠在減小j的回溯狀況下與主串匹配。

模式串依據Next數組與主串匹配的過程,與Next數組求解的過程很是類似,惟一的區別是:求解Next數組時是模式串模式串匹配的過程,且要記錄Next數組 ; 而KMP串匹配時是主串模式串的匹配過程,但須要使用Next數組做爲指導。

以上就是KMP模式串匹配的核心算法思想,巧妙使用模式串中的重複項來減小ij的回溯,提升算法效率。可是這個算法不是穩定的,由於性能的好壞其實重度依賴於模式串,若是模式串中幾乎沒有重複項,那算法就會退化爲和傳統算法差很少的性能

JS實現一下KMP算法

KMP算法主要是思想比較難理解,很繞,不畫例子想想Si,Pj,Pt之間的關係,很難真正搞懂爲何要這樣去設計算法。可是明白原理後,實現起來是真的很簡單。這裏我用本身擅長的語言Javascript寫了一個KMP,僅供參考

function getNext(pattern){
    let i = 0 , j = -1
    let next = [-1] //first pos
    while(i<pattern.length-1){
        if(j==-1 || pattern[i] == pattern[j]){
            ++i
            ++j
            next[i] = j
        }else{
            j = next[j]
        }
    }
    
    return next
}
const kmp = function(main , pattern){
    let next = getNext(pattern) //get Next Array
    let i = 0 , j = 0 
    while(i<main.length && j<pattern.length){
        if(j==-1 || main[i] == pattern[j]){
            ++i
            ++j
        }else{
            j = next[j]
        }
    }
    return i-pattern.length
}
module.exports = kmp
複製代碼

改進的KMP算法

上面我提到若是模式串幾乎沒有重複項的話,算法會退化。可是模式串中重複項過多的話,也是存在一些問題的。咱們舉個極端的例子來講明,好比對於模式串aaaaab,它的next數組以下:

咱們看,當 b 不匹配時,

-> j回溯到4

-> a≠b , j回溯到3

-> ...

-> j == -1 , i 越界,退出循環,匹配失敗

咱們能夠看到,其實在b不匹配時徹底能夠直接跳到 j=-1 ,而不用從j=4,j=3...j=-1,由於這些位置上對應的字符都是a,徹底只須要比較1次就足夠了。這就是KMP算法改進的關鍵點。

由於next數組是由前至後逐步創建的,就是說求next[j]時,前j-1項next是已知的。因此咱們只需沒走一步往前看一個就能夠了。好比,求next[j]時,

  • Pj = P(next[j]) , 也便是Pj下一個位置對應的字符和本身同樣時,則 next[j] = next[next[j]]
  • Pj ≠ P(next[j]) , 則next[j] = next[j] , 和以前KMP算法中的值同樣,保持不變

這裏改進不難, 就是在構造next數組時作一些小改動,下面是改進後的代碼示例:

function getNext(pattern){
    let i = 0 , j = -1
    let next = [-1] //first pos
    while(i<pattern.length-1){
        if(j==-1 || pattern[i] == pattern[j]){
            ++i
            ++j
            if(pattern[i]!=pattern[j])    //改進求Next數組
            next[i] = j                   //
            else                          //
            next[i] = next[j]             //
        }else{
            j = next[j]
        }
    }
    return next
}
const kmp = function(main , pattern){
    let next = getNext(pattern) //get Next Array
    let i = 0 , j = 0 
    while(i<main.length && j<pattern.length){
        if(j==-1 || main[i] == pattern[j]){
            ++i
            ++j
        }else{
            j = next[j]
        }
    }
    return i-pattern.length
}
module.exports = kmp




複製代碼

有問題,請評論區指正!

相關文章
相關標籤/搜索