字首樹及其Java實現

汪小川 發表於 2021-09-13
Java

字首樹

基礎知識

Trie樹。又稱之為單詞查詢樹或者鍵樹,是一種樹形結構。應用於統計和排序大量的字串。常被搜尋引擎系統用於文字詞頻統計。它的優點:能夠最大限度的減少無謂的字串比較,查詢效率比雜湊表高。

核心思想是以空間換時間。利用記錄字串公共字首來降低查詢時間的開銷。

3個基本性質

  1. 根節點不包含字元,除根節點外每一個節點都只包含一個字元
  2. 從根節點到某一節點,路徑上經過的字元連線起來,為該節點所對應的字串
  3. 每個節點的所有子節點所包含的字元都不同。
  4. 每個節點對應一個字首,葉節點對應最長字首,即單詞本身。

功能

應該實現查詢,插入,字首查詢的功能。

資料結構組成

Trie,又稱字首樹或字典樹,是一棵有根樹,其每個節點包含以下欄位:

指向子節點的指標陣列children。對於本題而言,陣列長度為26,即小寫英文字母的數量。此時children[0]對應小寫字母 a。
布林欄位isEnd,表示該節點是否為字串的結尾。

實現

插入

我們從字典樹的根開始,插入字串。對於當前字元對應的子節點,有兩種情況:

  • 子節點存在。沿著指標移動到子節點,繼續處理下一個字元。
  • 子節點不存在。建立一個新的子節點,記錄在children陣列的對應位置上,然後沿著指標移動到子節點,繼續搜尋下一個字元。

重複以上步驟,直到處理字串的最後一個字元,然後將當前節點標記為字串的結尾。

查詢字首

我們從字典樹的根開始,查詢字首。對於當前字元對應的子節點,有兩種情況:

子節點存在。沿著指標移動到子節點,繼續搜尋下一個字元。
子節點不存在。說明字典樹中不包含該字首,返回空指標。
重複以上步驟,直到返回空指標或搜尋完字首的最後一個字元。

若搜尋到了字首的末尾,就說明字典樹中存在該字首。此外,若字首末尾對應節點的isEnd為真,則說明字典樹中存在該字串。

查詢

實現了查詢字首的函式,就可以直接呼叫這個函式,檢查返回的node是否不為空且是葉子節點。若是則說明此時的字串存在,不然就不存在。

package JavaCode.leetcode.DataStructure.Tree;

 class Trie {
     //Trie的兩個屬性,指向子節點的指標陣列和表示該節點是否為結尾的布林值
     private Trie[] children;
     private boolean isEnd;
    
     //構造
     public Trie() {
         children = new Trie[26];
         isEnd = false;
     }
    
     //插入節點。
     public void insert(String word) {
         Trie node = this;//指標指向當前的根
         for (int i = 0; i < word.length(); i++) {
             char ch = word.charAt(i);//待插入的字元
             int index = ch - 'a';//引數
             //當前的節點為null,就新建一個節點
             if (node.children[index] == null) {
                 node.children[index] = new Trie();
             }
             //當前節點不為null,就將node沿指標移動到子節點
             node = node.children[index];
         }
         //完成插入後,就將此時node所指向的節點isEnd置為true
         node.isEnd = true;
     }
     //查詢字首樹中是否含有本字串,使用查詢字首和的函式得到節點node,
     //若返回的node不為null,則說明找到了word的字首,且如果此時isEnd為true,說明node是葉子
     //則說明此時的word存在於字首樹中。
     public boolean search(String word) {
         Trie node = searchPrefix(word);
         return node != null && node.isEnd;
     }

     //查詢字首
     public boolean startsWith(String prefix) {
         //只要返回值不為null,說明搜尋到了字首的末尾就為true,否則為false
         return searchPrefix(prefix) != null;
     }

     private Trie searchPrefix(String prefix) {
         Trie node = this;//指標指向當前的根
         for (int i = 0; i < prefix.length(); i++) {
             //當前訪問的字元及其引數
             char ch = prefix.charAt(i);
             int index = ch - 'a';
             //訪問的節點不存在,就返回一個null
             if (node.children[index] == null) {
                 return null;
             }
             //訪問的節點存在,就沿著指標指向的節點移動
             node = node.children[index];
         }
         return node;//最後搜尋到了末尾就返回這個末尾的節點,說明存在這個字首
     }
}