【重學數據結構與算法(JS)】字符串匹配算法(二)——KMP算法

前言

在上一篇文章字符串匹配算法(一)——BF算法提到過,字符串匹配的思路是固定的:算法

  1. 模式串主串進行比較segmentfault

    • 從前日後比較
    • 從後往前比較
  2. 匹配時,比較主串模式串的下一個位置
  3. 失配時,數組

    • 模式串中尋找一個合適的位置數據結構

      • 若是找到,從這個位置開始與主串當前失配位置進行比較
      • 若是未找到,從模式串的頭部與主串失配位置的下一個位置進行比較
    • 主串中找到一個合適的位置,從新與模式串進行比較

優化在於其中的步驟,而KMP算法就是優化第3步失配時尋找模式串合適位置的操做框架

算法介紹和分析

那麼如何尋找模式串中所謂合適的位置呢?能夠先來看個栗子:工具

QQ20200114-214316.png
QQ20200114-214756.png

......優化

QQ20200114-215525.png
QQ20200114-220336.png

上面是 BF 匹配過程當中從Nk到Nk+mm 次匹配過程,從中咱們能夠發現,從第 k 步到第 k+m 步時,指針 ij 又回到了相同的位置,且 第 k+m 步 更具備匹配的可能性,因此咱們思考一下,是否是能夠由第 k 步直接跳到第 k+m 步呢?若是能夠,就能夠減小 m-1 次比較,大大提高效率。再進一步思考,若是將整個匹配過程再看做是重複地由Nk直接到Nk+m的推動,那麼每次重複時,模式串開始比較的位置就是咱們所要找的合適的位置spa

如何尋找這些位置呢?咱們能夠把這個問題轉化爲求next數組的過程。3d

求 next 數組

咱們再仔細觀察下 Nk 和 Nk+m 兩個狀態指針

QQ20200114-214316.png
QQ20200114-220336.png

因爲 Nk 狀態下,模式串主串具備徹底匹配的部分,且要達到 Nk+m 狀態所需移動到的位置信息也存在於匹配的部分,所以咱們能夠無視掉主串,只看模式串便可獲得next數組

再認真觀察咱們還能發現,Nk 狀態不匹配時,Nk+m 狀態本質上是將模式串中的另一對 AB主串 達成以前的已匹配狀態。因此求next數組的問題又能夠轉化爲當m位置不匹配時,求m位置以前的子串的最大相同先後綴的問題。

首先要創建一個規則,具備先後綴的字符串長度至少爲2,因此咱們定義若是長度爲0,則對應next數組值爲-1,若是長度爲1,值爲0。下面舉個栗子:

ABABABD

QQ20200115-204949.png

手工求這麼看其實沒什麼難度,本身多寫幾個串練一遍就會了。

代碼

學會如何手工求next數組以後,整個KMP算法的代碼如何寫呢?
還記得最開始提到要記住的一點嗎?匹配思路是同樣的,只是優化了失配後的操做。根據這一點,咱們能夠把BF算法的框架先搬過來:

carbon.png

這樣是否是能夠接下來去補全 getNext() 方法就能夠了呢?咱們來看一個特殊狀況:

QQ20200115-211138.png

QQ20200115-211437.png

當處在Nk+m狀態時,發現失配位置前的 AB 沒有最長公共先後綴,因而只能退回到BF算法的作法,也就是i++;j=0。可是這和咱們上面的框架代碼不符,須要進行改造:

  • 每當 j = next[j] === -1 時,也須要進入第一個分支,使得 i++;j++(-1 + 1 = 0),變相達到效果。

獲得最終的框架代碼:

carbon的副本.png

接下來就是進行對next數組的求解——完善 getNext()。這時候有的同窗可能就會想對上述手工求法進行代碼轉化,但是萬一模式串很長的話,那麼這個時間複雜度就會變得至關的高,因此須要採用迭代法,利用每次所得的結果來求下一個結果,從而拼湊出next數組

咱們假設某一時刻有一個狀態Sk

QQ20200116-214003.png

此時咱們已經求完了next[j]的值,如何去求next[j+1]呢?仔細觀察狀態圖,發現:

  • 若Pk === Pj,則 Pj+1 前有next[j] + 1 = 4個相同的先後綴 P0P1Pk-jPk 和 Pj-kPj-k+1Pj-1Pj,也就是 next[j+1] = next[j] +1 = k + 1

再來看一個狀態

QQ20200118-151300.png

一樣是求完了next[j]的值,

  • 若Pk === Pj,對比 Pnext[k] 是否 等於 Pj;若是 Pnextn[k] === Pj,則next[j+1] = Pnextn[k] + 1 = k + 1

若是 Pnextn[k] !== Pj呢?

QQ20200118-151336.png

能夠看到,

  • 若是Pnextn[k] !== Pj,則不斷地遞歸前綴索引 k = next[k] 直到回到前綴第一個位置,則表示沒有相同的先後綴,此時 j = -1,則 next[j+1] = Pnextn[k] + 1 = k + 1 = 0

根據以上分析,咱們能夠補充完 getNext()

carbon的副本2.png

再優化一下寫法

carbon的副本3.png

至此,一個完整的KMP算法就寫好了。

思考是否還有優化的空間

咱們來看一個特殊的例子:

QQ20200118-155025.png

這是一個前綴相同的一個模式串,且咱們已經求得了next數組,接下來咱們模擬一下上面寫好的程序進行的操做:

  1. j = 5,needle[5] !== haystack[i];next[j] = 4,j = next[j];
  2. j = 4,needle[4] !== haystack[i];next[j] = 3,j = next[j];
  3. j = 3,needle[3] !== haystack[i];next[j] = 2,j = next[j];
  4. j = 2,needle[2] !== haystack[i];next[j] = 1,j = next[j];
  5. j = 1,needle[1] !== haystack[i];next[j] = 0,j = next[j];
  6. j = 0,needle[0] !== haystack[i];next[j] = -1,j = next[j];
  7. j = -1, j++;i++;

咱們發現因爲前綴都是相等的,當第1步發現失配時,直接 j = -1 就能夠了,也就是 next[5] = -1 便可。因此,優化點實際上是體如今對next數組的優化,咱們稱之爲nextVal數組

求nextVal數組

如何求nextVal數組呢?咱們仍是以上面的特殊狀況爲例,看兩個狀態:

QQ20200118-163223.png

QQ20200118-164922.png

此時咱們已經求完了nextVal[j]的值,仔細觀察狀態圖,發現:

  • 根據求next數組的過程,next[j + 1] = k + 1

    • 若Pj+1 !== Pnext[j + 1],在Pnext[j + 1]發生失配時,只要跳到Pj+1就有可能解決失配問題,則此時的 nextVal[j + 1] = next[j + 1]便可
    • 若Pj+1 === Pnext[j + 1],在Pnext[j + 1]發生失配時,跳到Pj+1就並不能解決失配問題,則此時應該繼續回溯nextVal的next[j + 1]的位置上(因爲是迭代求法,此時nextVal[next[j + 1]]上的值必定是經過nextVal[next2[j + 1]]求得了),即 nextVal[j + 1] = nextVal[next[j + 1]]

能夠在 getNext() 的基礎上獲得如下代碼:

carbon的副本4.png

next數組如今就已是一個無關緊要的工具人了,咱們把去掉,獲得下一版代碼:

carbon (1).png

再進行如下優化獲得最終代碼:

carbon (2).png

總結

總的來講,KMP算法BF算法的字符串匹配思路在大方向上是沒有區別的,只是引入了一個next數組nextVal數組來求得模式串合適的位置。只要理解了這兩個數組的求法,也就基本理解了KMP算法

後記

「字符串匹配算法」是「重學數據結構與算法」系列筆記:

相關文章
相關標籤/搜索