【数据结构】关于前缀树(单词查找树,Trie)
前缀树的说明和用途
前缀树又叫单词查找树,Trie,是一类常用的数据结构,其特点是以空间换时间,在查找字符串时有极大的时间优势,其查找的时间复杂度与键的数量无关,在能找到时,最大的时间复杂度也仅为键的长度+1,在找不到时可以小于键的长度。前缀树又被称为R向查找树,因为其树中的每个节点都有R个链接,但每个节点都只有一个父节点。前缀树的使用也很广泛,其常见问题有单词拆分,实现前缀树等
实现API
单词查找树的API将使用符号表的通用API,以体现其功能的共性,在解决具体问题时稍做变动即可。
public class String<T> | 说明 |
|
构造函数 |
|
向前缀树中添加一个键值对 |
|
获取给出的键对应的值,如果键不存在,返回null |
|
删除给出的键对应的值,根据其在树中的结构,可能会删除键 |
|
获取所有的键 |
|
获取给出指定前缀对应的键 |
重要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)的更多相关文章
- K:单词查找树(Trie)
单词查找树,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串.Trie可以看作是一个确定有限状态自动机(DFA).与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中 ...
- Trie树,又称单词查找树、字典
在百度或淘宝搜索时,每输入字符都会出现搜索建议,比如输入“北京”,搜索框下面会以北京为前缀,展示“北京爱情故事”.“北京公交”.“北京医院”等等搜索词.实现这类技术后台所采用的数据结构是什么?[中国某 ...
- cogs 293. [NOI 2000] 单词查找树 Trie树字典树
293. [NOI 2000] 单词查找树 ★★☆ 输入文件:trie.in 输出文件:trie.out 简单对比时间限制:1 s 内存限制:128 MB 在进行文法分析的时候,通常需 ...
- COGS 293.[NOI2000] 单词查找树
★ 输入文件:trie.in 输出文件:trie.out 简单对比 时间限制:1 s 内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高 ...
- [NOI2000] 单词查找树
★★ 输入文件:trie.in 输出文件:trie.out 简单对比 时间限制:1 s 内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提 ...
- 293. [NOI2000] 单词查找树——COGS
293. [NOI2000] 单词查找树 ★★ 输入文件:trie.in 输出文件:trie.out 简单对比时间限制:1 s 内存限制:128 MB 在进行文法分析的时候,通常需要检 ...
- codevs 1729 单词查找树
二次联通门 : codevs 1729 单词查找树 /* codevs 1729 单词查找树 Trie树 统计节点个数 建一棵Trie树 插入单词时每新开一个节点就计数器加1 */ #include ...
- 解题报告:luogu P5755 [NOI2000]单词查找树
题目链接:P5755 [NOI2000]单词查找树 曾几何时,NOI 也有这么水的题( 裸的\(Trie\),只用维护插入即可,记得\(+1\)就好了,真没用讲的. \(Code\): #includ ...
- 【NOI2000】 单词查找树
问题描述 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高查找和定位的速度,通常都画出与单词列表所对应的单词查找树,其特点如下: 根结点不包含字母,除根结点外每一个结点都仅包 ...
随机推荐
- Netty源码解析 -- 服务端启动过程
本文通过阅读Netty源码,解析Netty服务端启动过程. 源码分析基于Netty 4.1 Netty是一个高性能的网络通信框架,支持NIO,OIO等多种IO模式.通常,我们都是使用NIO模式,该系列 ...
- Java学习的第二十九天
1. 如果类中的某个属性不希望被序列化则需要transient关键字 序列化一组对象 2.无问题 3.明天学习打印流
- Vue项目入门实例
前言 本文记录Vue2.x + Element-UI + TypeScript语法入门实例 为什么要用TypeScript? 1.TypeScript是JavaScript的超集,利用es6语法,实现 ...
- Jmeter 用户定义的变量的使用
第一步: 打开Jmeter软件,新建一个线程组,添加 > 配置元素 > 用户定义的变量 第二步: 设置值,如下图所示: 第三步,使用设置的名称 :
- Mybatis执行SQL的完整过程及四大组件介绍
一切的执行从MapperProxy开始,MapperProxy是MapperProxyFactory使用SqlSession创建出来的.所以MapperProxy中包含SqlSession. 可以看到 ...
- C++ storage allocation + Dynamic memory allocation + setting limits + initializer list (1)
1. 对象的空间在括号开始就已经分配,但是构造在定义对象的时候才会实现,若跳过(譬如goto),到括号结束析构会发生错误,编译会通不过. 2.初始化 1 struct X { int i ; floa ...
- leetcode17gas-station
题目描述 环形路上有n个加油站,第i个加油站的汽油量是gas[i]. 你有一辆车,车的油箱可以无限装汽油.从加油站i走到下一个加油站(i+1)花费的油量是cost[i],你从一个加油站出发,刚开始的时 ...
- 2020 中国.NET 开发者峰会正式启动
2014年微软组织并成立.NET基金会,微软在成为主要的开源参与者的道路上又前进了一步. 2014年以来已经有众多知名公司加入.NET基金会,Google,微软,AWS三大云厂商已经齐聚.NET基金会 ...
- 基于 Nebula Operator 的 K8s 自动化部署运维
摘要:Nebula Operator 是 Nebula Graph 在 Kubernetes 系统上的自动化部署运维插件.在本文,你将了解到 Nebula Operator 的特性及它的工作原理. 从 ...
- 面经手册 · 第18篇《AQS 共享锁,Semaphore、CountDownLatch,听说数据库连接池可以用到!》
作者:小傅哥 博客:https://bugstack.cn Github:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有 ...