【資料結構】關於字首樹(單詞查詢樹,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 }