字典树的数据结构

news2024/9/20 23:58:47

Trie字典树主要用于存储字符串,Trie 的每个 Node 保存一个字符。用链表来描述的话,就是一个字符串就是一个链表。每个Node都保存了它的所有子节点。

例如我们往字典树中插入see、pain、paint三个单词,Trie字典树如下所示:

也就是说如果只考虑小写的26个字母,那么Trie字典树的每个节点都可能有26个子节点。

Trie字典树的基本操作

插入

本文是使用链表来实现Trie字典树,字符串的每个字符作为一个Node节点,Node主要有两部分组成:

  1. 是否是单词 (boolean isWord)
  2. 节点所有的子节点,用map来保存 (Map next)

例如插入一个paint单词,如果用户查询pain,尽管 paint 包含了 pain,但是Trie中仍然不包含 pain 这个单词,所以如果往Trie中插入一个单词,需要把该单词的最后一个字符的节点的 isWord 设置为 true。所以为什么Node需要存储 是否是单词 这个属性。

节点的所有子节点,通过一个Map来存储,key是当前子节点对应的字符,value是子节点。

实现的伪代码如下:

public void add(String word) {
	Node current = root;
	char[] cs = word.toCharArray();
	for (char c : cs) {
		Node next = current.next.get(c);
		if (next == null) {
		    //一个字符对应一个Node节点
			current.next.put(c, new Node());
		}
		current = current.next.get(c);
	}
	//current就是word的最后一个字符的Node
	
	//如果当前的node已经是一个word,则不需要添加
	if (!current.isWord) {
		size++;
		current.isWord = true;
	}
}

复制

查找

Trie查找操作就比较简单了,遍历带查找的字符串的字符,如果每个节点都存在,并且待查找字符串的最后一个字符对应的Node的 isWord 属性为 true ,则表示该单词存在,伪代码如下:

public boolean contains(String word) {
	Node current = root;
	for (int i = 0; i < word.length(); i++) {
		char c = word.charAt(i);
		Node node = current.next.get(c);
		if (node == null) {
			return false;
		}
		current = node;
	}
	//current就是word的最后一个字符的Node
	return current.isWord;
}

复制

前缀查询

前缀查询和上面的查询操作基本类似,就是不需要判断 isWord

public boolean containsPrefix(String prefix) {
    Node current = root;
    for (int i = 0; i < prefix.length(); i++) {
        char c = prefix.charAt(i);
        Node node = current.next.get(c);
        if (node == null) {
            return false;
        }
        current = node;
    }
    return true;
}

复制

删除

Trie的删除操作就稍微复杂一些,主要分为以下3种情况:

如果单词是另一个单词的前缀

如果待删除的单词是另一个单词的前缀,只需要把该单词的最后一个节点的 isWord 的改成false

比如Trie中存在 pandapan 这两个单词,删除 pan ,只需要把字符 n 对应的节点的 isWord 改成 false 即可

如下图所示

如果单词的所有字母的都没有多个分支,删除整个单词

如果单词的所有字母的都没有多个分支(也就是说该单词所有的字符对应的Node都只有一个子节点),则删除整个单词

例如要删除如下图的see单词,如下图所示:

如果单词的除了最后一个字母,其他的字母有多个分支

基于链表的Trie字典树

public class Trie {
private Node root;
private int size;
private static class Node {
public boolean isWord;
public Map<Character, Node> next;
public Node() {
next = new TreeMap<>();
}
public Node(boolean isWord) {
this();
this.isWord = isWord;
}
}
public Trie() {
root = new Node();
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 插入操作
*
* @param word 单词
*/
public void add(String word) {
Node current = root;
char[] cs = word.toCharArray();
for (char c : cs) {
Node next = current.next.get(c);
if (next == null) {
current.next.put(c, new Node());
}
current = current.next.get(c);
}
//如果当前的node已经是一个word,则不需要添加
if (!current.isWord) {
size++;
current.isWord = true;
}
}
/**
* 是否包含某个单词
*
* @param word 单词
* @return 存在返回true,反之false
*/
public boolean contains(String word) {
Node current = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
Node node = current.next.get(c);
if (node == null) {
return false;
}
current = node;
}
//如果只存在 panda这个词,查询 pan,虽然有这3个字母,但是并不存在该单词
return current.isWord;
}
/**
* Trie是否包含某个前缀
*
* @param prefix 前缀
* @return
*/
public boolean containsPrefix(String prefix) {
Node current = root;
for (int i = 0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
Node node = current.next.get(c);
if (node == null) {
return false;
}
current = node;
}
return true;
}
/*
* 1,如果单词是另一个单词的前缀,只需要把该word的最后一个节点的isWord的改成false
* 2,如果单词的所有字母的都没有多个分支,删除整个单词
* 3,如果单词的除了最后一个字母,其他的字母有多个分支,
*/
/**
* 删除操作
*
* @param word
* @return
*/
public boolean remove(String word) {
Node multiChildNode = null;
int multiChildNodeIndex = -1;
Node current = root;
for (int i = 0; i < word.length(); i++) {
Node child = current.next.get(word.charAt(i));
//如果Trie中没有这个单词
if (child == null) {
return false;
}
//当前节点的子节点大于1个
if (child.next.size() > 1) {
multiChildNodeIndex = i;
multiChildNode = child;
}
current = child;
}
//如果单词后面还有子节点
if (current.next.size() > 0) {
if (current.isWord) {
current.isWord = false;
size--;
return true;
}
//不存在该单词,该单词只是前缀
return false;
}
//如果单词的所有字母的都没有多个分支,删除整个单词
if (multiChildNodeIndex == -1) {
root.next.remove(word.charAt(0));
size--;
return true;
}
//如果单词的除了最后一个字母,其他的字母有分支
if (multiChildNodeIndex != word.length() - 1) {
multiChildNode.next.remove(word.charAt(multiChildNodeIndex + 1));
size--;
return true;
}
return false;
}
}

复制

基于Trie的Set性能对比

现在使用Trie实现下Set集合,然后三者性能做一个比较,还是以傲慢与偏见双城记战争与和平三本原著作为数据源。

傲慢与偏见(Pride and Prejudice)的性能对比

Pride and Prejudice
Total words: 125901
Total different words: 6530
TrieSet       Time: 0.099788784
BSTSet        Time: 0.339963625
LinkedListSet Time: 3.554973381

复制

从中可以看出傲慢与偏见不同的单词只有6000左右,阅读难度不是很大。

双城记(A Tale of Two Cities)的性能对比

A Tale of Two Cities
Total words: 141489
Total different words: 9944
TrieSet       Time: 0.119505174
BSTSet        Time: 0.331334495
LinkedListSet Time: 5.26063235

复制

战争与和平(War and peace)的性能对比

War and Peace
Total words: 602359
Total different words: 16725
TrieSet       Time: 0.09750872
BSTSet        Time: 0.233328074

复制

以上关于原著词汇的统计只是简单的对比单词是否一致,并没有考虑一个单词的过去式、进行时等时态,只要字符串不一致都把它当作不同的单词。

更多关于Trie的话题

上面实现的Trie中,我们是使用TreeMap来保存节点的所有的子节点,也可以使用HashMap来保存所有的子节点,效率更高:

public Node() {
next = new HashMap<>();
}

复制

当然我们也可以使用一个定长的数组来存储所有的子节点,效率比HashMap更高,因为不需要使用hash函数:

public Node(boolean isWord){
this.isWord = isWord;
next = new Node[26];//只能存储26个小写字母
}

复制

Trie查询效率非常高,但是对空间的消耗还是挺大的,这也是典型的空间换时间。

可以使用 压缩字典树(Compressed Trie) ,但是维护相对来说复杂一些。

如果我们不止存储英文单词,还有其他特殊字符,那么维护子节点的集合可能会更多。

可以对Trie字典树做些限制,比如每个节点只能有3个子节点,左边的节点是小于父节点的,中间的节点是等于父节点的,右边的子节点是大于父节点的,这就是三分搜索Trie字典树(Ternary Search Trie)

LeetCode相关线段树的问题

LeetCode第208号问题

问题描述:

实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。

示例:

Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 true
trie.search("app");     // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");   
trie.search("app");     // 返回 true

复制

问题说明:

你可以假设所有的输入都是由小写字母 a-z 构成的。 保证所有输入均为非空字符串。

这个问题在我们实现的 Trie字典树 中已经实现了这个功能了,add()就是对应的insert(),contains()就是对应的search(),starcontainsPrefix()就是对应的startsWith(),这里就不贴代码了。

LeetCode第211号问题

问题描述:

设计一个支持以下两种操作的数据结构:

void addWord(word)
bool search(word)
search(word) 

复制

可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z. 可以表示任何一个字母。

示例:

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

复制

问题说明:

你可以假设所有单词都是由小写字母 a-z 组成的。

这个问题就是上一个问题的基础上加上 . 的处理,稍微复杂点。

如果下一个字符是 . ,那么需要遍历该节点的所有子节点,对所有子节点的处理就是一个递归程序:

public boolean searchByWildCard(String express) {
return search(root, express, 0);
}
private boolean search(Node node, String express, int index) {
//如果已经到了待查询字符串的尾端了
if (index == express.length()) {
return node.isWord;
}
char c = express.charAt(index);
if (c != '.') {
Node nextChar = node.next.get(c);
if (nextChar == null) {
return false;
}
return search(nextChar, express, index + 1);
} else {//如果是通配符
Map<Character, Node> nextNodes = node.next;
//遍历所有的子节点
for (Map.Entry<Character, Node> entry : nextNodes.entrySet()) {
if (search(entry.getValue(), express, index + 1)) {
return true;
}
}
return false;
}
}

复制

LeetCode第677号问题

问题描述:

实现一个 MapSum 类里的两个方法,insert 和 sum。

对于方法 insert,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。

对于方法 sum,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。

示例 1:

输入: insert("apple", 3), 输出: Null
输入: sum("ap"), 输出: 3
输入: insert("app", 2), 输出: Null
输入: sum("ap"), 输出: 5

复制

总结一句话就是,求出所有符合该前缀的字符串的键值的总和。

节点需要保存一个键值,用于求和。节点Node不需要维护 isWord 这个属性了,因为不关注是不是一个单词。

class Node {
public int value;
public Map<Character, Node> next;
}
public int sum(String prefix) {
Node cur = root;
for (int i = 0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
Node node = cur.next.get(c);
if (node == null) {
return 0;
}
cur = node;
}
//cur指向prefix的最后一个字符的Node
//对每个以prefix为前缀的node进行累加
return countValue(cur);
}
private int countValue(Node node) {
int result = node.value;
for (char c : node.next.keySet()) {
result += countValue(node.next.get(c));
}
return result;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/714257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

zookeeper的动态扩容

附属意义的扩容&#xff1a;扩容的新增节点为观察者observer 1.观察者概念&#xff1a; a.在zookeeper引入此新的zookeeper节点类型为observer&#xff0c;是为了帮助处理投票成本随着追随者增加而增加的问题并且进一步完善了zookeeper的可扩展性 b.观察者不参与投票&#x…

【机器学习】基于卷积神经网络 CNN 的猫狗分类问题

文章目录 一、卷积神经网络的介绍1.1 什么是卷积神经网络1.2 重要层的说明1.3 应用领域二、 软件、环境配置2.1 安装Anaconda2.2 环境准备 三、猫狗分类示例3.1 图像数据预处理3.2 基准模型3.3 数据增强3.4 dropout层四、总结 一、卷积神经网络的介绍 1.1 什么是卷积神经网络 …

决策树ID3

文章目录 题目一基础知识解题过程①算总的信息量②求解各个指标的信息增益&#xff0c;以此比较得出根节点③ 从根节点下的晴天节点出发循环上述步骤④ 从根节点下的多云节点出发&#xff0c;循环上述步骤⑤ 从根节点下的雨节点出发&#xff0c;循环上述步骤⑥画出最终的决策树…

ChatGPT实战:职业生涯规划

ChatGPT的出现&#xff0c;不仅改变了人们对人工智能技术的认识&#xff0c;也对经济社会发展产生了深远的影响。那么&#xff0c;在ChatGPT时代&#xff0c;人们应该如何规划自己的职业呢&#xff1f; 职业规划是一个有意义且重要的过程&#xff0c;它可以帮助你在职业生涯中取…

避坑指南:当你将 Django 项目部署到 Heroku 你需要避多少坑?

文章目录 Cors 跨域问题localhost 阶段Heroku 部署阶段 Procfile 启动文件Database 数据库相关内容localhost 阶段Heroku 部署阶段settings.py 中 正确的设置方式官方给出的 settings.py makemigration & migrate 数据迁移 requirements.txt & runtime.txt 版本和库总结…

如何记录程序运行时间

使用c标准库中时钟类来实现。 使用模板类&#xff1a; chrono::duration<int,ratio<1,2>(20)>前面的int限定了延时单位是一个整数。只要1小时&#xff0c;2小时&#xff0c;但是没有1.5小时。ratio<1,2>代表一个分数。后面的2代表分母&#xff0c;前面的1为分…

Keil5中写的软件延时函数不起作用现象解析_ARM_Compiler_volatile关键字

一、问题描述 在学习野火霸天虎F407寄存器点亮LED时&#xff0c;出现实验现象&#xff1a;LED灯不亮&#xff0c;野火霸天虎F407资料。 main.c代码如下&#xff1a; #include "stm32f4xx.h"void Delay(unsigned int count);int main(void) { #if 0/* 第一步&a…

Axure教程——循环倒计时

本文介绍的是用Axure制作的循环倒计时 效果 预览&#xff1a;https://zhgcck.axshare.com 功能 1、点击“开始”按钮&#xff0c;倒计时开始 2、数值到1时&#xff0c;从10重新倒计时 制作 一、需要的元件 矩形、动态面板 二、制作过程 拖入一个动态面板&#xff0c;命名为…

gnuplot 命令行绘图工具命令

gnuplot命令行绘图工具命令 绘图示例预览 gnuplot工具非常强大&#xff0c;可以在命令行进行曲线绘图&#xff0c;当然也可以在UI界面绘图。 绘图命令&#xff1a; gnuplot> plot test.csv u ($0):1 w lp t c1, test.csv u ($0):2 w lp t c2绘图效果&#xff1a; 数据文…

CSDN 成长记

博客之星入围排行榜 - 2023.5.7 博文 PaddleVideo 简介以及文件目录详解 - 入选内容榜咯 - 2023.5.9 付费专栏 微机系统与接口上机实验_TD PITE型 终于开张咯 - 2023.5.15 博文 ResNet 论文理解含视频 - 入选内容榜第13名 - 2023.5.16 博文 ResNet 论文理解含视频 - 入选全站综…

4.设计模式之后七种模式后11种模式命令访问者迭代器发布订阅中介者忘备录解释器状态策略职责链和空模式

1.命令(command)模式 不知道命令接收者(对象)是谁,支持撤销 (接受者 间接调用执行 的具体行为) 命令调用者和接收者解耦 //只要实现命令接口即可 (就是客户端给个命令,然后命令类传给接收类执行) 优点和缺点 容易撤销操作 命令队列可以多线程操作 增加过多的命令类 空命令也是一…

关于credal set和credal decision tree的一点思考(其实就是论文笔记)

阅读Abelln老师的Credal-C4.5时&#xff0c;发现好难。。。然后又额外补充了一些论文&#xff0c;终于稍微懂一点点了&#xff0c;所以记录如下。 credal set在DS theory的定义如下 [1]&#xff1a; 这句话的意思是&#xff08;证据理论中的&#xff09;credal set是一个概率…

Web安全-Behinder(冰蝎)Webshell管理工具使用

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 工具下载地址02 运行环境03 工具介绍04 使用案例 01 工具下载地址 https://github.com/rebeyond/Behinder点击页面右侧"releases"&#xff0c;进入工具的版本下载页面。 在个人终端安…

经典软件工程复兴?大模型驱动的软件工程实践标准化

TL&#xff1b;DR。 简单来说&#xff0c;本文探讨了大模型驱动的软件工程实践标准化&#xff0c;以及如何将需求和设计规范化为 DSL 格式。通过这种方式&#xff0c;可以让 AI 更自动化、高效地编写代码。 随着大语言模型在软件开发中的应用越来越广泛&#xff0c;传统的软件工…

sourcetree中添加秘钥 - 工具篇

文章目录 1. 首先配置秘钥2. 打开sourcetree 1. 首先配置秘钥 参考文章&#xff1a;https://blog.csdn.net/qq_40968685/article/details/131328007 2. 打开sourcetree 打开“选项” 然后&#xff0c;从gitlab中复制ssh地址到sourcetree中&#xff0c;会看到如图提示&…

【python】ndarray的广播功能

目录 0.环境 1.背景简介 2.ndarray的广播功能 2.1概念 2.2意义 2.3使用前提 2.4举例 2.5完整代码 0.环境 windows jupyter notebook验证代码 1.背景简介 我是在查询【ndarray和array的区别】时&#xff0c;看到了“广播功能”这个词&#xff0c;之前只在网络的相关定义…

分布式版本控制系统Git介绍

Git 是一款开源的分布式版本控制系统&#xff0c;具备分布式、轻量级分支、强大的协作能力以及适用于大小项目的版本管理。本文简要介绍Git工具的特性、Git中的对象以及分支管理&#xff0c;以加深了解。 1、版本控制系统介绍 版本控制是指对软件开发过程中各种程序代码、配置…

Docker是什么以及docker的常用命令

Docker简介 Docker是一种开源的容器化平台&#xff0c;用于构建、部署和运行应用程序。它允许开发人员将应用程序及其所有依赖项打包到一个称为"容器"的独立单元中。这个容器可以在任何支持Docker的环境中运行&#xff0c;无论是开发人员的本地机器、虚拟机还是云服务…

Bert

参考资料&#xff1a; 《机器学习》李宏毅 1 Bert 是怎么运作的&#xff1f; Bert 是一种自监督学习&#xff08;Self-supervised Learning&#xff09;模型。Bert 的目标是 pre-train 出一个能够理解语义的多功能语言模型&#xff0c;使之能够在特定任务上只学习较少的带标…

快速上手 vercel,手把手教你白嫖部署上线你的个人项目

壹 ❀ 关于 vercel Vercel 是一个云服务平台&#xff0c;支持静态网站&#xff08;纯静态页面&#xff0c;比如现在base utils 文档也是基于vercel&#xff09;和动态网站的应用部署、预览和上线。如果你用过 GitHub Pages &#xff0c;那么心里可能不会太陌生&#xff0c;但你…