首頁  >  文章  >  web前端  >  JavaScript中資料結構與演算法(五):經典KMP演算法_javascript技巧

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

WBOY
WBOY原創
2016-05-16 15:54:061634瀏覽

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的方法

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值即可

//子循环
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演算法

<!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算法

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算法

<!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>

git代码下载: https://github.com/JsAaron/data_structure

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn