JavaScript中資料結構與演算法(五):經典KMP演算法

來源:互聯網
上載者:User

   這篇文章主要介紹了JavaScript中資料結構與演算法(五):經典KMP演算法,本文詳解了KMP演算法的方方面在,需要的朋友可以參考下

  KMP演算法和BM演算法

  KMP是首碼匹配和BM尾碼匹配的經典演算法,看得出來首碼匹配和尾碼匹配的區別就僅僅在於比較的順序不同

  首碼匹配是指:模式串和母串的比較從左至右,模式串的移動也是從 左到右

  尾碼匹配是指:模式串和母串的的比較從右至左,模式串的移動從左至右。

  通過上一章顯而易見BF演算法也是屬於首碼的演算法,不過就非常霸蠻的逐個匹配的效率自然不用提了O(mn),網上蛋疼的KMP是講解很多,基本都是走的高大上路線看的你也是一頭霧水,我試圖用自己的理解用最接地氣的方式描述

  KMP

  KMP也是一種最佳化版的首碼演算法,之所以叫KMP就是Knuth、Morris、Pratt三個人名的縮寫,對比下BF那麼KMP的演算法的最佳化點就在“每次往後移動的距離”它會動態調整每次模式串的移動距離,BF是每次都+1,

  KMP則不一定

  如圖BF與KMP前置演算法的區別對比

  我通過圖對比我們發現:

  在文本串T中搜尋模式串P,在自然匹配第6個字母c的時候發現二等不一致了,那麼BF的方法,就是把整個模式串P移動一位,KMP則是移動二位.

  BF的匹配方法我們是知道的,但是KMP為什麼會移動二位,而不是一位或者三位四位呢?

  這就上一張圖我們講解下,模式串P在匹配了ababa的時候都是正確的,當到c的時候才是錯誤,那麼KMP演算法的想法是:ababa是正確的匹配完成的資訊,我們能不能利用這個資訊,不要把"搜尋位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。

  那麼問題來了, 我怎麼知道要移動多少個位置?

  這個位移的演算法KMP的作者們就給我們總結好了:

  代碼如下:

  移動位元 = 已匹配的字元數 - 對應的部分匹配值

  位移演算法只跟子串有關係,沒文本串沒毛線關係,所以這裡需要特別注意了

  那麼我們怎麼理解子串中已匹配的字元數與對應的部分匹配值?

  已匹配的字元:

   代碼如下:

  T : abababaabab

  p : ababacb

  p中紅色的標記就是已經匹配的字元,這個很好理解

  部分匹配值:

  這個就是核心的演算法了,也是比較難於理解的

  假如:

   代碼如下:

  T:aaronaabbcc

  P:aaronaac

  我們可以觀察這個文本如果我們在匹配c的時候出錯,我們下一個移動的位置就上個的結構來講,移動到那裡最合理?

   代碼如下:

  aaronaabbcc

  aaronaac

  那麼就是說:在模式文本內部,某一段字元頭尾都一樣,那麼自然過濾的時候可以跳過這一段內容了,這個思路也是合理的

  知道了這個規律,那麼給出來的部分匹配表演算法如下:

  首先,要瞭解兩個概念:"首碼"和"尾碼"。 "首碼"指除了最後一個字元以外,一個字串的全部頭部組合;"尾碼"指除了第一個字元以外,一個字串的全部尾部組合。

  "部分匹配值"就是"首碼"和"尾碼"的最長的共有元素的長度”

  我們看看aaronaac的如果是BF匹配的時候劃分是這樣的

  BF的位移: a,aa,aar,aaro,aaron,aarona,aaronaa,aaronaac

  那麼KMP的劃分呢?這裡就要引入首碼與尾碼了

  我們先看看KMP部分匹配表的結果是這樣的:

  代碼如下:

  a a r o n a a c

  [0, 1, 0, 0, 0, 1, 2, 0]

  肯定是一頭霧水,不急我們分解下,首碼與尾碼

   代碼如下:

  匹配字串 :“Aaron”

  首碼:A,Aa, Aar ,Aaro

  尾碼:aron,ron,on,n

  移動的位置:其實就是針對每一個已匹配的字元做首碼與尾碼的對比是否相等,然後算出共有的長度

  部分匹配表的分解

  KMP中的匹配表的演算法,其中p表示首碼,n表示尾碼,r表示結果

  代碼如下:

  a, p=>0, n=>0 r = 0

  aa, p=>[a],n=>[a] , r = a.length => 1

  aar, p=>[a,aa], n=>[r,ar] ,r = 0

  aaro, p=>[a,aa,aar], n=>[o,ra,aro] ,r = 0

  aaron p=>[a,aa,aar,aaro], n=>[n,on,ron,aron] ,r = 0

  aarona, p=>[a,aa,aar,aaro,aaron], n=>[a,na,ona,rona,arona] ,r = a.lenght = 1

  aaronaa, p=>[a,aa,aar,aaro,aaron,aarona], n=>[a,aa,naa,onaa,ronaa,aronaa] , r = Math.max(a.length,aa.length) = 2

  aaronaac p=>[a,aa,aar,aaro,aaron,aarona], n=>[c,ac,aac,naac,onaac,ronaac] r = 0

  類似BF演算法一下,先分解每一次可能匹配的下標的位置先緩衝起來,在匹配的時候通過這個《部分匹配表》來定位需要後移動的位元

  所以最後aaronaac的匹配表的結果 0,1,0,0,0,1,2,0 就是這麼來的

  下面將會實現JS版的KMP,有2種

  KMP實現(一):緩衝匹配表的KMP

  KMP實現(二):動態計算next的KMP

  KMP實現(一)

  匹配表

  KMP演算法中最重要的就是匹配表,如果不要匹配表那就是BF的實現,加上匹配表就是KMP了

  匹配表決定了next下一個位移的計數

  針對上面匹配表的規律,我們設計一個kmpGetStrPartMatchValue的方法

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function kmpGetStrPartMatchValue(str) { var prefix = []; var suffix = []; var partMatch = []; for (var i = 0, j = str.length; i < j; i++) { var newStr = str.substring(0, i + 1); if (newStr.length == 1) { partMatch[i] = 0; } else { for (var k = 0; k < i; k++) { //首碼 prefix[k] = newStr.slice(0, k + 1); //尾碼 suffix[k] = newStr.slice(-k - 1); //如果相等就計算大小,並放入結果集中 if (prefix[k] == suffix[k]) { partMatch[i] = prefix[k].length; } } if (!partMatch[i]) { partMatch[i] = 0; } } } return partMatch; }

  完全按照KMP中的匹配表的演算法的實現,通過str.substring(0, i + 1) 分解a->aa->aar->aaro->aaron->aarona->aaronaa-aaronaac

  然後在每一個分解中通過首碼尾碼算出共有元素的長度

  回退演算法

  KMP也是前置演算法,完全可以把BF那一套搬過來,唯一修改的地方就是BF回溯的時候直接是加1,KMP在回溯的時候我們就通過匹配表算出這個next值即可

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 //子迴圈 for (var j = 0; j < searchLength; j++) { //如果與主串匹配 if (searchStr.charAt(j) == sourceStr.charAt(i)) { //如果是匹配完成 if (j == searchLength - 1) { result = i - j; break; } else { //如果匹配到了,就繼續迴圈,i++是用來增加主串的下標位 i++; } } else { //在子串的匹配中i是被疊加了 if (j > 1 && part[j - 1] > 0) { i += (i - j - part[j - 1]); } else { //移動一位 i = (i - j) } break; } }

  紅色標記的就是KMP的核心點 next的值 = 已匹配的字元數 - 對應的部分匹配值

  完整的KMP演算法

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 <!doctype html><div id="test2"><div><script type="text/javascript">     function kmpGetStrPartMatchValue(str) { var prefix = []; var suffix = []; var partMatch = []; for (var i = 0, j = str.length; i < j; i++) { var newStr = str.substring(0, i + 1); if (newStr.length == 1) { partMatch[i] = 0; } else { for (var k = 0; k < i; k++) { //取首碼 prefix[k] = newStr.slice(0, k + 1); suffix[k] = newStr.slice(-k - 1); if (prefix[k] == suffix[k]) { partMatch[i] = prefix[k].length; } } if (!partMatch[i]) { partMatch[i] = 0; } } } return partMatch; }       function KMP(sourceStr, searchStr) { //產生匹配表 var part = kmpGetStrPartMatchValue(searchStr); var sourceLength = sourceStr.length; var searchLength = searchStr.length; var result; var i = 0; var j = 0;   for (; i < sourceStr.length; i++) { //最外層迴圈,主串   //子迴圈 for (var j = 0; j < searchLength; j++) { //如果與主串匹配 if (searchStr.charAt(j) == sourceStr.charAt(i)) { //如果是匹配完成 if (j == searchLength - 1) { result = i - j; break; } else { //如果匹配到了,就繼續迴圈,i++是用來增加主串的下標位 i++; } } else { //在子串的匹配中i是被疊加了 if (j > 1 && part[j - 1] > 0) { i += (i - j - part[j - 1]); } else { //移動一位 i = (i - j) } break; } }   if (result || result == 0) { break; } }     if (result || result == 0) { return result } else { return -1; } }   var s = "BBC ABCDAB ABCDABCDABDE"; var t = "ABCDABD";     show('indexOf',function() { return s.indexOf(t) })   show('KMP',function() { return KMP(s,t) })   function show(bf_name,fn) { var myDate = +new Date() var r = fn(); var div = document.createElement('div') div.innerHTML = bf_name +'演算法,搜尋位置:' + r + ",耗時" + (+new Date() - myDate) + "ms"; document.getElementById("test2").appendChild(div); }     </script></div></div>

  KMP(二)

  第一種kmp的演算法很明顯,是通過緩衝尋找匹配表也就是常見的空間換時間了。那麼另一種就是時時尋找的演算法,通過傳遞一個具體的完成字串,算出這個匹配值出來,原理都一樣

  產生緩衝表的時候是整體全部算出來的,我們現在等於只要挑其中的一條就可以了,那麼只要演算法定位到當然的匹配即可

  next演算法

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function next(str) { var prefix = []; var suffix = []; var partMatch; var i = str.length var newStr = str.substring(0, i + 1); for (var k = 0; k < i; k++) { //取首碼 prefix[k] = newStr.slice(0, k + 1); suffix[k] = newStr.slice(-k - 1); if (prefix[k] == suffix[k]) { partMatch = prefix[k].length; } } if (!partMatch) { partMatch = 0; } return partMatch; }

  其實跟匹配表是一樣的,去掉了迴圈直接定位到當前已成功匹配的串了

  完整的KMP.next演算法

  ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 <!doctype html><div id="testnext"><div><script type="text/javascript">   function next(str) { var prefix = []; var suffix = []; var partMatch; var i = str.length var newStr = str.substring(0, i + 1); for (var k = 0; k < i; k++) { //取首碼 prefix[k] = newStr.slice(0, k + 1); suffix[k] = newStr.slice(-k - 1); if (prefix[k] == suffix[k]) { partMatch = prefix[k].length; } } if (!partMatch) { partMatch = 0; } return partMatch; }   function KMP(sourceStr, searchStr) { var sourceLength = sourceStr.length; var searchLength = searchStr.length; var result; var i = 0; var j = 0;   for (; i < sourceStr.length; i++) { //最外層迴圈,主串   //子迴圈 for (var j = 0; j < searchLength; j++) { //如果與主串匹配 if (searchStr.charAt(j) == sourceStr.charAt(i)) { //如果是匹配完成 if (j == searchLength - 1) { result = i - j; break; } else { //如果匹配到了,就繼續迴圈,i++是用來增加主串的下標位 i++; } } else { if (j > 1) { i += i - next(searchStr.slice(0,j)); } else { //移動一位 i = (i - j) } break; } }   if (result || result == 0) { break; } }     if (result || result == 0) { return result } else { return -1; } }   var s = "BBC ABCDAB ABCDABCDABDE"; var t = "ABCDAB";     show('indexOf',function() { return s.indexOf(t) })   show('KMP.next',function() { return KMP(s,t) })   function show(bf_name,fn) { var myDate = +new Date() var r = fn(); var div = document.createElement('div') div.innerHTML = bf_name +'演算法,搜尋位置:' + r + ",耗時" + (+new Date() - myDate) + "ms"; document.getElementById("testnext").appendChild(div); }   </script></div></div>
相關文章

Beyond APAC's No.1 Cloud

19.6% IaaS Market Share in Asia Pacific - Gartner IT Service report, 2018

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。