字典树(Trie/前缀树)详解

news2024/11/17 19:45:15

目录

字典树的概念

字典树的逻辑

字典树的实现

字典树小结

例题强化


字典树的概念

字典树(Trie)是一种空间换时间的数据结构,是一棵关于“字典”的树,主要用于统计、排序和保存大量的字符串。字典树是通过利用字符串的公共前缀来节约存储空间,因此字典树又叫前缀树。字典树是对于字典的一种存储方式。这个词典中的每个“单词”就是从根节点出发一直到某一个目标节点的路径,路径中每条边的字母连起来就是一个单词。

字典树的逻辑

在学习字典树之前我们先来看这样一个问题:

给你n个单词,并进行x次查找,每次查找随机一个单词word。

问:word是否出现在这n个单词中?

想想看,这像不像平常我们查字典?比如要在字典中查找单词“abandon”,我们一般是找到首字母为'a'的部分,然后再找第二个单词为‘b’的部分······如果字典中存在这个单词,那么我们最后就可以找到这个单词,反之则不能。

接下来,我们通过图解存储了单词{“a”,"abc",“bac”,“bbc”,"ca" }的字典树对上方内容进行解释:

非原图,如有侵权,请联系作者

从这张图我们可以看出,这棵字典树的每条边上都有一个字母(可以类比查字典时查找的第k个字母来理解),并且这棵树的一些节点被指定成了标记节点,用于表示到此为止是一个完整的单词。

字典树的实现

通过上面的图不难发现,字典树并不是二叉树,所以字典树的结构定义和二叉树的并不同,这里不要惯性思维了。通常我们定义多叉树时常用孩子表示法,像这样

struct TreeNode {
    ELEMENT_TYPE value;    //结点值
    TreeNode* children[NUM];    //孩子结点
};

字典树也类似:

struct TrieNode {
    int isEnd; //该结点是否是一个串的结束
    TrieNode* children[SIZE]; //字母映射表
};

常规的多叉树类型一般是,一个成员存放当前节点的内容,如上方的value成员;一个成员存放所有孩子节点的地址,如上方的children[NUM]成员,其数组中存放的是一个个TreeNode*类型的孩子节点的地址。

而字典树却与其有些不同,字典树的一个成员存放的是boo/intl型的isEnd成员,用于判断当前节点是否为一个单词的结束,如果是就标记为true,这样做的目的是在查找的时候方便确定找到最后是不是一个单词,因为并不是一个单词中所有的子串都是一个单词,例如”abandon“是一个单词,而其子串“ab“就不是一个单词。其另一个成员存放的也是一个指向孩子节点的指针,与常规的多叉树理解类似。

这里也许你会发现一些”端倪“:字典树的结构里好像并没有用于存储节点内容的成员,比如常规多叉树中的value成员。其实就是没有,但这并不妨碍我们实现这个功能,例如我们暂定字典中只有小写的26个字母,那么字典树的children[SIZE] 成员中SIZE就可以设置为26,那么我们就可以存储,那么我们将要查找或是存储的单词每一个字母(的ASCII值)减去一个'a' (字符a),那么其对应的值,就是我们要存放的children数组下标。了解过哈希表(散列表)的同学肯定会有一种茅塞顿开的感觉,因为这里其实就是用的哈希表的思路。下标为k的元素如果不为空(数组中的每一个元素都是一个指针)就说明这个元素有孩子节点,进而说明这个元素对应的字母是存在的。其中,一般第n层的children数组的下标k位置如果不为空,就表示这个单词中的第n个位置存在k下标位置所对应的字母。例如包含单词 "sea","sells","she" 的字典树图解如下:

非原图,如有侵权,请联系作者


在这里,我们还可以试着讨论一下为什么这个children成员用数组实现。(这一小部分内容与字典树的核心内容并没有多大关系,如果没有兴趣可以直接跳过)

相信有部分人(比如我)第一次接触字典树时对于

TrieNode* children[SIZE]

这种的定义是很难认同的,因为我们可能会觉得这会浪费很多空间,为什么不用链表等其它顺序结构来实现呢?

        首先,如果将数组替换为链表或其它线性结构,那么实现起来会非常繁琐,而且即使实现了,每次查找字母时都要进行一次链表的遍历,相较于数组的直接下标访问,是一个很费时的过程。

        其次,这里的数组其实就是一个哈希表,哈希表的一大优点就是用空间换时间,而且一个良好的哈希表结构是必须要留有一定的空闲位置的,所以这种实现方式也并非不妥。

        也许会有人想到用C++中的map/unordered_map来实现,这确实是可行的。首先,如果用map容器替换数组的话,实现起来并不是很难,理论上相较于哈希表确实会节省空间,但map的底层实现是红黑树,用一个红黑树去实现一个字典树,是不是有点小题大做了呢?其次,对于unordered_map来说,其本身就是一个哈希容器,与使用数组的本质是一样的,所以当然可行啦。

字典树小结

字典树的实现是一种哈希表和树的结合。字典树的每次插入和查找操作的时间复杂度都是O(NlogN)的,而且为了防止删除操作对树操作不可逆的损坏,所以我们一般都是在结构体中额外增加一个成员,用以表示当前节点是否被删除(专业术语叫“惰性删除”)。

例题强化

原题链接: 208. 实现 Trie (前缀树) 

  • 题目描述:

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
     

示例:

输入
        ["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
        [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
        [null, null, true, false, true, null, true]

解释
        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

提示:

        1 <= word.length, prefix.length <= 2000
        word 和 prefix 仅由小写英文字母组成
        insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/implement-trie-prefix-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 参考答案:

C语言版本

typedef struct TrieNode{
    int isEnd;
    struct TrieNode* children[26];
} Trie;


Trie* trieCreate() 
{
    Trie* trie = (Trie*) calloc (1,sizeof(Trie));
    return trie;
}

void trieInsert(Trie* obj, char * word) 
{
    int len = strlen(word);
    Trie* node = obj;
    for(int i = 0; i < len; i++)
    {
        if(node->children[word[i] - 'a'] == NULL) 
            node->children[word[i] - 'a'] =  (Trie*) calloc (1,sizeof(Trie));
        node = node->children[word[i] - 'a'];
    }
    node->isEnd = true;
}


bool trieSearch(Trie* obj, char * word) 
{
    int len = strlen(word);
    Trie* node = obj;
    for(int i = 0; i < len; i++)
    {
        if(node->children[word[i] - 'a'] == NULL)
            return false;
        node = node->children[word[i] - 'a'];
    }
    return node->isEnd;
}

bool trieStartsWith(Trie* obj, char * prefix) 
{
    int len = strlen(prefix);
    Trie* node = obj;
    for(int i = 0; i < len; i++)
    {
        if(node->children[prefix[i] - 'a'] == NULL)
            return 0;
        node = node->children[prefix[i] - 'a'];
    }
    return true;
}

void trieFree(Trie* obj) 
{
    for(int i = 0; i < 26; i++)
    {
        if(obj->children[i] != NULL)
            trieFree(obj->children[i]);
    }
    free(obj);
}

/**
 * Your Trie struct will be instantiated and called as such:
 * Trie* obj = trieCreate();
 * trieInsert(obj, word);
 
 * bool param_2 = trieSearch(obj, word);
 
 * bool param_3 = trieStartsWith(obj, prefix);
 
 * trieFree(obj);
*/

C++版本

class Trie {
private:
    bool isEnd; 
    vector<Trie*> children;
public:
    Trie() : isEnd(false) , children(26) {}
    //插入操作
    void insert(string word) 
    {
        Trie* node = this;
        for(char ch : word) 
        {
            if(node->children[ch - 'a'] == nullptr)
            {
                node->children[ch - 'a'] = new Trie();
            }
            //相对于node = node->next;
            node = node->children[ch - 'a'];       
        }
        node->isEnd = true;
    }
    //查找单词
    bool search(string word) 
    {
        Trie* node = this;
        for(char ch : word) 
        {
            if(node->children[ch - 'a'] == nullptr) 
                return false;
            node = node->children[ch - 'a'];
        }
        return node->isEnd; 
    }
    //查找前缀
    bool startsWith(string prefix) 
    {
        Trie* node = this;
        for(char ch : prefix) 
        {
            if(node->children[ch - 'a'] == nullptr) 
                return false;
            node = node->children[ch - 'a'];
        }
        return true;
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */

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

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

相关文章

【unity实战】随机地下城生成2——绘制地图Tilemap的使用及一些技巧的使用(含源码)

绘制房间 修改素材配置 切割图片 绘制瓦片地图 先新建我们的调色盘,保存好位置 拖入我们刚才切片好的素材 在房间预设体创建我们的瓦片地图 绘制地图的小技巧 点选移动适合的瓦片移动到位置上绘画 框选复制已经绘制好的地图,快捷键i

【C# .NET】chapter 13 使用多任务改进性能和可扩展性

目录 一、物理内存和虚拟内存使用&#xff08;Recorder 类&#xff09; 二、 对比 string的“”操作与stringbuilder 操作 的处理效率&#xff0c;内存消耗情况&#xff0c; 三、异步运行任务、三种启动任务方法、将上一任务方法处理结果作为参数传给下一任务方法 四、嵌套…

妙记多 Mojidoc |构建企业和个人专属知识库

虽然市面上有很多项目管理工具&#xff0c;但对于小公司或初创企业来说&#xff0c;它们要么年费太贵&#xff0c;要么流程和操作太复杂&#xff0c;无法满足小团队的需求。 如果您有基本需求并希望使用低成本的文档工具&#xff0c;作为团队共享知识库和工作流程管理的工具&a…

【GIS学习笔记】快速上手GeoServer,并发布 Shapefile /PostGis / PostgreSQL

文章目录 安装配置1、下载2、配置环境 快速上手 &#xff08;Shapfile文件发布&#xff09;1、登录2、图层预览 Layer Preview3、发布Shapefile4、创建工作空间Workspace5、创建一个 Store&#xff08;数据存储&#xff09;6、创建图层报错解决 7、发布图层 快速上手&#xff0…

一款表现不佳的游戏,如何能爆火出圈?

根据《2022年中国游戏产业报告》&#xff0c;2022年中国游戏市场实际销售收入2658.84亿元&#xff0c;同比下降10.33%。游戏用户规模6.64亿&#xff0c;同比下降0.33%。继2021年规模增长明显放缓之后&#xff0c;又出现过去八年来的首次下降&#xff0c;表明产业发展已进入存量…

使用Dom元素的animate实现无限滚动列表

一、需求 实现一个内容重复滚动的列表&#xff0c;鼠标hover时滚动停止&#xff0c;鼠标移走&#xff0c;继续滚动 二、实现逻辑与代码 这个需求用到了一个dom API&#xff08;animate&#xff09;&#xff0c;这个方法可以用来做过渡动画、关键帧动画&#xff0c;接收两个参…

全景丨0基础学习VR全景制作,平台篇第八章:全局功能-说一说管理

一、功能说明 蛙色VR的全景能够允许用户发布说一说&#xff0c;这些说一说的信息会同步呈现在全景中。 鼓励用户参与到内容中来&#xff0c;增强了全景的互动性&#xff0c;体验感更好&#xff0c;也更容易引导用户分享。 此外&#xff0c;后台系统可以对说一说进行必要的审核…

C++数据结构:二叉搜索树的结构、模拟实现及应用

目录 一. 二叉搜索树的结构 二. 二叉搜索树的模拟实现 2.1 二叉搜索树的节点及类的成员变量 2.2 数据插入操作Insert的实现 2.3 数据删除操作Erase的实现 2.4 数据查找操作Find的实现 2.5 中序遍历InOrder的实现 2.6 构造函数的实现 2.7 析构函数的实现 三. 二叉搜…

第三方软件测试公司与开发人员在进行测试时有什么不一样?

随着科技信息的发展&#xff0c;软件企业要想在市场上站住脚&#xff0c;就必须在产品质量上下功夫。高质量的软件也是开发、测试、质量保证等相关人员共同追求的目标&#xff0c;用户往往会选择体验性、服务性以及安全性较强的软件产品。软件测试可以很好的检测出软件质量&…

基于web的小型餐厅餐饮饭馆供货订货系统asp.net+sqlserver

本研究课题重点主要包括了下面几大模块&#xff1a;用户登录&#xff0c;管理员信息管理&#xff0c;类别信息管理&#xff0c;商家信息管理&#xff0c;商品信息管理&#xff0c;订单信息管理&#xff0c;损耗信息管理&#xff0c;退货信息管理&#xff0c;修改密码等功能。。…

2023-热门ChatGPT解析及使用方法

什么是Chat GPT&#xff1f;我们能用它来干什么&#xff1f; Chat GPT是一款基于人工智能技术的自然语言处理模型&#xff0c;由OpenAI团队开发。它能够通过机器学习技术从海量文本数据中学习语言知识&#xff0c;实现自然语言生成、对话生成和语言理解等功能&#xff0c;使得…

144. 二叉树的前序遍历【78】

难度等级&#xff1a;容易 上一篇算法&#xff1a; 102. 二叉树的层序遍历【206】 力扣此题地址&#xff1a; 144. 二叉树的前序遍历 - 力扣&#xff08;Leetcode&#xff09; 1.题目&#xff1a;144. 二叉树的前序遍历 给你二叉树的根节点 root &#xff0c;返回它节点值的 前…

电源常识-PCB材质防火等级焊锡工艺

1、目前主流的PCB材质分类主要有以下几种,如图1&#xff0c;图2&#xff0c;图3。FR-4材质比CEM-1好&#xff0c;CEM-1比FR-1好。 按结构分为单面板&#xff0c;双面板&#xff0c;多层板。单面板就是单面铺铜走线&#xff0c;双面板就是上下两面都可以铺铜走线&#xff0c;多层…

JavaWeb综合案例

综合案例 1 查询所有 1.1 后端实现 1.1.1 dao方法实现 在 com.itheima.mapper.BrandMapper 接口中定义抽象方法&#xff0c;并使用 Select 注解编写 sql 语句 /*** 查询所有* return*/ Select("select * from tb_brand") List<Brand> selectAll();由于表中…

leetcode 376. 摆动序列

思路没想到就很难&#xff0c;&#xff0c;&#xff0c;&#xff0c;&#xff0c;&#xff0c;看了题解就觉得&#xff0c;还可以 加个图吧&#xff0c;贪心这玩意。。 我之前的困惑就在于&#xff1a; 不知道如何判断 正负规律&#xff0c;发现我双指针的思想用错了。 我一开…

react-7 组件库 Ant Design

1.安装组件库 npm install --save antd-mobile 常用组件 tabbar 底部导航 Swiper 轮播图&#xff08;走马灯&#xff09; NavBar&#xff08;顶部返回累&#xff09; 配合 Dialog&#xff0c;Toast InfiniteScroll 无限滚动&#xff08;实现下拉刷新&#xff09; Skeleto…

沃顿商学院6个最受欢迎的工商管理课程

沃顿商学院创立于1881年&#xff0c;是美国第一所大学商学院。它的故事开始于企业家约瑟夫沃顿&#xff08;Joseph Wharton&#xff09;&#xff0c;他出生于一个费城富有的商业家庭&#xff0c;通过经营佰利恒钢铁公司和美国镍公司积累了大量的财富。在1881年他55岁时&#xf…

借由Net5.5G,看到运营商的新沧海

我们都记得这样一句诗&#xff1a;“东临碣石&#xff0c;以观沧海”。 想要看到沧海的壮阔波澜&#xff0c;就先要抵达碣石山这样可以看到大海的地方。在数字化的发展过程中&#xff0c;往往一个技术或产业趋势就是一座碣石山&#xff0c;借由它可以看到描绘着未来机遇的新沧海…

pandas笔记:tseries.offset

进行date的偏移 1 各种offset 1.1 DateOffset 1.1.1 基本使用方法 class pandas.tseries.offsets.DateOffset n 偏移量表示的时间段数。 如果没有指定时间模式&#xff0c;则默认为n天。 normalize是否将DateOffset偏移的结果向下舍入到前一天午夜**kwds 添加到偏移量的时…

ROS学习第十节——参数服务器

前言&#xff1a;本小节主要是对于参数服务器参数的修改&#xff0c;需要掌握操作参数的函数使用 1.基本介绍 参数服务器实现是最为简单的&#xff0c;该模型如下图所示,该模型中涉及到三个角色: ROS Master (管理者)Talker (参数设置者)Listener (参数调用者) ROS Master …