javascript trie首碼樹使用詳解

來源:互聯網
上載者:User
這次給大家帶來javascript trie首碼樹使用詳解,使用javascript trie首碼樹的注意事項有哪些,下面就是實戰案例,一起來看一下。

引子

Trie樹(來自單詞retrieval),又稱首碼字,單詞尋找樹,字典樹,是一種樹形結構,是一種雜湊樹的變種,是一種用於快速檢索的多叉樹結構。

它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。

Trie的核心思想是空間換時間。利用字串的公用首碼來降低查詢時間的開銷以達到提高效率的目的。

Trie樹也有它的缺點, 假定我們只對字母與數字進行處理,那麼每個節點至少有52+10個子節點。為了節省記憶體,我們可以用鏈表或數組。在JS中我們直接用數組,因為JS的數組是動態,內建最佳化。

基本性質

  1. 根節點不包含字元,除根節點外的每一個子節點都包含一個字元

  2. 從根節點到某一節點。路徑上經過的字元串連起來,就是該節點對應的字串

  3. 每個節點的所有子節點包含的字元都不相同

程式實現

// by 司徒正美class Trie { constructor() {  this.root = new TrieNode(); } isValid(str) {  return /^[a-z1-9]+$/i.test(str); } insert(word) {  // addWord  if (this.isValid(word)) {   var cur = this.root;   for (var i = 0; i < word.length; i++) {    var c = word.charCodeAt(i);    c -= 48; //減少”0“的charCode    var node = cur.son[c];    if (node == null) {     var node = (cur.son[c] = new TrieNode());     node.value = word.charAt(i);     node.numPass = 1; //有N個字串經過它    } else {     node.numPass++;    }    cur = node;   }   cur.isEnd = true; //檣記有字串到此節點已經結束   cur.numEnd++; //這個字串重複次數   return true;  } else {   return false;  } } remove(word){   if (this.isValid(word)) {     var cur = this.root;     var array = [], n = word.length     for (var i = 0; i < n; i++) {       var c = word.charCodeAt(i);       c = this.getIndex(c)       var node = cur.son[c];       if(node){         array.push(node)         cur = node       }else{         return false       }      }     if(array.length === n){       array.forEach(function(){         el.numPass--       })       cur.numEnd --       if( cur.numEnd == 0){         cur.isEnd = false       }      }   }else{     return false   } } preTraversal(cb){//先序遍曆    function preTraversalImpl(root, str, cb){       cb(root, str);      for(let i = 0,n = root.son.length; i < n; i ++){        let node = root.son[i];        if(node){          preTraversalImpl(node, str + node.value, cb);        }      }    }     preTraversalImpl(this.root, "", cb);  } // 在字典樹中尋找是否存在某字串為首碼開頭的字串(包括前置詞字元串本身) isContainPrefix(word) {  if (this.isValid(word)) {   var cur = this.root;   for (var i = 0; i < word.length; i++) {    var c = word.charCodeAt(i);    c -= 48; //減少”0“的charCode    if (cur.son[c]) {     cur = cur.son[c];    } else {     return false;    }   }   return true;  } else {   return false;  } } isContainWord(str) {  // 在字典樹中尋找是否存在某字串(不為首碼)  if (this.isValid(word)) {   var cur = this.root;   for (var i = 0; i < word.length; i++) {    var c = word.charCodeAt(i);    c -= 48; //減少”0“的charCode    if (cur.son[c]) {     cur = cur.son[c];    } else {     return false;    }   }   return cur.isEnd;  } else {   return false;  } } countPrefix(word) {  // 統計以指定字串為首碼的字串數量  if (this.isValid(word)) {   var cur = this.root;   for (var i = 0; i < word.length; i++) {    var c = word.charCodeAt(i);    c -= 48; //減少”0“的charCode    if (cur.son[c]) {     cur = cur.son[c];    } else {     return 0;    }   }   return cur.numPass;  } else {   return 0;  } } countWord(word) {  // 統計某字串出現的次數方法  if (this.isValid(word)) {   var cur = this.root;   for (var i = 0; i < word.length; i++) {    var c = word.charCodeAt(i);    c -= 48; //減少”0“的charCode    if (cur.son[c]) {     cur = cur.son[c];    } else {     return 0;    }   }   return cur.numEnd;  } else {   return 0;  } }}class TrieNode { constructor() {  this.numPass = 0;//有多少個單詞經過這節點  this.numEnd = 0; //有多少個單詞就此結束  this.son = [];  this.value = ""; //value為單個字元  this.isEnd = false; }}

我們重點看一下TrieNode與Trie的insert方法。 由於字典樹是主要用在詞頻統計,因此它的節點屬性比較多, 包含了numPass, numEnd但非常重要的屬性。

insert方法是用於插入重詞,在開始之前,我們必須判定單詞是否合法,不能出現 特殊字元與空白。在插入時是打散了一個個字元放入每個節點中。每經過一個節點都要修改numPass。

最佳化

現在我們每個方法中,都有一個c=-48的操作,其實數字與大寫字母與小寫字母間其實還有其他字元的,這樣會造成無謂的空間的浪費

// by 司徒正美getIndex(c){   if(c < 58){//48-57     return c - 48   }else if(c < 91){//65-90     return c - 65 + 11   }else {//> 97      return c - 97 + 26+ 11   } }

然後相關方法將c-= 48改成c = this.getIndex(c)即可

測試

var trie = new Trie();   trie.insert("I");   trie.insert("Love");   trie.insert("China");   trie.insert("China");   trie.insert("China");   trie.insert("China");   trie.insert("China");   trie.insert("xiaoliang");   trie.insert("xiaoliang");   trie.insert("man");   trie.insert("handsome");   trie.insert("love");   trie.insert("Chinaha");   trie.insert("her");   trie.insert("know");   var map = {}  trie.preTraversal(function(node, str){    if(node.isEnd){     map[str] = node.numEnd    }  })  for(var i in map){    console.log(i+" 出現了"+ map[i]+" 次")  }  console.log("包含Chin(包括本身)首碼的單詞及出現次數:");   //console.log("China")  var map = {}  trie.preTraversal(function(node, str){    if(str.indexOf("Chin") === 0 && node.isEnd){      map[str] = node.numEnd    }   })  for(var i in map){    console.log(i+" 出現了"+ map[i]+" 次")  }

Trie樹和其它資料結構的比較

Trie樹與二叉搜尋樹

二叉搜尋樹應該是我們最早接觸的樹結構了,我們知道,資料規模為n時,二叉搜尋樹插入、尋找、刪除操作的時間複雜度通常只有O(log n),最壞情況下整棵樹所有的節點都只有一個子節點,退變成一個線性表,此時插入、尋找、刪除操作的時間複雜度是O(n)。

通常情況下,Trie樹的高度n要遠大於搜尋字串的長度m,故尋找操作的時間複雜度通常為O(m),最壞情況下的時間複雜度才為O(n)。很容易看出,Trie樹最壞情況下的尋找也快過二叉搜尋樹。

文中Trie樹都是拿字串舉例的,其實它本身對key的適宜性是有嚴格要求的,如果key是浮點數的話,就可能導致整個Trie樹巨長無比,節點可讀性也非常差,這種情況下是不適宜用Trie樹來儲存資料的;而二叉搜尋樹就不存在這個問題。

Trie樹與Hash表

考慮一下Hash衝突的問題。Hash表通常我們說它的複雜度是O(1),其實嚴格說起來這是接近完美的Hash表的複雜度,另外還需要考慮到hash函數本身需要遍曆搜尋字串,複雜度是O(m)。在不同鍵被映射到“同一個位置”(考慮closed hashing,這“同一個位置”可以由一個普通鏈表來取代)的時候,需要進行尋找的複雜度取決於這“同一個位置”下節點的數目,因此,在最壞情況下,Hash表也是可以成為一張單向鏈表的。

Trie樹可以比較方便地按照key的字母序來排序(整棵樹先序遍曆一次就好了),這跟絕大多數Hash表是不同的(Hash表一般對於不同的key來說是無序的)。

在較理想的情況下,Hash表可以以O(1)的速度迅速命中目標,如果這張表非常大,需要放到磁碟上的話,Hash表的尋找訪問在理想情況下只需要一次即可;但是Trie樹訪問磁碟的數目需要等於節點深度。

很多時候Trie樹比Hash表需要更多的空間,我們考慮這種一個節點存放一個字元的情況的話,在儲存一個字串的時候,沒有辦法把它儲存成一個單獨的塊。Trie樹的節點壓縮可以明顯緩解這個問題,後面會講到。

Trie樹的改進

按位Trie樹(Bitwise Trie)

原理上和普通Trie樹差不多,只不過普通Trie樹儲存的最小單位是字元,但是Bitwise Trie存放的是位而已。位元據的存取由CPU指令一次直接實現,對於位元據,它理論上要比普通Trie樹快。

節點壓縮。

分支壓縮:對於穩定的Trie樹,基本上都是尋找和讀取操作,完全可以把一些分支進行壓縮。例如,前圖中最右側分支的inn可以直接壓縮成一個節點“inn”,而不需要作為一棵常規的子樹存在。Radix樹就是根據這個原理來解決Trie樹過深問題的。

節點映射表:這種方式也是在Trie樹的節點可能已經幾乎完全確定的情況下採用的,針對Trie樹中節點的每一個狀態,如果狀態總數重複很多的話,通過一個元素為數位多維陣列(比如Triple Array Trie)來表示,這樣儲存Trie樹本身的空間開銷會小一些,雖說引入了一張額外的映射表。

首碼樹的應用

首碼樹還是很好理解,它的應用也是非常廣的。

(1)字串的快速檢索

字典樹的查詢時間複雜度是O(logL),L是字串的長度。所以效率還是比較高的。字典樹的效率比hash表高。

(2)字串排序

從我們很容易看出單詞是排序的,先遍曆字母序在前面。減少了沒必要的公用子串。

(3)最長公用首碼

inn和int的最長公用首碼是in,遍曆字典樹到字母n時,此時這些單詞的公用首碼是in。

(4)自動匹配首碼顯示尾碼

我們使用辭典或者是搜尋引擎的時候,輸入appl,後面會自動顯示一堆首碼是appl的東東吧。那麼有可能是通過字典樹實現的,前面也說了字典樹可以找到公用首碼,我們只需要把剩餘的尾碼遍曆顯示出來即可。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援指令碼之家。

相信看了本文案例你已經掌握了方法,更多精彩請關注php中文網其它相關文章!

推薦閱讀:

Angular2 父子組件通訊方式

jQuery代碼最佳化方式的總結

360瀏覽器安全色模式的頁面顯示不全怎麼處理

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.