kmp 算法簡介及 next 數組推導

Knuth-Morris-Pratt字符串查找算法(簡稱爲KMP算法)可在一個主文本字符串S內查找一個詞W的出現位置。此算法經過運用對這個詞在不匹配時自己就包含足夠的信息來肯定下一個匹配將在哪裏開始的發現,從而避免從新檢查先前匹配的字符。php

好比下面這種狀況 html

gif中能夠看出,匹配失敗以後kmp算法不對主字符串的指針進行任何的回退,其關心的是對搜索詞指針的處理。git

細心的你可能已經感覺到了一點,上面的處理是抽象的(通用的),既徹底不須要知道主字符串具體是多少的狀況下進行的模擬演習。github

gif中模擬了指針k在字符c處失配的狀況,經過這樣一個預處理,在實際匹配中,若是遇到了這種狀況,咱們只須要從容的將搜索詞指針移動到E處,而後繼續匹配便可。算法

補充一下,爲何搜索詞在移動到 K = E的位置停了下來? 這裏能夠感性的理解,因爲在移動過程當中 AB成功進行了匹配,而在不知道 ‘?’所表明的具體字符是多少的狀況下繼續向前移動搜索詞,則可能會出現錯失匹配的狀況。數組

如今依舊已ABEABC做爲搜索詞,再看幾種演習狀況學習

咱們來總結一下規律。 對於前兩種狀況K指針都沒有移動到起點0,而是中途位置停了下來。 能夠發現 ABEAB?與ABEABC在失配字符以前的字符ABEAB 的頭部與尾部存在相同的字符AB測試

ABEA?和ABEAB在失配以前的字符ABEA中,頭部和尾部存在相同的字符 Aspa

對於這種字符咱們稱之爲先後綴,經過上面的圖咱們發現 K指針在失配時移動到的位置恰好是前綴的後一個字符。但一個字符串的前綴並非惟一的,因此這句話很是不嚴謹。

首先,要了解兩個概念:"前綴"和"後綴"。 "前綴"指除了最後一個字符之外,一個字符串的所有頭部組合;"後綴"指除了第一個字符之外,一個字符串的所有尾部組合。 3d

出自 阮一峯 字符串匹配的KMP算法

瞭解了前綴與後綴以後,咱們再次定義。 當指針j所指向的字符與指針k所指向的字符失配時,失配以前的字符存在一個前綴集合和一個後綴集合,咱們能夠獲得

k' = max(0 ~ k-1的前綴集合 ∩ 0 ~ k-1的後綴集合後綴集合)

k'的含義從公式中看的很清楚(前綴與後綴的交集的最大值)。而另外一層含義則是,若是搜索詞下標從0開始計算,當k處失配時,咱們只須要將k移動到k'處繼續匹配便可。

kmp算法的一般把計算出的k'放到next數組中存儲 next[k] = k'。當咱們實戰中在指針k處失配時,咱們只須要將k指針回退到k'處,既k = k' = next[k]便可。

確實如咱們以前所言,經過定義能夠清楚的認識到,計算next數組徹底不須要主字符串參與,徹底是搜索詞自匹配計算k' = max(0~k-1的前綴集合 ∩ 0~k-1的後綴集合)的過程。

這個定義雖然很嚴謹,便於理解,但卻不能很好的使用計算機語言描述出來。下面看看便於計算機理解的next數組的推導過程。這應該是整個kmp算法最難理解的地方

next數組推導

根據next數組的定義,咱們能夠有

next[j] = k,則 w[0 ~ k-1] = w[j-k ~ j-1]

要明白這二者之間是充分必要條件關係,既 若 w[0 ~ k-1] = w[j-k ~ j-1]next[j] = k

下圖圖中的狀況爲一種知足定義的狀況next[6] = 2

這個我不知道怎麼證實,由於這是由next數組的定義獲得的,因此也不須要證實。

如今已經知道了next[j] = k,瓜熟蒂落,接下來咱們繼續求next[j+1]next[j+1]求解過程當中存在兩種狀況

w[k] == w[j]

根據上面的推導,當 w[k] == w[j]時,有w[0 ~ k] = w[j-k ~ j], 則能夠獲得 next[j+1] = k + 1

w[k] != w[j]

w[k] != w[j]則進入了熟悉的字符串失配環節,明確一下,誰與誰的比較中產生了失配?下圖是一個符合咱們討論的例子

能夠看出在尋找字符串ABEFABA的最大先後綴交集時,kj發生了失配

在kmp算法中若是發生了這種狀況,則另 k = next[k],而後再次讓w[k]與w[j]比較。那麼問題來了

  1. 爲何當w[k] != w[j]時,令 k = next[k], 而不是k = k-1或者其餘呢?

    w[k]與w[j]失配時, k至少要移動到next[k]處才能使得k與主字符串的j繼續匹配。這是next數組的定義,如今只不過在使用這個定義而已

  2. w[k] != w[j],因此另k' = next[k],假如此時w[k'] == w[j],如何證實 w[0 ~ k'] == w[j-k' ~ j] 呢?(圖中粉色部分)

    k' = next[k]獲得w[0 ~ k'-1] == w[k-k' ~ k-1]

    next[j] = k獲得w[0 ~ k-1] == w[j-k ~ j-1]

    由於 w[0 ~ k-1] == w[j-k ~ j-1] 因此 w[k-k' ~ k-1] == w[j-k' ~ j-1]

    這裏屬於感性證實,能力不足暫時沒法使用公式證實

    因此 w[0 ~ k'-1] == w[j-k' ~ j-1]

    又由於 w[k'] == w[j] 因此 w[0 ~ k'] == w[j-k' ~ j]

    w[0 ~ k'] == w[j-k' ~ j],獲得 next[j+1] = k' + 1

    這是假如此時 w[k'] == w[j]的狀況,但大多數狀況是w[k'] != w[j]的,這種狀況咱們在算法實現中討論。

算法實現

next數組實現

private function getNext($word): array {
    $next = [-1];
    $len = strlen($word);
    $k = -1;
    $j = 0;

    while ($j < $len - 1) {
        if ($k == -1 || $word[$j] == $word[$k]) {
            $next[++$j] = ++$k;
        } else {
            $k = $next[$k];
        }
    }

    return $next;
}
複製代碼

next[0] = -1 中-1是一種特殊標誌,方便進行判斷。在上面的w[k] != w[j]時,咱們另 k = next[k]而後再去判斷w[k]是否等於w[j],若是仍是不相等,則再另k = next[k]像這樣一直循環下去。 可是循環總歸有個盡頭,在盡頭會出現這種狀況,此時k = 0,w[k] != w[j],按照算法k = next[0] = -1

所以當咱們看到 k = -1時,咱們就可以知道 w[0 ~ k]不存在前綴與後綴的交集,既 max(0~k的前綴集合 ∩ 0~k的後綴集合) = 0 因此咱們另 next[k+1] = 0便可

上面的算法爲了保持簡潔性,令特殊值爲-1,使得一個if,else能夠覆蓋三種狀況,固然你用下面的寫法也是一個意思

if($k == -1) {
    $next[++$j] = 0;
} elseif ($word[$j] == $word[$k]) {
    $next[++$j] = ++$k;
} else {
    $k = $next[$k];
}

複製代碼

詳細實現含測試用例 github.com/weiwenhao/a…

kmp算法實際上在字符串匹配中使用的狀況並很少,雖然其時間複雜度是O(m+n),但實際上其表現跟樸素算法並不會差太多,在學習的過程當中其實也應該發現了,可以部分匹配的狀況其實很少見。 不得不說kmp算法很是的難以理解,細節太多很容易陷入一個拆東牆補西牆的狀況,各類牛角尖鑽到停不下來。可是其狀態機的思想,以及next數組的推導過程卻很是值得學習。

相關文章
相關標籤/搜索