【從蛋殼到滿天飛】JS 數據結構解析和算法實現-Trie字典樹

思惟導圖

前言

【從蛋殼到滿天飛】JS 數據結構解析和算法實現,所有文章大概的內容以下: Arrays(數組)、Stacks(棧)、Queues(隊列)、LinkedList(鏈表)、Recursion(遞歸思想)、BinarySearchTree(二分搜索樹)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(優先隊列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(哈希表)html

源代碼有三個:ES6(單個單個的 class 類型的 js 文件) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)java

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。node

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。python

Trie 字典樹 前綴樹

  1. Trie 這種樹結構和以前的二分搜索樹、堆、線段樹不同,
    1. 以前的樹結構本質都是一棵二叉樹,
    2. 而 Trie 是一種奇特的 n 叉樹,這個 n 是大於 2 的,
    3. 這種 n 叉樹能夠很是快速的處理和字符串相關的問題,
    4. 也就是字典樹。
  2. 在有些語言中將映射 Map 看成是字典
    1. 字典其實表示的是
    2. 一個詞條與一段釋意相對應 的這樣的一種結構。
  3. Trie 是專門的真正的爲字典設計的數據結構
    1. 由於一般 Trie 只會用來處理字符串,
    2. 而映射 Map 來講,從一個對象映射到另一個對象,
    3. 這個對象不必定是字符串,而對於 Trie 這種數據結構,
    4. 它是專門爲處理字符串設計的。
  4. 相關場景
    1. 一個字典中有 n 個條目的話,
    2. 若是使用映射 Map 這種數據結構,它的底層是一個二分搜索樹之類的樹結構,
    3. 查詢操做的時間複雜度是O(logn)這個級別,
    4. 二分搜索樹可能還會退化成一個鏈表,可是使用平衡二叉樹就不會這樣,
    5. 其實O(logn)這個級別已經很是高效了,可是若是字典中有 100 萬個條目,
    6. 大概就是(2^20),那麼 logn 大約是 20。
    7. 可是若是使用 Trie 這種數據結構,
    8. 那麼就能夠作到查詢每一個條目的時間複雜度和字典中具體有多少個條目毫無相關,
    9. 會和你查詢的這個字符串的長度相關,那麼時間複雜度是O(w)這個級別,
    10. w 爲查詢的那個單詞的長度,若是你查詢的字符串有 10 個字符,
    11. 那麼使用 trie 查詢的時間複雜度就是 10,若是你查詢的字符串有 3 個字符的話,
    12. 那麼使用 trie 查詢的時間複雜度就是 3,這樣一來對於大多數英語單詞來講就有優點了,
    13. 由於絕大多數單詞的長度小於 10。
  5. trie 的初級原理
    1. 以前的映射 Map 存儲單詞的時候是將整個字符串看做是一個總體,
    2. 可是 trie 打破了這個思路,它將整個字符串以字母爲單位一個一個拆開,
    3. 從根節點開始,一直到葉子節點去遍歷,每遍歷到一個葉子節點就造成了一個單詞,
    4. 查詢任何一個單詞,從根節點出發,
    5. 只須要通過這個單詞有多少個字母就相應的通過了多少個節點最終到達葉子節點,
    6. 這樣就成功的查找了單詞,這樣的一種數據結構就叫作 Trie,很是的好理解。
  6. trie 的節點定義
    1. 每個節點有 26 個指向下一個節點的指針,這是由於在英文的字母表中一共有 26 個字母,
    2. 從根節點開始出發,相應的他有 26 棵子樹,
    3. 每一棵子樹表明的是從一個不一樣的字母開始的這樣的一個新的子樹,
    4. 在 trie 中節點的定義大概是這個樣子。
      class TrieNode {
         c; // char
         next; // Array(26)
      }
      複製代碼
  7. 這樣的節點的定義會遇到這樣的問題
    1. 語言的不一樣和情景的不一樣,有可能 26 個指針是富於的,也有可能 26 個指針是不夠的,
    2. 可是每個節點下面跟 26 個孩子,在這裏就沒有考慮大小寫的問題,
    3. 不過你要設計的 trie 要考慮大小寫的問題,相應的就須要有 52 個指針,
    4. 可是你的 trie 你裝載的內容更加複雜的話,好比你裝載的內容是網址或者是郵件地址,
    5. 這一類的字符串,那麼相應的有一些字符也應該計算在內,好比@:/\_-等等,
    6. 正由於這個緣由,若是你想設計一個更靈活的 trie,
    7. 一般不會固定每個節點只有 26 個指向下一個節點的指針,
    8. 除非你很是確定這個 trie 所處理的內容只包含全部小寫的英文字母,
    9. 一般會讓每個節點有若干個指向下一個節點的指針,將 26 改爲若干,
    10. 也就是將靜態的節點數改爲了動態的節點數,這是一種動態的思想,
    11. 要實現這種動態的思想,那麼那個 next 就可使用一個映射 Map 來實現,
    12. 實際上這個 next 就是指 一個 char 和一個 Node 之間的映射,
    13. 這個 Map 中存多少個映射實際上是不知道的,
    14. 可是每個映射 Map 中必定都是一個字符到一個新的節點這樣的一個映射,
    15. 本身實現的映射這種數據結構,在這裏又使用上了,
    16. 也可使用 系統內置的 Map,由於本身實現的映射 Map 的底層是二分搜索樹,
    17. 二分搜索樹在最壞的狀況下會退化爲一個鏈表,而 Map 是使用改良後的平衡的二叉樹,
    18. 並且 系統內置的 Map 的底層是紅黑樹,因此性能相對來講會好一些。
    class TrieNode {
       c; // char
       next; // Map
    }
    複製代碼
  8. trie 的中級原理
    1. 其實從根節點找到下一個節點的過程當中,其實你就已經知道這個字母是誰了,
    2. 是由於你在根節點就知道了你下一個節點要到指定的那個節點,
    3. 因此更準確的來講,其實在你來到這個節點以前就已經知道這個字母具體是什麼,
    4. 纔可能經過這個映射 Map 來找到下一個節點,因此在這個節點的實現中,
    5. 你不存儲這個char c是沒有問題的,在 trie 中添加或者查詢某一個單詞的時候,
    6. 不存儲這個char c是沒有問題的,
    7. 由於在這個映射 Map 中已經有了從某一個 char 到某一個節點這樣相應一個條目,
    8. 直接經過這個條目來到下一個節點,
    9. 那麼你天然就知道了這個節點對應的就是映射裏相應的那個字符,
    10. 有可能在有一些情境下在節點中存這樣一個char c
    11. 能夠幫你更快的組織這個邏輯,這一點了解便可。
    class TrieNode {
       next; // Map
    }
    複製代碼
  9. 另一個很關鍵的問題是
    1. trie 查詢一個單詞都是從根節點出發一直到葉子節點,
    2. 到了葉子節點的時候,就到了一個單詞的地方,
    3. 不過在英語單詞世界中不少單詞多是另一個單詞的前綴,
    4. 例如平底鍋 pan 和熊貓 panda,你即要存 pan 又要存 panda,
    5. 此時對於 pan 這個單詞結尾的這個 n 並非一個葉子節點,
    6. 否則就沒有辦法存 panda 這個單詞了,正由於如此,
    7. 對於每個 node 就須要一個標識,
    8. 用這個標識來標明當前這個節點是不是某一個單詞的結尾,
    9. 某一個單詞的結尾只靠葉子節點是不能區分出來的,
    10. 那麼 pan 這個單詞至關因而 panda 這個單詞的一個前綴,
    11. 這樣一來就須要多添加一個 bool 值boolean isWord
    12. 表示的是 當前這個節點是否表明了一個單詞的結尾,
    13. 也就是訪問到了當前這個節點以後,是否訪問到了一個單詞了,
    14. 這樣就將 Trie 中每個節點進行了相應的一個定義,
    15. 節點的定義最複雜的部分在於這個 next,
    16. 由於這個 next 又是一個映射,至關於每個節點裏面其實
    17. 都蘊含一個相對比較複雜的一個數據結構來支撐這個節點的運行,
    18. 其實這也是數據結構自己的魅力,就是這樣一點一點的從最底層
    19. 開始像搭積木同樣逐漸的搭出更加複雜的數據結構,
    20. 而已經搭建好的這些結構封裝好了以後,就能夠很是簡單的複用,
    21. 對於上層用戶來講徹底屏蔽了底層的實現細節。
    class TrieNode {
       isWord; // Boolean
       next; // Map
    }
    複製代碼

Trie 字典樹 簡單實現及添加操做

  1. 中日韓這些語言體系中對於什麼是單詞這樣的定義是模糊的,
    1. 它不像在英文的語句中單詞和單詞之間直接由空格分開,
    2. 能夠很是清晰的界定什麼是一個單詞,
    3. 因此不少時候對於這種其它的語言體系
    4. 它不是由一個一個字母組成的單詞這樣的語言體系,
    5. 就會有特殊的語言處理的方法,因此瞭解便可。
  2. 本身實現的 Trie 不是泛型的
    1. 主要用於英語,其它中日韓無論它。
  3. 對於 Trie 來講它的本質只是一個多叉樹而已
    1. 和二叉樹是沒有區別的,對於二叉樹來講,
    2. 它有兩個指針,分別是 left 和 right,
    3. 而對於 Trie 來講是有多個,因此才用一個映射來存儲,
    4. 區別只在這裏而已,因此總體添加元素的邏輯和二叉樹是很是像的。
  4. Trie 的添加操做 非遞歸
    1. 其實很是的容易,添加的是一個字符串並非一個字符,
    2. 這是 trie 和二叉樹的一個區別,添加一個字符串,
    3. 是由於要把這個字符串拆成一個一個的字符,
    4. 而後把這一個一個的字符作成一個一個的節點,
    5. 最後再添加進這個樹結構中,就是這樣的一個邏輯。
  5. 原理
    1. 根據單詞中的每個字符建立新的節點,
    2. 每個節點中都有一個 next(映射),
    3. 這個映射中存儲了 指定字符對應的指定節點的信息,
    4. 根節點是不存字符的,從根節點開始,從它的 next 中開始存儲,
    5. 將新添加的單詞進行字符的拆分,這個添加順序是從左到右,
    6. 存儲的順序是從外到內的,映射中有字符對應一個節點,
    7. 這個節點也有映射,因此這是一個嵌套的關係,就像一顆樹,
    8. 存儲完一個單詞後,這個單詞的最後一個字符會被設置一個標記,即表示單詞的結尾。

代碼示例

  1. MyTriec++

    // 自定義字典樹節點 TrieNode
    class MyTrieNode {
       constructor(letterChar, isWord = false) {
          this.letterChar = letterChar;
          this.isWord = isWord; // 是不是單詞
          this.next = new Map(); // 存儲 字符所對應的節點的 字典映射
       }
    }
    
    // 自定義字典樹 Trie
    class MyTrie {
       constructor() {
          this.root = new MyTrieNode();
          this.size = 0;
       }
    
       // 向Trie中添加一個新的單詞word
       add(word) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             this.size++;
          }
       }
    
       // 向Trie中添加一個新的單詞word 遞歸算法
       recursiveAdd(word) {
          this.recursiveAddFn(this.root, word, 0);
       }
    
       // 向Trie中添加一個新的單詞word 遞歸輔助函數
       recursiveAddFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) {
             if (!node.isWord) {
                node.isWord = true;
                this.size++;
             }
             return;
          }
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空就添加
          if (!map.has(letterChar))
             map.set(letterChar, new MyTrieNode(letterChar));
          recursiveAddFn(map.get(letterChar), word, index + 1);
       }
    }
    複製代碼

Trie 字典樹 查詢操做

  1. 字典樹中不會去添加劇復的單詞,
    1. 這和以前本身實現的集合 Set 很像,
    2. 只不過對於 Trie 來講,
    3. 它是一個只可以存儲字符串這樣的元素的相應的集合
    4. 而以前基於二分搜索樹實現的集合,能夠存儲任意元素,
    5. 更準確的來講,是可比較大小的這樣的元素,
    6. 相應的集合 Set 均可以進行存儲。
  2. 集合 Set 和映射 Map 之間相應的是有聯繫的
    1. 若是是在 TrieNode 中再設置一個屬性,
    2. 這個屬性就是該字符串的特殊意義,如存放詞頻數等等,
    3. 那樣 Trie 就被改形成了一個映射了。

讓目前這個 Trie 與集合 Set 進行對比

  1. 使用 Trie 實現一個 TrieSet
    1. 與鏈表 Set 和二分搜索樹 Set 對比後,
    2. TrieSet 性能相對來講比較好,
    3. 當你添加到 set 中的數據越多,
    4. 那麼 TrieSet 的性能相對來講就越好。
  2. 在 trie 中添加字符串和查詢字符串
    1. 與 trie 中有多少個元素有多少個字符串是沒有關係的,
    2. 只和你添加的那個字符串和你查找的那個字符串的長度有關,
    3. 若是你添加的字符串總體都比較短的話,
    4. 那麼在一個大的集合中使用 Trie 就會有很是高的性能優點。

代碼示例

  1. (class: MyTrie, class: MyTrieSet, class: Main)git

  2. MyTriegithub

    // 自定義字典樹節點 TrieNode
    class MyTrieNode {
       constructor(letterChar, isWord = false) {
          this.letterChar = letterChar;
          this.isWord = isWord; // 是不是單詞
          this.next = new Map(); // 存儲 字符所對應的節點的 字典映射
       }
    }
    
    // 自定義字典樹 Trie
    class MyTrie {
       constructor() {
          this.root = new MyTrieNode();
          this.size = 0;
       }
    
       // 向Trie中添加一個新的單詞word
       add(word) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             this.size++;
          }
       }
    
       // 向Trie中添加一個新的單詞word 遞歸算法
       recursiveAdd(word) {
          this.recursiveAddFn(this.root, word, 0);
       }
    
       // 向Trie中添加一個新的單詞word 遞歸輔助函數 -
       recursiveAddFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) {
             if (!node.isWord) {
                node.isWord = true;
                this.size++;
             }
             return;
          }
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空就添加
          if (!map.has(letterChar))
             map.set(letterChar, new MyTrieNode(letterChar));
          this.recursiveAddFn(map.get(letterChar), word, index + 1);
       }
    
       // 查詢單詞word是否在Trie中
       contains(word) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (node === null) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 單詞遍歷完畢
          // 返回最後一個字符是不是一個單詞的結尾
          return cur.isWord;
       }
    
       // 查詢單詞word是否在Trie中 遞歸算法
       recursiveContains(word) {
          return this.recursiveContainsFn(this.root, word, 0);
       }
    
       // 查詢單詞word是否在Trie中 遞歸賦值函數 -
       recursiveContainsFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空那麼就說明這個單詞沒有進行存儲
          if (!map.has(letterChar)) return false;
          return this.recursiveContainsFn(map.get(letterChar), word, index + 1);
       }
    
       // 獲取字典樹中存儲的單詞數量
       getSize() {
          return this.size;
       }
    
       // 獲取字典樹中是否爲空
       isEmpty() {
          return this.size === 0;
       }
    }
    複製代碼
  3. MyTrieSet正則表達式

    // 自定義字典集合 TrieSet
    class MyTrieSet {
       constructor() {
          this.trie = new MyTrie();
       }
    
       // 添加操做
       add(word) {
          this.trie.add(word);
       }
    
       // 刪除操做 待實現
       remove(word) {
          return false;
       }
    
       // 查單詞是否存在
       contains(word) {
          return this.trie.contains(word);
       }
    
       // 獲取實際元素個數
       getSize() {
          return this.trie.getSize();
       }
    
       // 獲取當前集合是否爲空
       isEmpty() {
          return this.trie.isEmpty();
       }
    }
    複製代碼
  4. Main算法

    // main 函數
    class Main {
       constructor() {
          this.alterLine('Set Comparison Area');
          const n = 2000000;
    
          const myBSTSet = new MyBinarySearchTreeSet();
          const myTrieSet = new MyTrieSet();
          let performanceTest1 = new PerformanceTest();
    
          const random = Math.random;
          let arr = [];
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) {
             arr.push(i.toString());
          }
    
          this.alterLine('MyBSTSet Comparison Area');
          const myBSTSetInfo = performanceTest1.testCustomFn(function() {
             for (const word of arr) myBSTSet.add(word);
          });
    
          // 總毫秒數:3173
          console.log(myBSTSetInfo);
          this.show(myBSTSetInfo);
    
          this.alterLine('MyTrieSet Comparison Area');
          const myTrieSetInfo = performanceTest1.testCustomFn(function() {
             for (const word of arr) myTrieSet.add(word);
          });
    
          // 總毫秒數:2457
          console.log(myTrieSetInfo);
          this.show(myTrieSetInfo);
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

Trie 字典樹 前綴查詢

  1. 查看 Trie 中是否有包含某個前綴相關的單詞
    1. 很像是模糊搜索,
    2. 例如 panda 這個單詞,p 是 panda 的前綴,pa 也是 panda 的前綴,
    3. 一個單詞的自己也算是這個單詞的前綴。
    4. 由於 Trie 就是這樣的一種數據結構,因此能夠經過前綴來進行查詢,
    5. 查當前所存儲的全部的單詞中,
    6. 是否有某一個前綴對應的這樣一個單詞。
  2. 對於 Trie 這樣的一個數據結構來講
    1. 能夠以很是高效的性能,
    2. 也就是以這個前綴的長度的時間複雜度,
    3. 在一個集合中是否可以直接找到以這個字符串爲前綴的單詞,
    4. 因此 Trie 又叫作前綴樹。
  3. 對於前綴查詢這樣的一個操做
    1. 若是使用基於二分搜索樹實現的集合來完成的話,
    2. 相應的就會複雜不少,要先查詢這個前綴是否是一個單詞,
    3. 而後遍歷每一個二分搜索樹 Set 中的單詞,
    4. 最後之前綴字符串的長度進行逐個對比,時間複雜度是 n 方的級別。

leetcode 上前綴樹的題目

  1. 208.實現 Trie (前綴樹)編程

    1. https://leetcode-cn.com/problems/implement-trie-prefix-tree/
    2. 這個就是本身實現的前綴樹,
    3. 第一個版本是映射 Map 版,是動態的
    4. 第二個版本是數組版,是靜態的
  2. Trie

    // 答題
    class Solution {
       // leetcode 208.實現 Trie (前綴樹)
       Trie() {
          // 數組版的Trie 靜態Trie
          function ArrayTrie() {
             // TrieNode
             var TrieNode = function(isWord = false) {
                this.isWord = isWord;
                this.next = new Array(26);
             };
    
             /** * Initialize your data structure here. */
             var Trie = function() {
                this.root = new TrieNode();
             };
    
             /** * Inserts a word into the trie. * @param {string} word * @return {void} */
             Trie.prototype.insert = function(word) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of word) {
                   const index = c.charCodeAt(0) - 97;
                   const array = cur.next;
                   if (array[index] === null || array[index] === undefined)
                      array[index] = new TrieNode();
                   cur = array[index];
                }
    
                if (!cur.isWord) cur.isWord = true;
             };
    
             /** * Returns if the word is in the trie. * @param {string} word * @return {boolean} */
             Trie.prototype.search = function(word) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of word) {
                   const index = c.charCodeAt(0) - 97;
                   const array = cur.next;
                   if (array[index] === null || array[index] === undefined)
                      return false;
                   cur = array[index];
                }
    
                return cur.isWord;
             };
    
             /** * Returns if there is any word in the trie that starts with the given prefix. * @param {string} prefix * @return {boolean} */
             Trie.prototype.startsWith = function(prefix) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of prefix) {
                   const index = c.charCodeAt(0) - 97;
                   const array = cur.next;
                   if (array[index] === null || array[index] === undefined)
                      return false;
                   cur = array[index];
                }
    
                return true;
             };
    
             /** * Your Trie object will be instantiated and called as such: * var obj = Object.create(Trie).createNew() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */
    
             return new Trie();
          }
    
          // 映射版的Trie 動態Trie
          function MapTrie() {
             // TrieNode
             var TrieNode = function(isWord = false) {
                this.isWord = isWord;
                this.next = new Map();
             };
             /** * Initialize your data structure here. */
             var Trie = function() {
                this.root = new TrieNode();
             };
    
             /** * Inserts a word into the trie. * @param {string} word * @return {void} */
             Trie.prototype.insert = function(word) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of word) {
                   const map = cur.next;
                   if (!map.has(c)) map.set(c, new TrieNode());
                   cur = map.get(c);
                }
    
                if (!cur.isWord) cur.isWord = true;
             };
    
             /** * Returns if the word is in the trie. * @param {string} word * @return {boolean} */
             Trie.prototype.search = function(word) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of word) {
                   const map = cur.next;
                   if (!map.has(c)) return false;
                   cur = map.get(c);
                }
    
                return cur.isWord;
             };
    
             /** * Returns if there is any word in the trie that starts with the given prefix. * @param {string} prefix * @return {boolean} */
             Trie.prototype.startsWith = function(prefix) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of prefix) {
                   const map = cur.next;
                   if (!map.has(c)) return false;
                   cur = map.get(c);
                }
    
                return true;
             };
    
             /** * Your Trie object will be instantiated and called as such: * var obj = Object.create(Trie).createNew() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */
    
             return new Trie();
          }
    
          // return new ArrayTrie();
          return new MapTrie();
       }
    }
    複製代碼
  3. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('leetcode 208.實現 Trie (前綴樹)');
    
          let s = new Solution();
          let trie = s.Trie();
          this.show(trie.insert('apple') + '');
          this.show(trie.search('apple') + ' // 返回 true'); // 返回 true
          this.show(trie.search('app') + '// 返回 false'); // 返回 false
          this.show(trie.startsWith('app') + '// 返回 true'); // 返回 true
          this.show(trie.insert('app') + '');
          this.show(trie.search('app') + '// 返回 true'); // 返回 true
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

代碼示例

// 自定義字典樹 Trie
class MyTrie {
   constructor() {
      this.root = new MyTrieNode();
      this.size = 0;
   }

   // 向Trie中添加一個新的單詞word
   add(word) {
      // 指定遊標
      let cur = this.root;
      // 遍歷出當前單詞的每個字符
      for (const c of word) {
         // 下一個字符所對應的映射是否爲空
         if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c));
         // 切換到下一個節點
         cur = cur.next.get(c);
      }

      // 若是當前這個單詞是一個新的單詞
      if (!cur.isWord) {
         // 當前這個字符是這個單詞的結尾
         cur.isWord = true;
         this.size++;
      }
   }

   // 向Trie中添加一個新的單詞word 遞歸算法
   recursiveAdd(word) {
      this.recursiveAddFn(this.root, word, 0);
   }

   // 向Trie中添加一個新的單詞word 遞歸輔助函數 -
   recursiveAddFn(node, word, index) {
      // 解決基本的問題,由於已經到底了
      if (index === word.length) {
         if (!node.isWord) {
            node.isWord = true;
            this.size++;
         }
         return;
      }

      const map = node.next; // 獲取節點的next 也就是字符對應的映射
      const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
      // 下一個字符所對應的映射是否爲空 爲空就添加
      if (!map.has(letterChar)) map.set(letterChar, new MyTrieNode(letterChar));
      this.recursiveAddFn(map.get(letterChar), word, index + 1);
   }

   // 查詢單詞word是否在Trie中
   contains(word) {
      // 指定遊標
      let cur = this.root;

      // 遍歷出當前單詞的每個字符
      for (const c of word) {
         // 獲取當前這個字符所對應的節點
         const node = cur.next.get(c);
         // 這個節點不存在,那麼就說明就沒有存儲這個字符
         if (node === null) return false;
         // 遊標切換到這個節點
         cur = node;
      }

      // 單詞遍歷完畢
      // 返回最後一個字符是不是一個單詞的結尾
      return cur.isWord;
   }

   // 查詢單詞word是否在Trie中 遞歸算法
   recursiveContains(word) {
      return this.recursiveContainsFn(this.root, word, 0);
   }

   // 查詢單詞word是否在Trie中 遞歸賦值函數 -
   recursiveContainsFn(node, word, index) {
      // 解決基本的問題,由於已經到底了
      if (index === word.length) return node.isWord;

      const map = node.next; // 獲取節點的next 也就是字符對應的映射
      const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
      // 下一個字符所對應的映射是否爲空 爲空那麼就說明這個單詞沒有進行存儲
      if (!map.has(letterChar)) return false;
      return this.recursiveContainsFn(map.get(letterChar), word, index + 1);
   }

   // 查詢在Trie中是否有單詞以 prefix 爲前綴
   isPrefix(prefix) {
      // 指定遊標
      let cur = this.root;

      // 遍歷出當前單詞的每個字符
      for (const c of prefix) {
         // 獲取當前這個字符所對應的節點
         const node = cur.next.get(c);
         // 這個節點不存在,那麼就說明就沒有存儲這個字符
         if (node === null) return false;
         // 遊標切換到這個節點
         cur = node;
      }

      // 前綴遍歷完畢 說明這個前綴有單詞與之匹配
      return true;
   }

   // 獲取字典樹中存儲的單詞數量
   getSize() {
      return this.size;
   }

   // 獲取字典樹中是否爲空
   isEmpty() {
      return this.size === 0;
   }
}
複製代碼

Trie 字典樹 簡單的模式匹配

  1. match 方法,是一個遞歸函數
    1. 有三個參數,
    2. 第一個參數是當前的節點,
    3. 第二個參數是字符串,
    4. 第三個參數是 當前匹配的字符的索引
  2. match 方法邏輯
    1. 先分紅兩種狀況,一種是遞歸到底的狀況,
    2. 一種是沒有遞歸到底就去調用這個遞歸相應的邏輯。
    3. 在調用遞歸的這個邏輯中,對當前考慮的這個字符進行判斷,
    4. 一種是當前這個字符等於正則表達式通配符號的.
    5. 另外一種是當前的這個字符不等於正則表達式通配符號的.
    6. 若是不等於.,那麼就很簡單,
    7. 直接查看這個字符對應的 TrieNode 是否爲 null,
    8. 若是爲 null 的話說明匹配失敗,直接返回 false,
    9. 不然就繼續以 return 的方式調用 match 函數,
    10. 傳入的參數進行一下變動,下一個節點、字符串、下一個要匹配的字符的索引。
    11. 若是等於.,那麼就相對來講複雜一點,
    12. 須要對當前節點下一個字符的全部可能都去進行一下匹配,
    13. 也就是遍歷當前節點的下一個映射 Map 中的全部 key,
    14. 也就是當前節點的`next.keys(),每遍歷到每個字符的時候都要作一下判斷,
    15. 判斷的方式是調用 match 方法,傳入的參數也是同樣進行變動,
    16. 下一個節點、字符串、下一個要匹配的字符的索引,
    17. 目的是爲了看看當前字符所對應的下一個節點是否可以匹配成功,
    18. 若是匹配成功就直接返回 true,不然全部遍歷的字符的下一個節點都匹配失敗的話,
    19. 那麼就返回 false,只要遍歷的字符中有一個下一個節點匹配成功就算匹配成功。
    20. 遞歸到底的條件是 當前匹配的字符的索引等於這個字符串的長度。

leetcode 上的題目

  1. 211.添加與搜索單詞 - 數據結構設計

    1. https://leetcode-cn.com/problems/add-and-search-word-data-structure-design/
    2. 和 實現前綴樹那道題差很少,
    3. 第一個版本是使用映射 Map 來實現的
    4. 第二個版本是使用數組來實現的
  2. WordDictionary

    // 答題
    class Solution {
       // leetcode 211.添加與搜索單詞 - 數據結構設計
       WordDictionary() {
          // 數組版
          function ArrayWordDictionary() {
             // TrieNode
             var TrieNode = function() {
                this.isWord = false;
                this.next = new Array(26);
             };
    
             /** * Initialize your data structure here. */
             var WordDictionary = function() {
                this.root = new TrieNode();
             };
    
             /** * Adds a word into the data structure. * @param {string} word * @return {void} */
             WordDictionary.prototype.addWord = function(word) {
                // 指定遊標
                let cur = this.root;
    
                for (const c of word) {
                   const index = c.charCodeAt(0) - 97;
                   const array = cur.next;
                   if (!array[index]) array[index] = new TrieNode();
                   cur = array[index];
                }
    
                if (!cur.isWord) cur.isWord = true;
             };
    
             /** * Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. * @param {string} word * @return {boolean} */
             WordDictionary.prototype.search = function(word) {
                return this.recursiveMatch(this.root, word, 0);
             };
    
             // 遞歸搜索
             WordDictionary.prototype.recursiveMatch = function( node, word, index ) {
                if (index === word.length) return node.isWord;
    
                const letterChar = word[index];
    
                if (letterChar !== '.') {
                   const i = letterChar.charCodeAt(0) - 97;
    
                   if (!node.next[i]) return false;
                   return this.recursiveMatch(node.next[i], word, index + 1);
                } else {
                   for (const next of node.next) {
                      if (next === undefined) continue;
                      if (this.recursiveMatch(next, word, index + 1))
                         return true;
                   }
                   return false;
                }
             };
    
             /** * Your WordDictionary object will be instantiated and called as such: * var obj = Object.create(WordDictionary).createNew() * obj.addWord(word) * var param_2 = obj.search(word) */
             return new WordDictionary();
          }
    
          // 映射版
          function MapWordDictionary() {
             // TrieNode
             var TrieNode = function(isWord = false) {
                this.isWord = isWord;
                this.next = new Map();
             };
             /** * Initialize your data structure here. */
             var WordDictionary = function() {
                this.root = new TrieNode();
             };
    
             /** * Adds a word into the data structure. * @param {string} word * @return {void} */
             WordDictionary.prototype.addWord = function(word) {
                let cur = this.root;
    
                for (const c of word) {
                   if (!cur.next.has(c)) cur.next.set(c, new TrieNode());
                   cur = cur.next.get(c);
                }
    
                if (!cur.isWord) cur.isWord = true;
             };
    
             /** * Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. * @param {string} word * @return {boolean} */
             WordDictionary.prototype.search = function(word) {
                return this.recursiveMatch(this.root, word, 0);
             };
             WordDictionary.prototype.recursiveMatch = function( node, word, index ) {
                if (index === word.length) return node.isWord;
    
                const letterChar = word[index];
                if (letterChar !== '.') {
                   const map = node.next;
                   if (!map.has(letterChar)) return false;
                   return this.recursiveMatch(
                      map.get(letterChar),
                      word,
                      index + 1
                   );
                } else {
                   const map = node.next;
                   const keys = map.keys();
                   for (const key of keys)
                      if (this.recursiveMatch(map.get(key), word, index + 1))
                         return true;
                   return false;
                }
             };
    
             /** * Your WordDictionary object will be instantiated and called as such: * var obj = Object.create(WordDictionary).createNew() * obj.addWord(word) * var param_2 = obj.search(word) */
             return new WordDictionary();
          }
    
          // return new ArrayWordDictionary();
          return new MapWordDictionary();
       }
    }
    複製代碼
  3. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('leetcode 208. 實現 Trie (前綴樹)');
    
          let trie = new MyTrie();
          this.show(trie.add('apple') + '');
          this.show(trie.contains('apple') + ' // 返回 true'); // 返回 true
          this.show(trie.contains('app') + '// 返回 false'); // 返回 false
          this.show(trie.isPrefix('app') + '// 返回 true'); // 返回 true
          this.show(trie.add('app') + '');
          this.show(trie.contains('app') + '// 返回 true'); // 返回 true
    
          this.alterLine('leetcode 211. 添加與搜索單詞 - 數據結構設計');
    
          trie = new MyTrie();
          this.show(trie.add('bad') + '');
          this.show(trie.add('dad') + '');
          this.show(trie.add('mad') + '');
          this.show(trie.regexpSearch('pad') + '-> false'); //-> false
          this.show(trie.regexpSearch('bad') + '-> true'); //-> true
          this.show(trie.regexpSearch('.ad') + '-> true'); //-> true
          this.show(trie.regexpSearch('b..') + '-> true'); //-> true
          this.show(trie.regexpSearch('b....') + '-> false'); //-> false
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

代碼示例

  1. MyTrie

    // 自定義字典樹 Trie
    class MyTrie {
       constructor() {
          this.root = new MyTrieNode();
          this.size = 0;
       }
    
       // 向Trie中添加一個新的單詞word
       add(word) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNode(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             this.size++;
          }
       }
    
       // 向Trie中添加一個新的單詞word 遞歸算法
       recursiveAdd(word) {
          this.recursiveAddFn(this.root, word, 0);
       }
    
       // 向Trie中添加一個新的單詞word 遞歸輔助函數 -
       recursiveAddFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) {
             if (!node.isWord) {
                node.isWord = true;
                this.size++;
             }
             return;
          }
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空就添加
          if (!map.has(letterChar))
             map.set(letterChar, new MyTrieNode(letterChar));
          this.recursiveAddFn(map.get(letterChar), word, index + 1);
       }
    
       // 查詢單詞word是否在Trie中
       contains(word) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (node === null) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 單詞遍歷完畢
          // 返回最後一個字符是不是一個單詞的結尾
          return cur.isWord;
       }
    
       // 查詢單詞word是否在Trie中 遞歸算法
       recursiveContains(word) {
          return this.recursiveContainsFn(this.root, word, 0);
       }
    
       // 查詢單詞word是否在Trie中 遞歸賦值函數 -
       recursiveContainsFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空那麼就說明這個單詞沒有進行存儲
          if (!map.has(letterChar)) return false;
          return this.recursiveContainsFn(map.get(letterChar), word, index + 1);
       }
    
       // 查詢在Trie中是否有單詞以 prefix 爲前綴
       isPrefix(prefix) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of prefix) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (node === null) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 前綴遍歷完畢 說明這個前綴有單詞與之匹配
          return true;
       }
    
       // 正則表達式 查詢單詞word是否在Trie中,目前只支持 統配符 "."
       regexpSearch(regexpWord) {
          return this.match(this.root, regexpWord, 0);
       }
    
       // 正則表達式 匹配單詞 遞歸算法 -
       match(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 判斷這個字符是不是通配符
          if (letterChar !== '.') {
             // 若是映射中不包含這個字符
             if (!map.has(letterChar)) return false;
             // 若是映射中包含這個字符,那麼就去找個字符對應的節點中繼續匹配
             return this.match(map.get(letterChar), word, index + 1);
          } else {
             // 遍歷 下一個字符的集合
             // 若是 從下一個字符繼續匹配,只要匹配成功就返回 true
             for (const key of map.keys())
                if (this.match(map.get(key), word, index + 1)) return true;
             // 遍歷一遍以後仍是沒有匹配成功 那麼就算匹配失敗
             return false;
          }
       }
    
       // 獲取字典樹中存儲的單詞數量
       getSize() {
          return this.size;
       }
    
       // 獲取字典樹中是否爲空
       isEmpty() {
          return this.size === 0;
       }
    }
    複製代碼

Trie 字典樹 字符串映射 Map

  1. leetcode 上的 677 號題目與 211 號題目相似,
    1. 也是須要進行相似模式匹配的模糊查詢,
    2. 若是模糊匹配成功就會返回最終存的 value 值,
    3. 也就是就是說先匹配已明確指示的字符,
    4. 而後再遍歷,若是最後遍歷到底證實那是一個單詞,
    5. 那麼就直接返回該單詞對應的 value 便可。
    6. 添加操做的話直接是覆蓋的操做,
    7. 相同的單詞所定義的映射的 value 直接覆蓋掉便可。
  2. 使用 Trie 實現映射 Map 並非很難,
    1. 其實就是在使用 Trie 實現集合 Set 的基礎上加一個屬性而已,
    2. 每添加一個字符串就在那個字符串最後一個字符的相應節點設置這個屬性的值便可。

leetcode 上的題目

  1. 677. 鍵值映射

    1. https://leetcode-cn.com/problems/map-sum-pairs/
    2. 和 211 號題目相似
    3. 第一個版本是使用映射 Map 來實現的
    4. 第二個版本是使用數組來實現的
  2. MapSum

    // 答題
    class Solution {
       // leetcode 677. 鍵值映射
       MapSum() {
          // 數組版
          function ArrayVersion() {
             var TrieNode = function(value) {
                this.value = value;
                this.next = new Array(26);
             };
    
             /** * Initialize your data structure here. */
             var MapSum = function() {
                this.root = new TrieNode(0);
             };
    
             /** * @param {string} key * @param {number} val * @return {void} */
             MapSum.prototype.insert = function(key, val) {
                this.__insert(this.root, key, val, 0);
             };
             MapSum.prototype.__insert = function(node, word, value, index) {
                if (index === word.length) {
                   node.value = value;
                   return;
                }
    
                const array = node.next;
                const i = word[index].charCodeAt(0) - 97;
                if (!array[i]) array[i] = new TrieNode(0);
                this.__insert(array[i], word, value, index + 1);
             };
    
             /** * @param {string} prefix * @return {number} */
             MapSum.prototype.sum = function(prefix) {
                // 先進行前綴匹配
                let cur = this.root;
                for (const c of prefix) {
                   const index = c.charCodeAt(0) - 97;
                   if (!cur.next[index]) return 0;
                   cur = cur.next[index];
                }
    
                // 前綴匹配成功以後 進行剩餘單詞的匹配 求和
                return this.__sum(cur);
             };
    
             MapSum.prototype.__sum = function(node) {
                let result = node.value || 0;
    
                for (const next of node.next) {
                   if (!next) continue;
                   result += this.__sum(next);
                }
    
                return result;
             };
    
             /** * Your MapSum object will be instantiated and called as such: * var obj = Object.create(MapSum).createNew() * obj.insert(key,val) * var param_2 = obj.sum(prefix) */
    
             return new MapSum();
          }
    
          // 映射版
          function MapVersion() {
             var TrieNode = function(value) {
                this.value = value;
                this.next = new Map();
             };
             /** * Initialize your data structure here. */
             var MapSum = function() {
                this.root = new TrieNode();
             };
    
             /** * @param {string} key * @param {number} val * @return {void} */
             MapSum.prototype.insert = function(key, val) {
                let cur = this.root;
    
                for (const c of key) {
                   const map = cur.next;
                   if (!map.has(c)) map.set(c, new TrieNode());
                   cur = map.get(c);
                }
    
                cur.value = val;
             };
    
             /** * @param {string} prefix * @return {number} */
             MapSum.prototype.sum = function(prefix) {
                // 先處理前綴部分
                let cur = this.root;
    
                for (const c of prefix) {
                   const map = cur.next;
                   if (!map.has(c)) return 0;
                   cur = map.get(c);
                }
    
                return this.__sum(cur);
             };
             MapSum.prototype.__sum = function(node) {
                let result = node.value || 0;
    
                const map = node.next;
                const keys = map.keys();
                for (const key of keys) result += this.__sum(map.get(key));
    
                return result;
             };
    
             /** * Your MapSum object will be instantiated and called as such: * var obj = Object.create(MapSum).createNew() * obj.insert(key,val) * var param_2 = obj.sum(prefix) */
             return new MapSum();
          }
    
          // return new ArrayVersion();
          return new MapVersion();
       }
    }
    複製代碼
  3. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('leetcode 677. 鍵值映射');
          let s = new Solution();
          let trie = s.MapSum();
          this.show(trie.insert('apple', 3) + ' 輸出: Null');
          this.show(trie.sum('ap') + ' 輸出: 3');
          this.show(trie.insert('app', 2) + ' 輸出: Null');
          this.show(trie.sum('ap') + ' 輸出: 5');
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

代碼示例

  1. (class: MyTrieUpgrade, class: MyTrieMap, class: Main)

  2. MyTrie

    // 自定義字典樹節點升級版 TrieNodeUpgrade
    class MyTrieNodeUpgrade {
       constructor(letterChar, element, isWord = false) {
          this.letterChar = letterChar;
          this.element = element; // 升級後能夠存儲特殊數據
          this.isWord = isWord; // 是不是單詞
          this.next = new Map(); // 存儲 字符所對應的節點的 字典映射
       }
    }
    
    // 自定義字典樹升級版 TrieUpgrade
    class MyTrieUpgrade {
       constructor() {
          this.root = new MyTrieNodeUpgrade();
          this.size = 0;
       }
    
       add(word, element) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             // 存儲 額外信息
             cur.element = element;
             this.size++;
          }
       }
    
       // 向Trie中添加一個新的單詞word 而且在word中存儲額外的信息,若是額外信息存在就覆蓋
       put(word, element) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             this.size++;
          }
    
          // 設置或者覆蓋 額外信息
          cur.element = element;
       }
    
       // 向Trie中添加一個新的單詞word 遞歸算法
       // 而且在word中存儲額外的信息,若是額外信息存在就覆蓋
       recursivePut(word, element) {
          this.recursiveAddFn(this.root, word, element, 0);
       }
    
       // 向Trie中添加一個新的單詞word 遞歸輔助函數 -
       // 而且在word中存儲額外的信息,若是額外信息存在就覆蓋
       recursivePutFn(node, word, element, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) {
             if (!node.isWord) {
                node.isWord = true;
                this.size++;
             }
             // 設置或者覆蓋 額外信息
             node.element = element;
             return;
          }
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空就添加
          if (!map.has(letterChar))
             map.set(letterChar, new MyTrieNodeUpgrade(letterChar));
          this.recursiveAddFn(map.get(letterChar), word, element, index + 1);
       }
    
       // 根據這個單詞來獲取額外信息
       get(word) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 單詞遍歷完畢
          if (cur.isWord) return cur.element;
          return null;
       }
    
       // 獲取與這個單詞前綴相關的 全部額外信息
       getPrefixAll(prefix) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of prefix) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return null;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 前綴遍歷完畢 說明這個前綴有單詞與之匹配
          // 開始進行獲取與這個前綴相關的全部單詞及其額外信息
          // 將這些單詞和額外信息以 {word1 : elemnt1, word2 : element2} 形式存儲並返回
          return this.recursiveGetPrefixAllInfo(cur, prefix, {});
       }
    
       // 獲取與這個單詞前綴相關的 全部額外信息 遞歸算法 -
       recursiveGetPrefixAllInfo(node, word, result) {
          if (node.isWord) result[word] = node.element;
    
          const map = node.next;
          const keys = map.keys();
          for (const key of keys) {
             this.recursiveGetPrefixAllInfo(
                map.get(key),
                word.concat(key),
                result
             );
          }
    
          return result;
       }
    
       // 獲取與這個單詞前綴相關的 帶有層次結構的全部額外信息 遞歸算法 -
       recursiveGetPrefixAllTreeInfo(node, word) {
          const result = [];
          if (node.isWord) result.push({ word: node.element });
    
          const map = node.next;
          const keys = map.keys();
          for (const key of keys)
             result.push(
                this.recursiveGetPrefixAll(
                   map.get(key),
                   word.concat(node.letterChar)
                )
             );
          return result;
       }
    
       // 查詢單詞word是否在Trie中
       contains(word) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 單詞遍歷完畢
          // 返回最後一個字符是不是一個單詞的結尾
          return cur.isWord;
       }
    
       // 查詢單詞word是否在Trie中 遞歸算法
       recursiveContains(word) {
          return this.recursiveContainsFn(this.root, word, 0);
       }
    
       // 查詢單詞word是否在Trie中 遞歸賦值函數 -
       recursiveContainsFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空那麼就說明這個單詞沒有進行存儲
          if (!map.has(letterChar)) return false;
          return this.recursiveContainsFn(map.get(letterChar), word, index + 1);
       }
    
       // 查詢在Trie中是否有單詞以 prefix 爲前綴
       isPrefix(prefix) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of prefix) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 前綴遍歷完畢 說明這個前綴有單詞與之匹配
          return true;
       }
    
       // 正則表達式 查詢單詞word是否在Trie中,目前只支持 統配符 "."
       regexpSearch(regexpWord) {
          return this.match(this.root, regexpWord, 0);
       }
    
       // 正則表達式 匹配單詞 遞歸算法 -
       match(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 判斷這個字符是不是通配符
          if (letterChar !== '.') {
             // 若是映射中不包含這個字符
             if (!map.has(letterChar)) return false;
             // 若是映射中包含這個字符,那麼就去找個字符對應的節點中繼續匹配
             return this.match(map.get(letterChar), word, index + 1);
          } else {
             // 遍歷 下一個字符的集合
             // 若是 從下一個字符繼續匹配,只要匹配成功就返回 true
             for (const key of map.keys())
                if (this.match(map.get(key), word, index + 1)) return true;
             // 遍歷一遍以後仍是沒有匹配成功 那麼就算匹配失敗
             return false;
          }
       }
    
       // 獲取字典樹中存儲的單詞數量
       getSize() {
          return this.size;
       }
    
       // 獲取字典樹中是否爲空
       isEmpty() {
          return this.size === 0;
       }
    }
    複製代碼
  3. MyTrieMap

    // 自定義字典映射 TrieMap
    class MyTrieMap {
       constructor() {
          this.trie = new MyTrieUpgrade();
       }
    
       // 添加操做
       add(key, value) {
          this.trie.add(key, value);
       }
    
       // 查詢操做
       get(key) {
          return this.trie.get(key);
       }
    
       // 刪除操做
       remove(key) {
          return null;
       }
    
       // 查看key是否存在
       contains(key) {
          return this.trie.contains(key);
       }
    
       // 更新操做
       set(key, value) {
          this.trie.set(key, value);
       }
    
       // 獲取映射Map中全部的key
       getKeys() {
          let items = this.trie.getPrefixAll('');
          return Object.keys(items);
       }
    
       // 獲取映射Map中全部的value
       getValues() {
          let items = this.trie.getPrefixAll('');
          return Object.values(items);
       }
    
       // 獲取映射Map中實際元素個數
       getSize() {
          return this.trie.getSize();
       }
    
       // 查看映射Map中是否爲空
       isEmpty() {
          return this.trie.isEmpty();
       }
    }
    複製代碼
  4. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('Map Comparison Area');
          const n = 2000000;
    
          const myBSTMap = new MyBinarySearchTreeMap();
          const myTrieMap = new MyTrieMap();
          let performanceTest1 = new PerformanceTest();
    
          const random = Math.random;
          let arr = [];
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) {
             arr.push(i.toString());
          }
    
          this.alterLine('MyBSTMap Comparison Area');
          const myBSTMapInfo = performanceTest1.testCustomFn(function() {
             for (const word of arr)
                myBSTMap.add(word, String.fromCharCode(word));
          });
    
          // 總毫秒數:3692
          console.log(myBSTMapInfo);
          this.show(myBSTMapInfo);
    
          this.alterLine('MyTrieMap Comparison Area');
          const myTrieMapInfo = performanceTest1.testCustomFn(function() {
             for (const word of arr)
                myTrieMap.add(word, String.fromCharCode(word));
          });
    
          // 總毫秒數:2805
          console.log(myTrieMapInfo);
          this.show(myTrieMapInfo);
          console.log(myTrieMap.getKeys()); // 有效
          console.log(myTrieMap.getValues()); // 有效
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

更多與 Trie 相關的話題

  1. 其實 Trie 還有一些話題值得去深刻理解
  2. Trie 的刪除操做
    1. 在大多數狀況下,尤爲是在競賽中使用 Trie 的時候,
    2. 基本上都不會涉及到刪除操做,
    3. 可是在實際的應用中去使用 Trie,不少時候須要涉及刪除操做的。
    4. 例如實現一個通信錄,使用 Trie 建立這個通信錄的話,
    5. 其實就是實現一個 TrieMap,人名做爲單詞,
    6. 在這個單詞的最後一個字母的位置存儲相應的那我的的信息,
    7. 如 電話號碼、郵編、家庭地址等等都是能夠的。
    8. 若是使用 Trie 實現這樣一個通信錄,相應就須要支持刪除操做,
    9. 刪除就是查詢到某一個單詞的最後一個字符的時候,
    10. 先將它的 isWord 設置爲 false,表示這個單詞要被刪除了,
    11. 而後再自底向上的進行刪除操做,
    12. 可是對於每個節點來講,若是它的 next 爲空,
    13. 那麼就證實了它沒有涉及到其它的單詞,那麼能夠直接刪除了,
    14. 不然就不能刪除這個節點,從而這個節點以上的節點都不能刪除,
    15. 那麼只須要將那個單詞的最後一個節點的 isWord 設置爲 false 便可,
    16. 這個操做在你查詢到這個單詞最後一個字符的時候就是作了。
  3. Trie 整體都是在處理和字符串相關的問題
    1. 字符串自己是計算機科學領域研究的一個很是很是重要的一種數據的形式,
    2. 這是由於在具體使用計算機的時候,在大多數時候,其實都是在和字符串打交道,
    3. 無路是在編程的時候仍是在網上文字聊天、搜索引擎搜索關鍵字、網頁顯示頁面、
    4. 網頁顯示的源碼等等這些都是字符串,因此字符串是在計算機科學中無處不在,
    5. 因此他也是一個被研究的很是深刻的問題,在字符串領域有不少經典的問題,
    6. 其中最爲經典的就是子串查詢,驗證某一個字符串是否是另一個字符串的子串,
    7. 這個場景很常見,
    8. 例如瀏覽一個網頁搜索網頁中的關鍵詞或者在 word 中搜索關鍵詞等等,
    9. 其實都是在作子串查詢,不管是網頁仍是 word 中的字符串,
    10. 查詢的對象都是很是大的,不管是一整本網頁仍是一整本電子書,
    11. 它們很是的長,包含的字符很是多,因此高效的子串查詢是很是有意義的,
    12. 經典的子串查詢方法好比 KMP、Boyer-Moore、Rabin-Karp 等等算法,
    13. 它們是很是重要的。
  4. 更多字符串問題
    1. 字符串領域另外一個很重要的問題,就是文件壓縮,
    2. 其實無論你這個文件是什麼文件,它背後都是 01 這種數字碼,
    3. 若是你是以文本的形式打開他們,都是能夠打開的,
    4. 也就是說每個文件其實就是一個字符串,正因如此,
    5. 文件壓縮算法的本質其實就是對一個超級長超級大的字符串進行壓縮,
    6. 文件壓縮相應的也有很是多的算法,最爲基礎的是哈夫曼算法,
    7. 哈夫曼算法自己也是創建了一棵樹,在文件壓縮領域還有更多現代的算法。
  5. 模式匹配
    1. 模式匹配自己就是也是字符串領域研究的一個很是重要的問題,
    2. 例如最經常使用的正則表達式,
    3. 如何實現一個高效的正則表達式這樣的一個引擎,
    4. 其實就是在使用模式匹配這個領域相應的問題,
    5. 一個正則表達式的引擎其中所包含的和字符串相關的算法是很是多的,
    6. 是一個很綜合的問題,能夠嘗試查找更多的資料瞭解模式匹配背後的不少算法,
    7. 甚至可能能夠實現一個屬於本身的小型的正則表達式這樣的一個引擎。
  6. 編譯原理
    1. 其實能夠將正則表達式理解成一種程序,全部的程序如 java、c++、python,
    2. 都是一個字符串,編譯器就作了一件很是偉大的事情,
    3. 它將你所寫的程序這樣的一個代碼字符串進行了解析,
    4. 進而在計算機中進行了運行,那麼這個過程自己是很是複雜的一個過程,
    5. 因此有一個專門的學科,
    6. 專門來研究這個過程以及這個過程當中所涉及算法及相應的優化,
    7. 這個學科就叫作編譯原理,其中就會有不少解決和字符串相關的問題,
    8. 編譯原理自己又是一個更大的學科,有專門的教科書來專門講解。
  7. DNA
    1. 對於字符串來講不只僅是在計算機科學領域發揮着重大的做用,
    2. 在不少其它的領域也發揮着重大的做用,
    3. 最典型的例子就是在生活科學領域,它裏面所研究的 DNA,
    4. 他自己就是一個超長的字符串,
    5. 對於 DNA 來講就是由四種嘌呤所組成的一個巨大的字符串,
    6. 不少生物問題甚至是醫學醫藥方面的問題,
    7. 都是在這個巨大的字符串中去尋找模式及特殊的目標,
    8. 他們的本質都是字符串問題,
    9. 字符串相關的問題是一個很是重要的領域,
    10. 其中所包含算法很是很是的多,
    11. Trie 這種數據結構只是爲某一些狀況下存儲字符串時,
    12. 高效的查詢提供了一些方便而已。

Trie 的刪除操做

  1. 自底向上就是模擬遞歸函數的回溯過程,在回溯的時候進行處理。

Trie 的侷限性

  1. 若是有一種數據結構可以有那麼大的優點有那麼高的性能進行字符串的訪問,
    1. 那麼它就會有相應的劣勢,可能這個世界一般都是很公平的,
    2. 這個劣勢就是它相應要付出的代價,也就是空間,對於 Trie 來講,
    3. 它每個 node 實際上只承載了一個字符的信息,而對於從這個字符到下一個字符,
    4. 須要使用一個 TreeMap 這樣的映射 Map 來映射到下一個字符,
    5. 即便只涉及到 26 個字母表這樣的一個字符空間,那麼最多也須要存儲 26 條記錄,
    6. 那麼須要的存儲空間總體能夠認爲是原來的字符串所佔的空間的 27 倍那麼大,
    7. 那麼空間的消耗實際上是很是多的。
  2. 爲了解決這個問題,相應的就有一些變種
    1. 最爲典型的一種變種就叫作壓縮字典樹(Compressed Trie),
    2. 在使用 Trie 的過程種不少時候對於一個節點,他只有一個後續的字符,
    3. 在這種狀況下這兩個後續的字符徹底能夠合併起來(將多個字符合併成一個字符串),
    4. 若是單鏈上有多個字符,那麼能夠徹底把它們合併在一塊兒,
    5. 由於經過這個字符節點沒法訪問到其它的字符,只能訪問到下一個字符,
    6. 若是有 pan 和 panda 這樣的單詞,那麼就把這條單鏈合併成 pan 和 da,
    7. 這樣合併的樹就叫壓縮字典樹,對於壓縮字典樹來講,
    8. 顯然空間進行了必定的節省,可是它的缺點是維護成本更加的高了,
    9. 由於你在壓縮字典樹中在添加一個單詞,好比添加的這個單詞是 park,
    10. 可是 pan 合在了一塊兒,那麼就須要對 pan 進行拆分的操做,拆成 pa 和 n 兩部分,
    11. 只有這樣才能把 park 這個單詞加入進去,因此總體會更加複雜一些,
    12. 因此有得就有失,爲了節省必定的空間,因此總體的操做就會更復雜一些,
    13. 相應的也會更加費時一些,這是一個平衡。
  3. 另一個變種:Ternary Search Trie
    1. Ternary 和 Binary 相對應,表示三叉也叫三分,
    2. 這是一個三分搜索樹,
    3. 這三叉分別表明小於、等於、大於根節點的字符,
    4. 小於根節點字符的就放到左邊去,
    5. 等於根節點字符的就放到中間來,
    6. 大於跟節點字符的就放到右側去,
    7. 這樣就建立了一個三分搜索樹的字典樹,
    8. 在這樣的三分搜索樹中,每個節點都會有三條叉,
    9. 例如你在下圖中,搜索 dog 這個詞,首先搜索字母 d,
    10. 先從根節點開始找,根節點是 d,那麼就找到了 d,
    11. 而後要繼續搜索 o,以前找到了 d,那麼就在 d 中間這個叉下去找,
    12. k 不是 o,那麼就須要從 k 的三條叉中找,o 比 k 要大,
    13. 那麼就到 k 的右子樹中去找,找到了 o,
    14. 最後繼續搜索 g,以前找到了 o,那麼就在 o 中間這個叉下去找,
    15. i 不是 g,那麼就須要在 i 的三條叉中去找,g 比 i 小,
    16. 那麼就到 i 的左子樹中去找,找到了 g,
    17. 至此就找到了 dog 這個單詞了。
    18. 使用這種三分搜索樹搜索的時間要比這個字母的總數要多的,由於你走到一個節點,
    19. 而這個節點並非你所要找的那個單詞的字符,
    20. 要經過判斷當前這個節點的字母與要找的字母之間的大小關係,
    21. 從而才能進行一個轉向,
    22. 不過這種三分搜索樹它的優勢就是每個節點只有左中右三個孩子,
    23. 而不像 Trie 那樣用一個映射 Map 來表示它的孩子,若是你考慮 26 個字母,
    24. 那麼它就有 26 個孩子,若是考慮大寫字母那就有 52 個孩子,
    25. 若是再考慮一些特殊的符號,那麼有可能一個節點有幾十個孩子,
    26. 可是對於三分搜索樹的 Trie 來講它只有可能有三個孩子,因此大大節省了空間,
    27. 可是代價就是相應的吸取了必定的時間,不過雖然吸取必定的時間,
    28. 三分搜索樹上查找一個單詞所用的時間依然是和這個單詞中的字母數量成正比。
    // d
    // / | \
    // / | \
    // / | \
    // a k z
    // / | \
    // / | \
    // o
    // / | \
    // / | \
    // i
    // / | \
    // / | \
    // g
    複製代碼

字符串識別模式

  1. Trie 其實是一種前綴樹,相應還有一種樹叫作後綴樹,
    1. 後綴樹在解決不少模式匹配的問題時候都有很是大的優點,
    2. 這個後綴樹聽起來叫作後綴樹,
    3. 可是它並非簡單的把一個字符串給它倒序過來而後建立要給 Trie 就行了,
    4. 他有他本身的一種建構的方式,自己仍是一種很巧妙的一種樹結構。

代碼示例

  1. (class: MyTrieUpgrade, class: MyTrieMap, class: PerformanceTest, class: Main)

  2. MyTrie

    // 自定義字典樹節點升級版 TrieNodeUpgrade
    class MyTrieNodeUpgrade {
       constructor(letterChar, element, isWord = false) {
          this.letterChar = letterChar;
          this.element = element; // 升級後能夠存儲特殊數據
          this.isWord = isWord; // 是不是單詞
          this.next = new Map(); // 存儲 字符所對應的節點的 字典映射
       }
    }
    // 自定義字典樹升級版 TrieUpgrade
    class MyTrieUpgrade {
       constructor() {
          this.root = new MyTrieNodeUpgrade();
          this.size = 0;
       }
    
       add(word, element) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             // 存儲 額外信息
             cur.element = element;
             this.size++;
          }
       }
    
       // 向Trie中添加一個新的單詞word 而且在word中存儲額外的信息,若是額外信息存在就覆蓋
       put(word, element) {
          // 指定遊標
          let cur = this.root;
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 下一個字符所對應的映射是否爲空
             if (!cur.next.has(c)) cur.next.set(c, new MyTrieNodeUpgrade(c));
             // 切換到下一個節點
             cur = cur.next.get(c);
          }
    
          // 若是當前這個單詞是一個新的單詞
          if (!cur.isWord) {
             // 當前這個字符是這個單詞的結尾
             cur.isWord = true;
             this.size++;
          }
    
          // 設置或者覆蓋 額外信息
          cur.element = element;
       }
    
       // 向Trie中添加一個新的單詞word 遞歸算法
       // 而且在word中存儲額外的信息,若是額外信息存在就覆蓋
       recursivePut(word, element) {
          this.recursiveAddFn(this.root, word, element, 0);
       }
    
       // 向Trie中添加一個新的單詞word 遞歸輔助函數 -
       // 而且在word中存儲額外的信息,若是額外信息存在就覆蓋
       recursivePutFn(node, word, element, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) {
             if (!node.isWord) {
                node.isWord = true;
                this.size++;
             }
             // 設置或者覆蓋 額外信息
             node.element = element;
             return;
          }
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空就添加
          if (!map.has(letterChar))
             map.set(letterChar, new MyTrieNodeUpgrade(letterChar));
          this.recursiveAddFn(map.get(letterChar), word, element, index + 1);
       }
    
       // 從Trie中刪除一個單詞word
       remove(word) {
          return this.recursiveRemove(this.root, word, 0);
       }
    
       // 從Trie中刪除一個單詞word 遞歸算法 -
       recursiveRemove(node, word, index) {
          let element = null;
          // 遞歸到底了
          if (index === word.length) {
             // 若是不是一個單詞,那麼直接返回 爲null的element
             if (!node.isWord) return element;
             element = node.element;
             node.isWord = false;
             this.size--;
             return element;
          }
    
          const map = node.next;
          const letterChar = word[index];
          const nextNode = map.get(letterChar);
          if (map.has(letterChar))
             element = this.recursiveRemove(nextNode, word, index + 1);
    
          if (element !== null) {
             if (!nextNode.isWord && nextNode.next.size === 0)
                map.delete(letterChar);
          }
          return element;
       }
    
       // 根據這個單詞來獲取額外信息
       get(word) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 單詞遍歷完畢
          if (cur.isWord) return cur.element;
          return null;
       }
    
       // 獲取與這個單詞前綴相關的 全部額外信息
       getPrefixAll(prefix) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of prefix) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return null;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 前綴遍歷完畢 說明這個前綴有單詞與之匹配
          // 開始進行獲取與這個前綴相關的全部單詞及其額外信息
          // 將這些單詞和額外信息以 {word1 : elemnt1, word2 : element2} 形式存儲並返回
          return this.recursiveGetPrefixAllInfo(cur, prefix, {});
       }
    
       // 獲取與這個單詞前綴相關的 全部額外信息 遞歸算法 -
       recursiveGetPrefixAllInfo(node, word, result) {
          if (node.isWord) result[word] = node.element;
    
          const map = node.next;
          const keys = map.keys();
          for (const key of keys) {
             this.recursiveGetPrefixAllInfo(
                map.get(key),
                word.concat(key),
                result
             );
          }
    
          return result;
       }
    
       // 獲取與這個單詞前綴相關的 帶有層次結構的全部額外信息 遞歸算法 -
       recursiveGetPrefixAllTreeInfo(node, word) {
          const result = [];
          if (node.isWord) result.push({ word: node.element });
    
          const map = node.next;
          const keys = map.keys();
          for (const key of keys)
             result.push(
                this.recursiveGetPrefixAll(
                   map.get(key),
                   word.concat(node.letterChar)
                )
             );
          return result;
       }
    
       // 查詢單詞word是否在Trie中
       contains(word) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of word) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 單詞遍歷完畢
          // 返回最後一個字符是不是一個單詞的結尾
          return cur.isWord;
       }
    
       // 查詢單詞word是否在Trie中 遞歸算法
       recursiveContains(word) {
          return this.recursiveContainsFn(this.root, word, 0);
       }
    
       // 查詢單詞word是否在Trie中 遞歸賦值函數 -
       recursiveContainsFn(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 下一個字符所對應的映射是否爲空 爲空那麼就說明這個單詞沒有進行存儲
          if (!map.has(letterChar)) return false;
          return this.recursiveContainsFn(map.get(letterChar), word, index + 1);
       }
    
       // 查詢在Trie中是否有單詞以 prefix 爲前綴
       isPrefix(prefix) {
          // 指定遊標
          let cur = this.root;
    
          // 遍歷出當前單詞的每個字符
          for (const c of prefix) {
             // 獲取當前這個字符所對應的節點
             const node = cur.next.get(c);
             // 這個節點不存在,那麼就說明就沒有存儲這個字符
             if (!node) return false;
             // 遊標切換到這個節點
             cur = node;
          }
    
          // 前綴遍歷完畢 說明這個前綴有單詞與之匹配
          return true;
       }
    
       // 正則表達式 查詢單詞word是否在Trie中,目前只支持 統配符 "."
       regexpSearch(regexpWord) {
          return this.match(this.root, regexpWord, 0);
       }
    
       // 正則表達式 匹配單詞 遞歸算法 -
       match(node, word, index) {
          // 解決基本的問題,由於已經到底了
          if (index === word.length) return node.isWord;
    
          const map = node.next; // 獲取節點的next 也就是字符對應的映射
          const letterChar = word[index]; // 獲取當前位置對應的單詞中的字符
          // 判斷這個字符是不是通配符
          if (letterChar !== '.') {
             // 若是映射中不包含這個字符
             if (!map.has(letterChar)) return false;
             // 若是映射中包含這個字符,那麼就去找個字符對應的節點中繼續匹配
             return this.match(map.get(letterChar), word, index + 1);
          } else {
             // 遍歷 下一個字符的集合
             // 若是 從下一個字符繼續匹配,只要匹配成功就返回 true
             for (const key of map.keys())
                if (this.match(map.get(key), word, index + 1)) return true;
             // 遍歷一遍以後仍是沒有匹配成功 那麼就算匹配失敗
             return false;
          }
       }
    
       // 獲取字典樹中存儲的單詞數量
       getSize() {
          return this.size;
       }
    
       // 獲取字典樹中是否爲空
       isEmpty() {
          return this.size === 0;
       }
    }
    複製代碼
  3. MyTrieMap

    // 自定義字典映射 TrieMap
    class MyTrieMap {
       constructor() {
          this.trie = new MyTrieUpgrade();
       }
    
       // 添加操做
       add(key, value) {
          this.trie.add(key, value);
       }
    
       // 查詢操做
       get(key) {
          return this.trie.get(key);
       }
    
       // 刪除操做
       remove(key) {
          return this.trie.remove(key);
       }
    
       // 查看key是否存在
       contains(key) {
          return this.trie.contains(key);
       }
    
       // 更新操做
       set(key, value) {
          this.trie.set(key, value);
       }
    
       // 獲取映射Map中全部的key
       getKeys() {
          let items = this.trie.getPrefixAll('');
          return Object.keys(items);
       }
    
       // 獲取映射Map中全部的value
       getValues() {
          let items = this.trie.getPrefixAll('');
          return Object.values(items);
       }
    
       // 獲取映射Map中實際元素個數
       getSize() {
          return this.trie.getSize();
       }
    
       // 查看映射Map中是否爲空
       isEmpty() {
          return this.trie.isEmpty();
       }
    }
    複製代碼
  4. PerformanceTest

    // 性能測試
    class PerformanceTest {
       constructor() {}
    
       // 對比隊列
       testQueue(queue, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             queue.enqueue(random() * openCount);
          }
    
          while (!queue.isEmpty()) {
             queue.dequeue();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比棧
       testStack(stack, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             stack.push(random() * openCount);
          }
    
          while (!stack.isEmpty()) {
             stack.pop();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比集合
       testSet(set, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          let arr = [];
          let temp = null;
    
          // 第一遍測試
          for (var i = 0; i < openCount; i++) {
             temp = random();
             // 添加劇復元素,從而測試集合去重的能力
             set.add(temp * openCount);
             set.add(temp * openCount);
    
             arr.push(temp * openCount);
          }
    
          for (var i = 0; i < openCount; i++) {
             set.remove(arr[i]);
          }
    
          // 第二遍測試
          for (var i = 0; i < openCount; i++) {
             set.add(arr[i]);
             set.add(arr[i]);
          }
    
          while (!set.isEmpty()) {
             set.remove(arr[set.getSize() - 1]);
          }
    
          let endTime = Date.now();
    
          // 求出兩次測試的平均時間
          let avgTime = Math.ceil((endTime - startTime) / 2);
    
          return this.calcTime(avgTime);
       }
    
       // 對比映射
       testMap(map, openCount) {
          let startTime = Date.now();
    
          let array = new MyArray();
          let random = Math.random;
          let temp = null;
          let result = null;
          for (var i = 0; i < openCount; i++) {
             temp = random();
             result = openCount * temp;
             array.add(result);
             array.add(result);
             array.add(result);
             array.add(result);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             if (map.contains(result)) map.add(result, map.get(result) + 1);
             else map.add(result, 1);
          }
    
          for (var i = 0; i < array.getSize(); i++) {
             result = array.get(i);
             map.remove(result);
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 對比堆 主要對比 使用heapify 與 不使用heapify時的性能
       testHeap(heap, array, isHeapify) {
          const startTime = Date.now();
    
          // 是否支持 heapify
          if (isHeapify) heap.heapify(array);
          else {
             for (const element of array) heap.add(element);
          }
    
          console.log('heap size:' + heap.size() + '\r\n');
          document.body.innerHTML += 'heap size:' + heap.size() + '<br /><br />';
    
          // 使用數組取值
          let arr = new Array(heap.size());
          for (let i = 0; i < arr.length; i++) arr[i] = heap.extractMax();
    
          console.log(
             'Array size:' + arr.length + ',heap size:' + heap.size() + '\r\n'
          );
          document.body.innerHTML +=
             'Array size:' +
             arr.length +
             ',heap size:' +
             heap.size() +
             '<br /><br />';
    
          // 檢驗一下是否符合要求
          for (let i = 1; i < arr.length; i++)
             if (arr[i - 1] < arr[i]) throw new Error('error.');
    
          console.log('test heap completed.' + '\r\n');
          document.body.innerHTML += 'test heap completed.' + '<br /><br />';
    
          const endTime = Date.now();
          return this.calcTime(endTime - startTime);
       }
    
       // 計算運行的時間,轉換爲 天-小時-分鐘-秒-毫秒
       calcTime(result) {
          //獲取距離的天數
          var day = Math.floor(result / (24 * 60 * 60 * 1000));
    
          //獲取距離的小時數
          var hours = Math.floor((result / (60 * 60 * 1000)) % 24);
    
          //獲取距離的分鐘數
          var minutes = Math.floor((result / (60 * 1000)) % 60);
    
          //獲取距離的秒數
          var seconds = Math.floor((result / 1000) % 60);
    
          //獲取距離的毫秒數
          var milliSeconds = Math.floor(result % 1000);
    
          // 計算時間
          day = day < 10 ? '0' + day : day;
          hours = hours < 10 ? '0' + hours : hours;
          minutes = minutes < 10 ? '0' + minutes : minutes;
          seconds = seconds < 10 ? '0' + seconds : seconds;
          milliSeconds =
             milliSeconds < 100
                ? milliSeconds < 10
                   ? '00' + milliSeconds
                   : '0' + milliSeconds
                : milliSeconds;
    
          // 輸出耗時字符串
          result =
             day +
             '天' +
             hours +
             '小時' +
             minutes +
             '分' +
             seconds +
             '秒' +
             milliSeconds +
             '毫秒' +
             ' <<<<============>>>> 總毫秒數:' +
             result;
    
          return result;
       }
    
       // 自定義對比
       testCustomFn(fn) {
          let startTime = Date.now();
    
          fn();
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    }
    複製代碼
  5. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('Map Comparison Area');
          const n = 2000000;
    
          const myBSTMap = new MyBinarySearchTreeMap();
          const myTrieMap = new MyTrieMap();
          let performanceTest1 = new PerformanceTest();
    
          const random = Math.random;
          let arr = [];
    
          // 循環添加隨機數的值
          for (let i = 0; i < n; i++) {
             arr.push(Math.floor(n * random()).toString());
          }
    
          this.alterLine('MyBSTMap Comparison Area');
          const myBSTMapInfo = performanceTest1.testCustomFn(function() {
             // 添加
             for (const word of arr)
                myBSTMap.add(word, String.fromCharCode(word));
    
             // 刪除
             for (const word of arr) myBSTMap.remove(word);
    
             // 查找
             for (const word of arr)
                if (myBSTMap.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:18703
          console.log(myBSTMapInfo);
          this.show(myBSTMapInfo);
    
          this.alterLine('MyTrieMap Comparison Area');
          const myTrieMapInfo = performanceTest1.testCustomFn(function() {
             for (const word of arr)
                myTrieMap.add(word, String.fromCharCode(word));
    
             // 刪除
             for (const word of arr) myTrieMap.remove(word);
    
             // // 查找
             for (const word of arr)
                if (myTrieMap.contains(word))
                   throw new Error("doesn't remove ok.");
          });
    
          // 總毫秒數:8306
          console.log(myTrieMapInfo);
          this.show(myTrieMapInfo);
          console.log(myTrieMap.getKeys()); // 有效
          console.log(myTrieMap.getValues()); // 有效
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼
相關文章
相關標籤/搜索