前缀树的说明和用途

  前缀树又叫单词查找树,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 }