本次分享一下經過實現kmp算法的動畫效果來試圖展現kmp的基本思路。html
歡迎關注個人博客,不按期更新中——c++
字符串匹配是計算機科學中最古老、研究最普遍的問題之一。一個字符串是一個定義在有限字母表∑上的字符序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一個字符串。字符串匹配問題就是在一個大的字符串T中搜索某個字符串P的全部出現位置。
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,所以人們稱它爲克努特——莫里斯——普拉特操做(簡稱KMP算法)。KMP算法的關鍵是利用匹配失敗後的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數自己包含了模式串的局部匹配信息。時間複雜度O(m+n)。
在js中字符串匹配咱們一般使用的是原生api,indexOf;其自己是c++實現的不在此次的討論範圍中。本次主要經過動畫演示的方式展示樸素算法與kmp算法對比過程的異同從而試圖理解kmp的基本思路。git
PS:在以後的敘述中BBC ABCDAB ABCDABCDABDE爲主串;ABCDABD爲模式串github
上方爲樸素算法即按位比較,下方爲kmp算法實現的字符串比較方式。kmp能夠經過較少的比較次數完成匹配。算法
從上圖的效果預覽中能夠看出使用樸素算法依次比較模式串須要移位13次,而使用kmp須要8次,故能夠說kmp的思路是經過避免無效的移位,來快速移動到指定的地點。接下來咱們關注一下kmp是如何「跳着」移動的:api
與樸素算法一致,在以前對於主串「BBC 」的匹配中模式串ABCBABD的第一個字符均與之不一樣故向後移位到如今上圖所示的位置。主串經過依次與模式串中的字符比較咱們能夠看出,模式串的前6個字符與主串相同即ABCDAB;而這也就是kmp算法的關鍵。函數
咱們先從下圖來看樸素算法與kmp中下一次移位的過程:性能
樸素算法雨打不動得向後移了一位。而kmp跳過了主串的BCD三個字符。從而進行了一次避免無心義的移位比較。那麼它是怎麼知道我此次要跳過三個而不是兩個或者不跳呢?關鍵在於上一次已經匹配的部分ABCDAB動畫
咱們已知此時主串與模式串均有此相同的部分ABCDAB。那麼如何從這共同部分中得到有用的信息?或者換個角度想一下:咱們能跳過部分位置的依據是什麼?spa
第一次匹配失敗時的情形以下:
BBC ABCDAB ABCDABCDABDE ABCDABD D != 空格 故失敗
爲了從已匹配部分提取信息。如今將主串作一下變形:
ABCDABXXXXXX... X多是任何字符
咱們如今只知道已匹配的部分,由於匹配已經失敗了不會再去讀取後面的字符,故用X代替。
那麼咱們能跳過多少位置的問題就能夠由下面的解得知答案:
//ABCDAB向後移動幾位可能能匹配上? ABCDABXXXXXX... ABCDABD
答案天然是以下移動:
ABCDABXXXXXX... ABCDABD
由於咱們不知道X表明什麼,只能從已匹配的串來分析。
故咱們能跳過部分位置的依據是什麼?
答:已匹配的模式串的前n位可否等於匹配部分的主串的後n位。而且n儘量大。
舉個例子:
//第一次匹配失敗時匹配到ABCDDDABC爲共同部分 XXXABCDDDABCFXXX ABCDDDABCE //尋找模式串的最大前幾位與主串匹配到的部分後幾位相同, //能夠發現最可能是ABC部分相同,故能夠略過DDD的匹配由於確定對不上 XXXABCDDDABCFXXX ABCDDDABCE
如今kmp的基本思路已經很明顯了,其就是經過經失敗後得知的已匹配字段,來尋找主串尾部與模式串頭部的相同最大匹配,若是有則能夠跨過中間的部分,由於所謂「中間」的部分,也是有可能進入主串尾與模式串頭的,沒進去的緣由便是相對位置字符不一樣,故最終在模式串移位時能夠跳過。
上面是用通俗的話來述說咱們如何根據已匹配的部分來決定下一次模式串移位的位置,你們應該已經大致知道kmp的思路了。如今來引出官方的說法。
以前敘述的在已匹配部分中查找主串頭部與模式串尾部相同的部分的結果咱們能夠用部分匹配值的說法來形容:
例如ABCDAB
很容易發現部分匹配值爲2即AB的長度。從而結合以前的思路能夠知道將模式串直接移位到主串AB對應的地方便可,中間的部分必定是不匹配的。移動幾位呢?
答:匹配串長度 - 部分匹配值;本次例子中爲6-2=4,模式串向右移動四位
function pmtArr(target) { var pmtArr = [] target = target.split('') for(var j = 0; j < target.length; j++) { //獲取模式串不一樣長度下的部分匹配值 var pmt = target var pmtNum = 0 for (var k = 0; k < j; k++) { var head = pmt.slice(0, k + 1) //前綴 var foot = pmt.slice(j - k, j + 1) //後綴 if (head.join('') === foot.join('')) { var num = head.length if (num > pmtNum) pmtNum = num } } pmtArr.push(j + 1 - pmtNum) } return pmtArr }
function mapKMPStr(base, target) { var isMatch = [] var pmt = pmtArr(target) console.time('kmp') var times = 0 for(var i = 0; i < base.length; i++) { times++ var tempIndex = 0 for(var j = 0; j < target.length; j++) { if(i + target.length <= base.length) { if (target.charAt(j) === base.charAt(i + j)) { isMatch.push(target.charAt(j)) } else { if(!j) break //第一個就不匹配直接跳到下一個 var skip = pmt[j - 1] tempIndex = i + skip - 1 break } } } var data = { index: i, matchArr: isMatch } callerKmp.push(data) if(tempIndex) i = tempIndex if(isMatch.length === target.length) { console.timeEnd('kmp') console.log('移位次數:', times) return i } isMatch = [] } console.timeEnd('kmp') return -1
有了思路後總體實現並不複雜,只須要先經過模式串計算各長度的部分匹配值,在以後的與主串的匹配過程當中,每失敗一次後若是有部分匹配值存在,咱們就能夠經過部分匹配值查找到下一次應該移位的位置,省去沒必要要的步驟。
因此在某些極端狀況下,好比須要搜索的詞若是內部徹底沒有重複,算法就會退化成遍歷,性能可能還不如傳統算法,裏面還涉及了比較的開銷。
慣例po做者的博客,不定時更新中——
有問題歡迎在issues下交流。