假設有字符串X和Y,知足len(X)>len(Y),要比對這兩個字符串。算法
咱們知道,最樸實的方法,就是現將兩者對齊,而後依次比對對應位置的字符。若是能匹配到Y最後位置,則匹配成功;若是匹配失敗,則將Y右移一位,再從頭進行匹配。數組
設字符串X爲dababeabafdababcg;字符串Y爲ababc。指針
這種比對方法以下所示:code
起始時,兩者對其,第一個字符不匹配 :| :dababeabafdababcg :ababc字符串
右移一位,比對位置移動到Y起始位置數學
: | :dababeabafdababcg : ababc
連續成功4次,再次遇到不匹配string
: | :dababeabafdababcg : ababc
右移一位,比對位置移動到Y起始位置原理
: | :dababeabafdababcg : ababc :
不斷重複該過程,直到…………搜索
: | :dababeabafdababcg : ababc
Y完整匹配到X,結束。循環
毫無疑問,這種方法太笨了。時間複雜度高達O(mn)。最大的問題是,進行了大量的重複比對工做。
kmp算法
正是爲了解決這一點而提出的。kmp算法
的中心思想是,已經匹配的部分不須要再次去匹配,相反,應根據已匹配的部分進行多字節的挪動,加快匹配速度。
怎麼作呢?kmp給出的答案是:
已經匹配的部分,能夠說是已知的,並且是隻取決於字符串Y的,對字符串Y的挪動能夠直接挪動到下一個知足匹配的位置。
什麼叫下一個知足匹配的位置?
假設字符串X和字符串Y已經匹配的部分爲abcab
,顯然:
: abcab... : abcab... : : abcab... : abcab... :
都是不匹配的,只有以下的
:abcab... : abcab...
才匹配。顯然,新的匹配序列,是舊的匹配序列的一個真後綴,並且仍是字符串Y的一個前綴;而舊的匹配序列也是字符串Y的前綴。
也就是說,根據已經匹配的部分,咱們已經能夠排除大量的位置了。kmp算法
正是基於這一原理實現的。
規則1
若是當前比對位置兩個字符相同,則比對位置右移1位
規則2
若是當前比對位置兩個字符不一樣,則:若是沒有匹配到序列,則比對位置右移1位,字符串Y右移1位
若是已有匹配到序列,則移動字符串Y到下一個匹配位置
以下例,起始
:| :dababeabafdababcg :ababc
字符不匹配,比對位置和字符串Y都右移一位
: | :dababeabafdababcg : ababc
字符匹配,比對位置右移1位
: | :dababeabafdababcg : ababc
連續成功匹配4次,再次遇到不匹配
: | :dababeabafdababcg : ababc
字符不匹配,挪動字符串Y到下一個匹配位置
: | :dababeabafdababcg : ababc
仍舊不匹配,繼續挪動字符串Y
: | :dababeabafdababcg : ababc
仍舊不匹配,已經沒有匹配序列了,比對位置和字符串Y都右移一位
: | :dababeabafdababcg : ababc
字符匹配,比對位置右移1位
: | :dababeabafdababcg : ababc
連續成功匹配3次,再次遇到不匹配
: | :dababeabafdababcg : ababc
字符不匹配,挪動字符串Y到下一個匹配位置
: | :dababeabafdababcg : ababc
仍舊不匹配,繼續挪動字符串Y
: | :dababeabafdababcg : ababc
字符不匹配,無匹配序列,比對位置和字符串Y都右移一位
: | :dababeabafdababcg : ababc
字符不匹配,無匹配序列,比對位置和字符串Y都右移一位
: | :dababeabafdababcg : ababc
字符匹配,比對位置右移1位
: | :dababeabafdababcg : ababc
連續成功匹配5次,到達字符串Y終點,匹配成功
: | :dababeabafdababcg : ababc
因而可知,kmp算法
的比對位置沒有倒車的狀況,並且同一位置比對的次數也明顯少於樸實算法
,比對速率是至關快的。
kmp算法的關鍵,是根據已有匹配序列計算下一個匹配序列
,而這一部分,是隻須要字符串Y就能夠實現的,由於下一匹配序列的計算和已有匹配序列以外的部分,無關。
事實上,根據已有匹配序列,徹底能夠獲得多個知足要求的下一級匹配序列(即既是已有匹配序列的真後綴,又是字符串Y的前綴),可是毫無疑問,咱們應該選擇最長的那個。
kmp算法
的關鍵,也就是對根據已有匹配序列計算下一個匹配序列
這一個步驟的計算了,由於咱們知道下一匹配序列必然是字符串Y的前綴,實際上只須要記錄下一匹配序列的長度便可。這也就是所謂的next數組的真面目。(其餘人介紹的next數組的值可能和我說的不一樣,但實際上都是對下一匹配長度進行數學變換的結果)
那麼關於next數組的計算,天然也有多種方法,好比很樸實的方法,我就很少說了。事實上,有個很好的方法能夠輕鬆的計算出next數組。這個方法和kmp自己比對的過程有些相似,同時也和後綴樹構樹的ukkonen算法
有殊途同歸之妙。
go代碼以下:
// 計算既是s[:i]真後綴又是其真前綴的最長(連續)序列的長度。 // 既是真後綴,也是真前綴,稱之爲(同向)迴文序列。 // 長度爲零的序列是任意序列的(同向)迴文序列。 // n[i]用來表示s[:i]最長(同向)迴文序列長度。 func next(s string) []int { // 已知當前比較位置前的字符都匹配 n := make([]int, l) // n[0]、n[1]都是0,從n[2]開始算起 for i, j := 2, 0; i < l; { // i-1表示s[:i]的後綴的當前比較位置 if s[i-1] == s[j] { // j 表示s[:i]的前綴的當前比較位置 n[i] = j+1 // j+1已是最大的序列長度了 i, j = i+1, j+1 // 同時移動兩個字符串的比較位置 continue // 進入下一比較位置的循環 } // 字符不匹配,最長迴文不能延長 if j == 0 { // 沒有非空的迴文序列了 i++ // n[i]的默認值即爲0 continue // 從頭開始匹配循環 } // 存在非空的迴文序列 j = n[j] // 找到下一個匹配位置 } return n }
咱們每一次循環的時候,只進行了一個字符的比對!這是由於以前的比對已經保證前面的部分是匹配的。那麼若是這一次比對成功,只須要延長下一次匹配的長度就好了;若是比對失敗,咱們也不須要從頭開始去查找下一個匹配的起始位置,由於以前的匹配結果已經告訴了咱們下一個匹配的位置。
結果是,kmp算法
構造next數組和比對的過程,都很迅速!
kmp算法
完整代碼以下:
// kmp字符串搜索算法 func KMP(s, r string) int { l := len(r) // 成員n[i]表示既是s[:i]真後綴又是s前綴的最長序列的長度。 // 真後綴指非自身的非空後綴。如不存在,則置該成員值爲0。 n := func(s string) []int { n := make([]int, l) // 從n[2]開始算起 for i, j := 2, 0; i < l; { // 已知前面的字符所有匹配 if s[i-1] == s[j] { j++ n[i] = j i++ continue } if j == 0 { // 申請的n會所有初始化爲零 i++ continue } // 相似後綴指針的功用 j = n[j] } return n }(r) // 進行搜索 i, j := 0, 0 for i+l < j+len(s) && j < l { if s[i] == r[j] { i, j = i+1, j+1 continue } if j == 0 { i++ } else { j = n[j] } } if j == l { return i - l } return -1 }
kmp算法是一個至關精巧,可是代碼卻很是簡單的算法。相比之下,雖然速度可能遜色於BM算法,可是確實優美得多。