前缀树的说明和用途

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

【数据结构】关于前缀树(单词查找树,Trie)的更多相关文章

  1. K:单词查找树(Trie)

      单词查找树,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串.Trie可以看作是一个确定有限状态自动机(DFA).与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中 ...

  2. Trie树,又称单词查找树、字典

    在百度或淘宝搜索时,每输入字符都会出现搜索建议,比如输入“北京”,搜索框下面会以北京为前缀,展示“北京爱情故事”.“北京公交”.“北京医院”等等搜索词.实现这类技术后台所采用的数据结构是什么?[中国某 ...

  3. cogs 293. [NOI 2000] 单词查找树 Trie树字典树

    293. [NOI 2000] 单词查找树 ★★☆   输入文件:trie.in   输出文件:trie.out   简单对比时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需 ...

  4. COGS 293.[NOI2000] 单词查找树

    ★   输入文件:trie.in   输出文件:trie.out   简单对比 时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高 ...

  5. [NOI2000] 单词查找树

    ★★   输入文件:trie.in   输出文件:trie.out   简单对比 时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提 ...

  6. 293. [NOI2000] 单词查找树——COGS

    293. [NOI2000] 单词查找树 ★★   输入文件:trie.in   输出文件:trie.out   简单对比时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检 ...

  7. codevs 1729 单词查找树

    二次联通门 : codevs 1729 单词查找树 /* codevs 1729 单词查找树 Trie树 统计节点个数 建一棵Trie树 插入单词时每新开一个节点就计数器加1 */ #include ...

  8. 解题报告:luogu P5755 [NOI2000]单词查找树

    题目链接:P5755 [NOI2000]单词查找树 曾几何时,NOI 也有这么水的题( 裸的\(Trie\),只用维护插入即可,记得\(+1\)就好了,真没用讲的. \(Code\): #includ ...

  9. 【NOI2000】 单词查找树

    问题描述 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高查找和定位的速度,通常都画出与单词列表所对应的单词查找树,其特点如下: 根结点不包含字母,除根结点外每一个结点都仅包 ...

随机推荐

  1. 模拟量采集模块433Mhz LoRa无线自组网络介绍

    模拟量采集模块433Mhz LoRa无线自组网络是LPWAN(低功耗广域网Low Power Wide Area Nerwork)通信技术中的一种,是美国Semtech公司采用和推广的一种基于扩频技术 ...

  2. 震惊!你还不知道SpringBoot真正的启动引导类

    引言 SpringBoot项目中的启动类,一般都是XXApplication,例如「StatsApplication」,「UnionApplication」. 每个项目的启动类名称都不一样.但是它的启 ...

  3. 《Clojure编程》笔记 第5章 宏

    目录 背景简述 第5章 宏 5.0 术语 5.1 宏到底是什么 5.1.1 宏不是什么 5.1.2 有什么是宏能做而函数不能做的 5.1.3 宏vsRuby的eval 5.2 编写你的第一个宏 5.3 ...

  4. 公钥、私钥、SSL/TLS、会话密钥、DES【转载】

    原文链接:https://www.cnblogs.com/thbCode/p/5829719.html 一,公钥私钥1,公钥和私钥成对出现2,公开的密钥叫公钥,只有自己知道的叫私钥3,用公钥加密的数据 ...

  5. Java复制数组的方法

    java数组拷贝主要有四种方法,分别是循环赋值,System.arraycopy(),Arrays.copyOf()(或者Arrays.copyOfRange)和clone()方法.下面分别介绍一下这 ...

  6. C语言100题集合004-统计各个年龄阶段的人数

    系列文章<C语言经典100例>持续创作中,欢迎大家的关注和支持. 喜欢的同学记得点赞.转发.收藏哦- 后续C语言经典100例将会以pdf和代码的形式发放到公众号 欢迎关注:计算广告生态 即 ...

  7. XJOI 夏令营501-511测试11 统计方案

    小B写了一个程序,随机生成了n个正整数,分别是a[1]..a[n],他取出了其中一些数,并把它们乘起来之后模p,得到了余数c.但是没过多久,小B就忘记他选了哪些数,他想把所有可能的取数方案都找出来.你 ...

  8. C\C++语言重点——指针篇 | 为什么指针被誉为 C 语言灵魂?(一文让你完全搞懂指针)

    本篇文章来自小北学长的公众号,仅做学习使用,部分内容做了适当理解性修改和添加了博主的个人经历. 注:这篇文章好好看完一定会让你掌握好指针的本质! 看到标题有没有想到什么? 是的,这一篇的文章主题是「指 ...

  9. Gromacs命令-Chapter1

    Gromacs的命令非常多,下面我将我最近用到的先总结一下.标题上也写了这只是Chapter1,以后有新的会继续写Chapter2...等等. 下面这个网址http://manual.gromacs. ...

  10. Git常用命令【ZeyFra】

    // 账户设置 git config --global user.name "ZeyFra" git config --global user.email "zeyfra ...