【恋上数据结构】前缀树 Tire 学习笔记

Tire

需求分析

如何判断一堆不重复的字符串是否以某个前缀开头?

  • Set\Map 存储字符串(不重复)
  • 遍历所有字符串进行判断
  • 缺点:时间复杂度 O(n)

有没有更优的数据结构实现前缀搜索?

Tire(和 Tree 同音)

简介

  • Trie 也叫做字典树、前缀树 (Prefix Tree)、单词查找树。
  • Trie 搜索字符串的效率主要跟字符串的长度有关。

假设使用 Trie 存储 catdogdoggydoescastadd 六个单词,结果如下所示

在这里插入图片描述

接口设计

有两种设计方案:

  1. 第一种仅仅是存储字符串。(像 set 集合)
  2. 第二种是存储字符串的同时可以再存储一个 value(像 map 接口)

分析:

第二种设计方案更为通用,比如说我们要做一个通讯录,以某个人的姓名作为 key,然后以他的详细信息作为 value(其他电话号码、邮箱、生日等各种详细信息)

public interface Trie <V> {
	int size(); 
	boolean isEmpty(); 
	void clear(); 
	boolean contains(String str); 
	V add(String str, V value); 
	V remove(String str); 
	boolean starswith(String prefix);
}

在这里插入图片描述

Node 设计

孩子节点集合解析(HashMap<Character, Node<V>> children;):

  • key 相当于代表的是路径值,Character 字符类型可以是英文也可以是中文
  • value 是嵌套了当前节点下的所有子节点,方便后面节点值寻找
  • word:true 为已存储单词(红色),false 为非单词(蓝色)
    /**
     * Trie 中的节点类,包含父节点、孩子节点集合、字符、值以及表示是否为一个完整单词的标志。
     *
     * @param <V> 值的类型
     */
    private static class Node<V> {
        Node<V> parent; // 父节点
        HashMap<Character, Node<V>> children; // 孩子节点集合
        Character character; // 字符,为删除做准备
        V value; // 节点对应的值,也就是整个单词
        boolean word; // 是否为单词的结尾(是否为一个完整的单词)

        /**
         * 构造函数,初始化节点时需要指定父节点。(在添加节点时用到)
         *
         * @param parent 父节点
         */
        public Node(Node<V> parent) {
            this.parent = parent;
        }
    }

完整代码实现附注释

/**
 * Trie(字典树)数据结构,用于存储字符串集合,支持添加、查询、删除等操作。
 *
 * @param <V> 值的类型
 */
public class Trie<V> {

    /** Trie 中存储的单词数量 */
    private int size;

    /** 根节点 */
    private Node<V> root;

    /**
     * Trie 中的节点类,包含父节点、孩子节点集合、字符、值以及表示是否为一个完整单词的标志。
     *
     * @param <V> 值的类型
     */
    private static class Node<V> {
        Node<V> parent; // 父节点
        HashMap<Character, Node<V>> children; // 孩子节点集合
        Character character; // 字符,为删除做准备
        V value; // 节点对应的值,也就是整个单词
        boolean word; // 是否为单词的结尾(是否为一个完整的单词)

        /**
         * 构造函数,初始化节点时需要指定父节点。(在添加节点时用到)
         *
         * @param parent 父节点
         */
        public Node(Node<V> parent) {
            this.parent = parent;
        }
    }

    /**
     * 获取 Trie 中存储的单词数量。
     *
     * @return Trie 中存储的单词数量
     */
    public int size() {
        return size;
    }

    /**
     * 判断 Trie 是否为空。
     *
     * @return 如果 Trie 为空,则返回 true;否则返回 false
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 清空 Trie,将单词数量重置为 0。
     */
    public void clear() {
        size = 0;
        root = null;
    }

    /**
     * 根据指定的键获取对应的值。
     *
     * @param key 键
     * @return 如果键存在且是一个完整的单词,则返回对应的值;否则返回 null
     */
    public V get(String key) {
        Node<V> node = node(key);
        return (node != null && node.word) ? node.value : null;
    }

    /**
     * 判断 Trie 是否包含指定的键。
     *
     * @param key 键
     * @return 如果 Trie 包含指定的键且是一个完整的单词,则返回 true;否则返回 false
     */
    public boolean contains(String key) {
        Node<V> node = node(key);
        return node != null && node.word;
    }

    /**
     * 添加键值对到 Trie 中。如果键已经存在,则更新对应的值;否则新增一个单词。
     *
     * @param key   键
     * @param value 值
     * @return 如果添加的键已经存在,则返回对应的旧值;否则返回 null
     */
    public V add(String key, V value) {
        keyCheck(key);

        // 创建根节点
        if (root == null) {
            root = new Node<>(null);
        }

        // 获取 Trie 根节点
        Node<V> node = root;
        // 获取键的长度
        int len = key.length();
        // 遍历键的每个字符
        for (int i = 0; i < len; i++) {
            // 获取当前字符
            char c = key.charAt(i);
            // 判断当前节点的孩子节点集合是否为空
            boolean emptyChildren = (node.children == null);
            // 获取当前字符对应的孩子节点
            Node<V> childNode = emptyChildren ? null : node.children.get(c);
            // 如果当前字符对应的孩子节点为空,说明该字符在当前节点的孩子节点集合中不存在
            if (childNode == null) {
                // 创建新的孩子节点,并将其加入到当前节点的孩子节点集合中
                childNode = new Node<>(node);
                childNode.character = c;
                // 判断孩子节点集合是否为空的同时,避免了每次都要创建新的 HashMap 对象,提高了效率
                node.children = emptyChildren ? new HashMap<>(16) : node.children;
                node.children.put(c, childNode);
            }
            // 将当前节点移动到其对应的孩子节点上,继续下一层的遍历
            node = childNode;
        }

        // 1 - 已经存在这个单词, 覆盖, 返回旧值
        if (node.word) {
            V oldValue = node.value;
            node.value = value;
            return oldValue;
        }

        // 2 - 不存在这个单词, 新增这个单词
        node.word = true;
        node.value = value;
        size++;
        return null;
    }

    /**
     * 移除 Trie 中的指定键。如果键存在且是一个完整的单词,将其从 Trie 中移除。
     *
     * @param key 键
     * @return 如果键存在且是一个完整的单词,则返回对应的值;否则返回 null
     */
    public V remove(String key) {
        Node<V> node = node(key);
        
        // 如果不是单词结尾,不用作任何处理
        if (node == null || !node.word) {
            return null;
        }
        size--;
        V oldValue = node.value;

        // 如果还有子节点
        if (node.children != null && !node.children.isEmpty()) {
            node.word = false;
            node.value = null;
            return oldValue;
        }

        // 没有子节点
        Node<V> parent = null;
        while ((parent = node.parent) != null) {
            parent.children.remove(node.character);
            if (parent.word || !parent.children.isEmpty()) {
                break;
            }
            node = parent;
        }
        return oldValue;
    }

    /**
     * 判断 Trie 是否包含指定前缀。
     *
     * @param prefix 前缀
     * @return 如果 Trie 包含指定前缀,则返回 true;否则返回 false
     */
    public boolean startsWith(String prefix) {
        return node(prefix) != null;
    }

    /**
     * 根据传入字符串,找到最后一个节点。
     * 例如输入 dog
     * 找到 g
     *
     * @param key 键
     * @return 如果键存在,则返回对应的节点;否则返回 null
     */
    private Node<V> node(String key) {
        keyCheck(key);

        Node<V> node = root;
        int len = key.length();
        for (int i = 0; i < len; i++) {
            if (node == null || node.children == null || node.children.isEmpty()) {
                return null;
            }
            char c = key.charAt(i);
            node = node.children.get(c);
        }
        return node;
    }

    /**
     * 检查键是否合法,不允许为空。
     *
     * @param key 键
     */
    private void keyCheck(String key) {
        if (key == null || key.length() == 0) {
            throw new IllegalArgumentException("key must not be empty");
        }
    }
}

总结

  1. Trie 的优点:搜索前缀的效率主要跟前缀的长度有关

  2. Trie 的缺点:需要耗费大量的内存(一个字符一个节点),因此还有待改进

  3. 更多 Trie 相关的数据结构和算法

    • Double-array Trie、Suffix Tree(后缀树)、Patricia Tree、Crit-bit Tree、AC 自动机

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

快速整合EasyExcel实现Excel的上传下载

1.EasyExcel 2.Excel的上传&#xff08;读Excel&#xff09; 3.Excel的下载&#xff08;写Excel&#xff09; 4.结语 1.EasyExcel 首先&#xff0c;这里给出EasyExcel的官方文档&#xff1a;https://easyexcel.opensource.alibaba.com/ alibaba.com不用我多说了吧&#xff0c;大…

2023-12-05 Qt学习总结2

点击 <C 语言编程核心突破> 快速C语言入门 Qt学习总结 前言五 Hello Qt!六 Qt控件和事件七 Qt信号和槽八 Qt自定义信号和槽总结 前言 要解决问题: 学习qt最核心知识, 多一个都不学. 五 Hello Qt! 现在我们已经有了一个空窗口工程, 传统上, 我们要实现一个"Hello …

HXDSP2441-Demo板

板卡图示 下图为HXDSP2441DEMO板&#xff0c;HXDSP2441DEMO板是围绕HXDSP2441构建的芯片演示验证平台。 板卡简介 除了为HXDSP2441芯片提供供电、时钟、储存、网络及调试电路&#xff0c;来实现芯片最基本的功能&#xff0c;也添加了相关模块以搭建HXDSP2441的典型应用场景…

活久见—当设置不同坐标系统时,ArcMap中的图形相关位置关系会变化

这两天一件十分神奇的事情发生了&#xff1a;当设置不同坐标系统时&#xff0c;ArcMap中的图形相对位置关系会变化。 事情起因是这样的&#xff1a;博主和同行用ArcMap同时验证2个相邻多边形的相对位置关系&#xff0c;见下图图1和图2的多边形&#xff0c;在博主的ArcMap中&am…

快速登录界面关于如何登录以及多账号列表解析以及config配置文件如何读取以及JsLogin模块与SdoLogin模块如何通信(4)

1、### Jslogin模块与前端以及JsLogin模块与Sdologin的交互 配置文件的读取: <CompanyIdForQq value"301"/> <CompanyIdForWx value"300"/><CompanyIdForWb value"302"/><qq value"https://graph.qq.com/oauth2.0/au…

爱智EdgerOS之深入解析如何应用爱智的视频流模块完成拉流

一、ONVIF 规范和常见视频流传输协议 ① ONVIF 规范 随着视频监控产业链的成熟&#xff0c;市面上陆陆续续出现了各式各样的网络摄像设备&#xff0c;这些设备都需要通讯协议才能进行数据传输。早期厂商都采用私有协议&#xff0c;但是现在厂商分工明确&#xff0c;有的负责生…

计算机毕业设计springboot+ssm停车场车位预约系统java

管理员不可以注册账号 停车位包括车位所在楼层、车位编号、车位类型(全时间开放/高峰期开放)、预定状态等 用户预约时要求支付预约时间段的停车费用 违规行为&#xff1a;1.停车超过预约时间段 2.预约未使用 于系统的基本要求 &#xff08;1&#xff09;功能要求&am…

【go-zero】go-zero使用ent框架 如何使用源生sql完成查询

背景 本篇教程我们采用的是go-zero的快速脚手架工具 simple-admin 框架的开发 github地址:https://github.com/suyuan32/simple-admin-core 因为框架推荐使用Ent 这篇教程我们则对Ent的基本使用的几种形式进行一个总结 一、开启ent的源生sql 1、simple-admin生成rpc 【go-…

BUUCTF [CISCN2019 华北赛区 Day2 Web1]Hack World 1(SQL注入之布尔盲注)

题目环境判断注入类型 1 2 3 1’ 输入1’报错提示bool(false) 可知是字符型的布尔注入&#xff08;盲注&#xff09; 尝试万能密码 1’ or ‘1’1 已检测SQL注入 猜测某些关键字或者字符被过滤 FUZZ字典爆破 可以看到部分关键字被过滤&#xff0c;包括空格 All You Want Is In …

[面试题~Docker] 云原生必问基础篇

文章目录 基础相关1. Docker 是什么&#xff1f;2. 镜像是什么3. 容器是什么4. 数据卷是什么5. Docker 和虚拟机的区别&#xff1f;6. Docker 常用命令有哪些&#xff1f; 原理相关1. docker 有几种网络模式host 模式container模式none模式bridge模式 2. docker 网络实现在Linu…

AWTK 串口屏开发(1) - Hello World

1. 功能 这个例子很简单&#xff0c;制作一个调节温度的界面。在这里例子中&#xff0c;模型&#xff08;也就是数据&#xff09;里只有一个温度变量&#xff1a; 变量名数据类型功能说明温度整数温度。范围 (0-100) 摄氏度 2. 创建项目 从模板创建项目&#xff0c;将 hmi/…

物奇平台CLI MIC切换对应ADC关系

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)&#xff1f;可加我微信hezkz17, 本群提供音频技术答疑服务&#xff0c;群赠送语音信号处理降噪算法&#xff0c;蓝牙耳机音频&#xff0c;DSP音频项目核心开发资料, 物奇平台CLI MIC切换对应ADC关系 1 发送CLI指令…

tqdm详细教程,实现tqdm进度条完美设计;解决进度条多行一直刷新的问题;如何使得滚动条不上下滚动(保持一行内滚动)

一、tqdm简介 tqdm是一个python进度条库&#xff0c;可以在 Python长循环中添加一个进度提示信息。 二、3种使用方法 1.tqdm(range)-自动更新 import time from tqdm import range# 自动更新 for i in tqdm(range(10)): # 共可以更新10次进度条time. Sleep(0.5) # 每次更新间…

leetcode系列:反转链表的形象表示

反转链表是一道比较简单的题&#xff0c;主要考察的是对链表数据结构的理解和双指针应用&#xff0c;比较容易出错的地方是指针的移动顺序。在练习的过程中想到了一个比较形象的表示方法&#xff0c;于是记录下来。 # Definition for singly-linked list. # class ListNode: #…

高效便捷的淘宝商品详情关键词搜索API接口

联讯数据可以介绍一些高效便捷的淘宝商品详情关键词搜索API接口。 以下是一些可以考虑使用的API接口&#xff1a; 阿里云搜索引擎API&#xff1a;阿里云搜索引擎API是一个基于云计算技术的搜索引擎&#xff0c;提供商品详情关键词搜索功能。它支持中文搜索&#xff0c;并且具…

听GPT 讲Rust源代码--src/tools(12)

File: rust/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs 在Rust源代码中&#xff0c;rust/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs文件的作用是定义和解析rust-analyzer的配置文件。该文件包含了各种配置项的数据结构和枚举类型&#xf…

Mysql、Oracle安全项检查表及操作脚本

软件开发全资料获取&#xff1a;点我获取 Mysql检查表 Oracle检查表

基于微群机器人的二次开发

请求URL&#xff1a; http://域名地址/modifyGroupName 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 参数&#xff1a; 参数名必选类型说明wId是String登录实例标识chatRoom…

vue3自定义路由栈pinia+vue-router来实现

我们知道vue-router使用push跳转可以通过浏览器的返回箭头&#xff0c;就能回到上一页&#xff0c;是因为它有路由栈&#xff0c;每次跳转都会存放&#xff08;记录&#xff09;当前路由的相关信息。 根据这个思路我们利用piniavue-router 来自定义路由栈。 说下项目中 自定义…

前端知识(九)------------JavaScript底层知识

1.事件循环机制 在实际的编码过程中小伙伴们不知道有没有遇到过这样的问题&#xff0c;我们都知道js是单线程的。而且是一门解释型语言。那么正常来讲执行代码的顺序就是自上而下一句一句执行对吧 但是有的时候我们发现返回的结果并不是自上而下执行的。我们先写了一段代码 …