1. 程式人生 > 實用技巧 >【資料結構】關於字首樹(單詞查詢樹,Trie)

【資料結構】關於字首樹(單詞查詢樹,Trie)

字首樹的說明和用途

  字首樹又叫單詞查詢樹,Trie,是一類常用的資料結構,其特點是以空間換時間,在查詢字串時有極大的時間優勢,其查詢的時間複雜度與鍵的數量無關,在能找到時,最大的時間複雜度也僅為鍵的長度+1,在找不到時可以小於鍵的長度。字首樹又被稱為R向查詢樹,因為其樹中的每個節點都有R個連結,但每個節點都只有一個父節點。字首樹的使用也很廣泛,其常見問題有單詞拆分實現字首樹

實現API

  單詞查詢樹的API將使用符號表的通用API,以體現其功能的共性,在解決具體問題時稍做變動即可。

public class String<T> 說明
public StringTrie()
建構函式
public void put(String key, T value)
向前綴樹中新增一個鍵值對
public T get(String key)
獲取給出的鍵對應的值,如果鍵不存在,返回null
public void delete(String key)
刪除給出的鍵對應的值,根據其在樹中的結構,可能會刪除鍵
public Iterable<String> keys()
獲取所有的鍵
public Iterable<String> keysWithPrefix(String pre)
獲取給出指定字首對應的鍵

重要API的說明實現

  本API的實現使用的都是遞迴操作,在樹中,遞迴操作都會是簡潔易懂的。最開始要說明的是樹的節點類,其是構成樹的基礎。程式碼如下,每個節點都包含值域和指標域,不同的指標域是一個數組,其長度代表的就是給出的字母表的長度,比如鍵都是由英文小寫字母表示的話,那字母表長度就為26.

 1 //節點類
 2 //Java泛型不支援陣列
 3 class Node {
 4     Object value;
 5     Node[] nextNodes;
 6 
 7     Node(int n) {
 8         nextNodes = new Node[n];
 9
} 10 }

新增鍵值對

  向樹中新增一個鍵值對,首先,如果當前根節點為空,這裡的根節點並不單單指整棵樹的根節點,也可能是當前子樹的根節點,根節點為空,則先例項化一個根節點。利用一個index記錄深度,也就是應當檢查key中的第index+1個字元了,那麼就可以向下遞迴當前節點的第n的子結點,n代表的就是第index+1個字元在字母表中的位置.

 1 /**
 2  * 放入一個鍵值對,值的型別為T,鍵型別確定為String
 3  * */
 4 public void put(String key, T value) {
 5     root = put(key, value, root, 0);
 6 }
 7 
 8 private Node put(String key, T value, Node node, int index) {
 9     if (node == null) node = new Node(count);
10     if (index == key.length()) {
11         node.value = value;
12         return node;
13 }
14     char cur = key.charAt(index);
15     node.nextNodes[cur] = put(key, value, node.nextNodes[cur], ++index);
16     return node;
17 }

根據鍵獲取值

  獲取同樣可以使用遞迴,如果當前根結點為空,說明給出的key中包含了樹中沒有的字元,可以直接返回null。如果不為空,且已經遞迴到了鍵的最後一個字元,當前節點到樹的根結點構成的字元流就是給出的key,可以返回當前節點,如果不是最後一個字元,可以重複遞迴操作。

 1 /**
 2 * 獲得以key對應的值,沒找到則返回null
 3 * */
 4 public T get(String key) {
 5     Node result = get(key, root, 0);
 6     if (result == null) return null;
 7     return (T) result.value;
 8 }
 9 
10 private Node get(String key, Node node, int index) {
11     if (node == null) return null;
12     if (index == key.length()) return node;
13     char cur = key.charAt(index);
14     return get(key, node.nextNodes[cur], ++index);
15 }

刪除操作

  刪除操作也使用的是遞迴,但操作設及到了要不要在樹中刪除某個字元,即刪除這個鍵。在我們找到鍵對應的節點後,如果這個節點有值,那麼直接將值賦空,但是如何處理這個節點呢,那就要檢查其是否有子結點存在,即這個字元還存在於其他鍵中。如果沒有子結點,那麼就可以刪除這個節點,並返回上層,檢查其父節點是否也已經沒有子結點了,沒有也刪除父節點,重複操作即可。

 1 /**
 2 * 刪除一個鍵值對
 3 * */
 4 public void delete(String key) {
 5         root = delete(key, root, 0);
 6 }
 7 
 8 private Node delete(String key, Node node, int index) {
 9     if (node == null) return null;
10     if (index == key.length()) {
11         node.value = null;//找到key後,將key對應的value賦空
12     }else {
13         char cur = key.charAt(index);
14         node.nextNodes[cur] = delete(key, node.nextNodes[cur], ++index);//在子樹中遞迴找key
15     }
16     if (node.value != null) return node;//如果當前node組成的key有值對應則可以直接返回
17     for (int i = 0;i < node.nextNodes.length;i++) {
18        if (node.nextNodes[i].value != null) return node;//如果當前node還有子樹則保留當前節點返回
19     }
20     return null;//當前key沒有任何value,其子結點也沒有,則刪除這個key。
21 }

根據條件獲取鍵

  獲取全部的鍵其實就是獲取以空字元為開頭的鍵,那如果獲取以某個字串開頭的鍵呢,其實如果我們先利用私有的get函式,根據給出的字首,就可以直接獲取到字首最後一個字元代表的那個節點。再獲取當前節點的全部子樹所代表的key值,那就是我們要的答案。獲取當前節點的全部子樹代表的key值,就要在遞迴到當前層是用已有的pre加上當前字元。如果節點的value不為空,代表這個節點到根節點組成的字串是一個合法的key。

 1 /**
 2 * 獲得全部的key
 3 * */
 4 public Iterable<String> keys() {
 5     //獲取所有的keys,就是收集以空字元開頭的key
 6     return keysWithPrefix("");
 7 }
 8 /**
 9 * 獲得以某個字串開頭的全部keys
10 * */
11 public Iterable<String> keysWithPrefix(String pre) {
12     Queue<String> queue = new LinkedList<>();
13     //呼叫get,代表先到達字首所在的那個節點,再向下收集
14     collect(get(pre, root, 0), pre, queue);
15     return queue;
16 }
17 
18 //在給定字首的節點後收集所有的字元
19 private void collect(Node node, String pre, Queue<String> queue) {
20     if (node == null) return;
21     if (node.value != null) queue.add(pre);//找到了一個以pre為字首的key
22     for (int i = 0;i < node.nextNodes.length;i++) {
23         //此處因為字母表的原因,只寫出大概意思,pre值應該更新為pre加上當前子結點代表的字元
24         collect(node.nextNodes[i], pre+i, queue);
25     }
26 }

全部實現

  1 public class StringTrie<T> {
  2 
  3     private Node root;
  4     private int count;
  5 
  6     public StringTrie() {
  7         this.count = 26;//預設查詢樹只包含26個小寫字母
  8         root = new Node(count);
  9     }
 10     public StringTrie(int count) {
 11         this.count = count;
 12         root = new Node(count);
 13     }
 14 
 15     /**
 16      * 放入一個鍵值對,值的型別為T,鍵型別確定為String
 17      * */
 18     public void put(String key, T value) {
 19         root = put(key, value, root, 0);
 20     }
 21 
 22     private Node put(String key, T value, Node node, int index) {
 23         if (node == null) node = new Node(count);
 24         if (index == key.length()) {
 25             node.value = value;
 26             return node;
 27         }
 28         char cur = key.charAt(index);
 29         node.nextNodes[cur] = put(key, value, node.nextNodes[cur], ++index);
 30         return node;
 31     }
 32     /**
 33      * 獲得以key對應的值,沒找到則返回null
 34      * */
 35     public T get(String key) {
 36         Node result = get(key, root, 0);
 37         if (result == null) return null;
 38         return (T) result.value;
 39     }
 40 
 41     private Node get(String key, Node node, int index) {
 42         if (node == null) return null;
 43         if (index == key.length()) return node;
 44         char cur = key.charAt(index);
 45         return get(key, node.nextNodes[cur], ++index);
 46     }
 47 
 48     /**
 49      * 刪除一個鍵值對
 50      * */
 51     public void delete(String key) {
 52         root = delete(key, root, 0);
 53     }
 54 
 55     private Node delete(String key, Node node, int index) {
 56         if (node == null) return null;
 57         if (index == key.length()) {
 58             node.value = null;//找到key後,將key對應的value賦空
 59         }else {
 60             char cur = key.charAt(index);
 61             node.nextNodes[cur] = delete(key, node.nextNodes[cur], ++index);//在子樹中遞迴找key
 62         }
 63         if (node.value != null) return node;//如果當前node組成的key有值對應則可以直接返回
 64         for (int i = 0;i < node.nextNodes.length;i++) {
 65             if (node.nextNodes[i].value != null) return node;//如果當前node還有子樹則保留當前節點返回
 66         }
 67         return null;//當前key沒有任何value,其子結點也沒有,則刪除這個key。
 68     }
 69 
 70     /**
 71      * 獲得全部的key
 72      * */
 73     public Iterable<String> keys() {
 74         //獲取所有的keys,就是收集以空字元開頭的key
 75         return keysWithPrefix("");
 76     }
 77     /**
 78      * 獲得以某個字串開頭的全部keys
 79      * */
 80     public Iterable<String> keysWithPrefix(String pre) {
 81         Queue<String> queue = new LinkedList<>();
 82         //呼叫get,代表先到達字首所在的那個節點,再向下收集
 83         collect(get(pre, root, 0), pre, queue);
 84         return queue;
 85     }
 86 
 87     //在給定字首的節點後收集所有的字元
 88     private void collect(Node node, String pre, Queue<String> queue) {
 89         if (node == null) return;
 90         if (node.value != null) queue.add(pre);//找到了一個以pre為字首的key
 91         for (int i = 0;i < node.nextNodes.length;i++) {
 92             //此處因為字母表的原因,只寫出大概意思,pre值應該更新為pre加上當前子結點代表的字元
 93             collect(node.nextNodes[i], pre+i, queue);
 94         }
 95     }
 96 
 97 }
 98 //節點類
 99 //Java泛型不支援陣列
100 class Node {
101     Object value;
102     Node[] nextNodes;
103 
104     Node(int n) {
105         nextNodes = new Node[n];
106     }
107 }