一、暴力法
爲了更好地理解KMP算法,我們先來看看樸素的暴力法是如何執行的。
設待匹配的字符串爲ABABABABAC,模式串爲ABABAC,i爲字符串下標,j爲模式串下標
首先取i=0,遍歷一遍j,直到出現不匹配:
ABABABABAC
ABABAC
然後取i=1,遍歷一遍j,直到出現不匹配:
ABABABABAC
ABABAC
然後取i=2,遍歷一遍j,直到出現不匹配:
ABABABABAC
ABABAC
……
不難發現,這種暴力法的缺點:做了很多無用功,從i往後已經走了很多步了,但是每次i還是隻能退回去。
二、“滑動條帶”的思想
如果把模式串想象成兩個重疊在一起的透明條帶,滑動其中一個 ,直到與另一個部分完全重合。
像這樣:(以i=2時爲例)
ABABABABAC
ABABAC
-> ABABAC
這樣,i就不用動了,直接把j調整爲4,繼續匹配
具體“滑動”到什麼位置(把j調整到什麼位置),可以事先計算好,這就要說到DFA了
三、DFA
圖中,每個圓圈都是一個狀態,圓圈中的數字表示已經匹配好的字符個數,箭頭表示了狀態轉移的方式,箭頭上的字母表示下一個出現的字母是什麼。狀態怎麼轉移,已經通過“滑動窗口”定義好了,於是DFA也就定義好了。
可以證明,如果每一步都遵循DFA來走,那麼在任意時刻,能匹配到的字符串都是最長的。(證明:如若不然,必然存在某個時刻t,使得t-1時刻是最長的,t時刻不是最長的,那麼就違背了滑動窗口的定義)
這一點很關鍵,在接下來DFA生成的部分會用到。
現在先不管DFA是怎麼高效生成的,既然我們已經可以用笨方法(滑動窗口)來生成DFA了。
假設我們已經有了DFA,那麼代碼就可以這麼寫:
(由於i不會回退,我們可以一邊讀數據一邊處理:
可見有了DFA,還是非常高效的。i不會回退,因此可以在O(待匹配字符串長度)的時間內完成匹配。
但是,DFA的生成會不會拖後腿呢?
四、DFA的生成(核心)
原理部分,我們可以這樣理解:
首先用“一次性完美匹配”的情況來構建DFA的骨架
接下來的問題是:在狀態j(意即:已經成功匹配了j個字符,也就是模式串中下標爲0~j-1的字符),如果在下一步出現了不匹配,該跳轉到哪裏?
設待匹配字符串中,不匹配的那個字符爲c。
目標是要滑動窗口,尋找最長的重疊字符串,換言之,要在 [ 下標爲1~j-1的字符 + c ] 中匹配最長的字符串。等等,這個場景似曾相識,既然恰好j個字符(不會越界),那我們用當前的DFA半成品過一遍 [ 下標爲1~j-1的字符 + c ] ,結果指向哪,我們就跳轉到哪!
於是,生成DFA的代碼如下:
代碼中的X代表用半成品DFA過一遍 [ 下標爲1~j-1的字符 ](沒有c)的結果
複雜度如圖所示,沒怎麼拖後腿
PS:還有一種NFA版本的KMP算法,更爲常見一些,改天再研究