字符串匹配的KMP算法
阮一峯【字符串匹配的KMP算法】html
-
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html -
原文地址:https://github.com/Aaaaaaaty/blog/issues/42
字符串匹配是計算機的基本任務之一。c++
舉例來講,有一個字符串"BBC ABCDAB ABCDABCDABDE",我想知道,裏面是否包含另外一個字符串"ABCDABD"?git
許多算法能夠完成這個任務,Knuth-Morris-Pratt算法(簡稱KMP)是最經常使用的之一。它以三個發明者命名,起頭的那個K就是著名科學家Donald Knuth。github
這種算法不太容易理解,網上有不少解釋,但讀起來都很費勁。直到讀到Jake Boxer
的文章,我才真正理解這種算法。下面,我用本身的語言,試圖寫一篇比較好懂的KMP算法解釋。web
字符串匹配
字符串匹配是計算機科學中最古老、研究最普遍的問題之一。一個字符串是一個定義在有限字母表∑上的字符序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一個字符串。字符串匹配問題就是在一個大的字符串T中搜索某個字符串P的全部出現位置。算法
kmp算法
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth
,J.H.Morris
和V.R.Pratt
同時發現,所以人們稱它爲克努特——莫里斯——普拉特操做(簡稱KMP算法)。KMP算法的關鍵是利用匹配失敗後的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數自己包含了模式串的局部匹配信息。時間複雜度O(m+n)
。api
在js中字符串匹配咱們一般使用的是原生api,indexOf;其自己是c++實現的不在此次的討論範圍中。本次主要經過動畫演示的方式展示樸素算法與kmp算法對比過程的異同從而試圖理解kmp的基本思路。微信
PS:在以後的敘述中BBC ABCDAB ABCDABCDABDE爲主串;ABCDABD爲模式串編輯器
效果預覽
【演示地址】(https://aaaaaaaty.github.io/blog/Algorithm/kmp/kmp.html)函數
上方爲樸素算法即按位比較,下方爲kmp算法實現的字符串比較方式。kmp能夠經過較少的比較次數完成匹配。
基本思路
從上圖的效果預覽中能夠看出使用樸素算法依次比較模式串須要移位13次,而使用kmp須要8次,故能夠說kmp的思路是經過避免無效的移位,來快速移動到指定的地點。接下來咱們關注一下kmp是如何「跳着」移動的:
與樸素算法一致,在以前對於主串「BBC 」的匹配中模式串ABCBABD的第一個字符均與之不一樣故向後移位到如今上圖所示的位置。主串經過依次與模式串中的字符比較咱們能夠看出,模式串的前6個字符與主串相同即ABCDAB;而這也就是kmp算法的關鍵。
根據已知信息計算下一次移位位置
咱們先從下圖來看樸素算法與kmp中下一次移位的過程:
樸素算法雨打不動得向後移了一位。而kmp跳過了主串的BCD三個字符。從而進行了一次避免無心義的移位比較。那麼它是怎麼知道我此次要跳過三個而不是兩個或者不跳呢?關鍵在於上一次已經匹配的部分ABCDAB
從已匹配部分發掘信息
咱們已知此時主串與模式串均有此相同的部分ABCDAB。那麼如何從這共同部分中得到有用的信息?或者換個角度想一下:咱們能跳過部分位置的依據是什麼?
第一次匹配失敗時的情形以下:
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
-
前綴分別爲A、AB、ABC、ABCD、ABCDA
-
後綴分別爲B、AB、DAB、CDAB、BCDAB
很容易發現部分匹配值爲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
}
kmp算法
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
}
有了思路後總體實現並不複雜,只須要先經過模式串計算各長度的部分匹配值,在以後的與主串的匹配過程當中,每失敗一次後若是有部分匹配值存在,咱們就能夠經過部分匹配值查找到下一次應該移位的位置,省去沒必要要的步驟。
因此在某些極端狀況下,好比須要搜索的詞若是內部徹底沒有重複,算法就會退化成遍歷,性能可能還不如傳統算法,裏面還涉及了比較的開銷。
完整地址:
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;
}
let source = 'BBC ABCDAB ABCDABCDABDE',
match = 'ABCDABD';
let res = mapKMPStr(source, match);
console.log(res);
console.log(source.indexOf(match));
本文分享自微信公衆號 - JavaScript忍者祕籍(js-obok)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。