考研中,複習KMP算法的時候,以爲KMP雖然寫起來很簡單,可是理解起來有些難度,屬於那種看一遍以爲本身懂了,過一下子再去想的時候又想不清楚了。因此寫篇總結性的文章,以本身的理解解讀一下KMP算法,加深理解的同時,也但願讀者有所收穫。javascript
KMP算法在網上有個俗稱叫「看毛片」算法,爲了給KMP正名,我特意查了一下,KMP之因此叫KMP,是由於這種算法是D.E. Knuth 與 V.R. Pratt 和 J.H. Morris 三人同時發現的,人們便以三人名字的首字母KMP命名了這個算法。😄java
KMP算法是一種改進的串模式匹配算法,就是在一個字符串中找到子串的位置。傳統的作法很簡單,相似於暴力求解。好比 主串是ababc
, 模式串是abc
的傳統作法中,是這樣匹配的算法
能夠看到,傳統作法的方式是模式串與主串一一比較,若是其中一個匹配失敗,則從主串上次開始比較的下一個位置在來一遍。若是用i
表示主串的位置,用j
表示模式串的位置,則不難看出在傳統作法下,i
和j
都有回溯,因此KMP改進傳統算法的關鍵點就是如何減小i
和j
的回溯。數組
緊接上文,KMP是如何減小i
和j
的回溯的呢?咱們一點點來分析性能
這裏,設主串爲S1S2S3...Sn ,模式串爲P1P2P3...Pm ,在上節傳統算法的匹配過程當中有一個關鍵狀態ui
table-1spa
爲何這裏是關鍵狀態?由於咱們能夠看到,傳統算法中每次遇到這個狀態時,就會開始回溯i
和j
進行下一輪的匹配了,因此改進算法的關鍵就從這裏開始。設計
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)
狀態時,有兩種狀況可能發生:
i
和j
都加1,向後繼續比較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]
分爲兩種狀況:
j=j+1/t=t+1
時的狀況,因此 next[j+1]=t+1這裏有一個特殊狀況須要另外說明,就是next[0]=-1,什麼意思呢?就是模式串第一個字符發生不匹配時下一個位置爲 -1 。這裏你或許奇怪,爲啥不是 0 ,而是 -1 ,-1是不存在的位置啊?這是由於若是next[0] = 0 ,則一旦第一個字符真的不匹配就會無限死循環。根據咱們上面說的 2)的處理,t=next[t],0位置的下一個回溯位置永遠是 0 ,會死循環的。因此這裏用 -1 做爲一個標識,表示是第一個字符不匹配的狀況。
Next數組求解出來後,就比較簡單了,只要對照着Next數組來「跳」值,就能夠在減小j
的回溯狀況下與主串匹配。
模式串依據Next數組與主串匹配的過程,與Next數組求解的過程很是類似,惟一的區別是:求解Next數組時是模式串
與模式串
匹配的過程,且要記錄Next數組 ; 而KMP串匹配時是主串
與模式串
的匹配過程,但須要使用Next數組做爲指導。
以上就是KMP模式串匹配的核心算法思想,巧妙使用模式串中的重複項來減小i
和j
的回溯,提升算法效率。可是這個算法不是穩定的,由於性能的好壞其實重度依賴於模式串,若是模式串中幾乎沒有重複項,那算法就會退化爲和傳統算法差很少的性能
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
複製代碼
上面我提到若是模式串幾乎沒有重複項的話,算法會退化。可是模式串中重複項過多的話,也是存在一些問題的。咱們舉個極端的例子來講明,好比對於模式串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]
時,
這裏改進不難, 就是在構造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
複製代碼
有問題,請評論區指正!