前缀树的说明和用途
前缀树又叫单词查找树,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 }
浙公网安备 33010602011771号