当前位置: 首页 > news >正文

从零开始理解 Trie 树:高效字符串存储与查找的利器【自动补全、拼写检查】

题目分析

这道题让我们实现一个 Trie 类(也称为前缀树),以便高效地插入和查询字符串。前缀树是一种特殊的树形数据结构,适用于快速存储和检索字符串数据集中的键,比如实现 自动补全拼写检查

题目要求

Trie 类中有以下几个方法:

  1. Trie(): 初始化前缀树对象。
  2. insert(String word): 向前缀树中插入一个字符串 word
  3. search(String word): 如果前缀树中包含 word,返回 true;否则,返回 false
  4. startsWith(String prefix): 如果前缀树中存在以 prefix 为前缀的字符串,则返回 true;否则,返回 false

解题思路

为了解决这个问题,我们可以按照以下步骤来思考:

  1. Trie 数据结构:前缀树用来存储字符串,字符间的共享前缀会共用路径,以减少空间消耗并提高查询效率。每个节点代表一个字符,树的根节点为空节点。沿着从根节点出发的路径到叶节点的字符组合表示存储的字符串。

  2. TrieNode 类设计:前缀树中的每个节点可以使用一个 TrieNode 类来表示。每个 TrieNode 保存两个信息:

    • 子节点列表:存储连接到当前节点的其他字符。
    • 结束标志:用于标记当前节点是否是一个单词的结尾。
  3. Trie 类设计:前缀树本身实现插入、查找和前缀匹配等功能。需要在 Trie 类中实现以下方法:

    • 插入(insert):从根节点开始,逐字符插入,如果字符节点不存在就创建新节点,最后一个字符节点标记为单词结尾。
    • 查找(search):从根节点出发,逐字符查找,直到找到整个单词或发现单词不存在。
    • 前缀匹配(startsWith):类似查找,只需检查前缀是否存在即可。

实现代码

以下是 Trie 类的 C++ 实现,包含插入、查找和前缀匹配功能。

#include <iostream>
#include <unordered_map>
#include <string>class TrieNode {
public:bool isEndOfWord;  // 标记当前节点是否为一个单词的结尾std::unordered_map<char, TrieNode*> children;  // 用于存储子节点TrieNode() : isEndOfWord(false) {}  // 构造函数初始化
};class Trie {
private:TrieNode* root;public:Trie() {root = new TrieNode();  // 初始化 Trie,根节点为空节点}// 插入字符串void insert(const std::string& word) {TrieNode* node = root;for (char ch : word) {if (node->children.find(ch) == node->children.end()) {node->children[ch] = new TrieNode();  // 如果子节点不存在,则创建}node = node->children[ch];  // 移动到下一个子节点}node->isEndOfWord = true;  // 最后一个节点标记为单词结尾}// 查找字符串bool search(const std::string& word) const {TrieNode* node = root;for (char ch : word) {if (node->children.find(ch) == node->children.end()) {return false;  // 如果字符节点不存在,返回 false}node = node->children[ch];  // 移动到下一个子节点}return node->isEndOfWord;  // 判断是否为单词结尾}// 查找前缀bool startsWith(const std::string& prefix) const {TrieNode* node = root;for (char ch : prefix) {if (node->children.find(ch) == node->children.end()) {return false;  // 如果前缀节点不存在,返回 false}node = node->children[ch];  // 移动到下一个子节点}return true;  // 所有字符都找到,返回 true}
};

示例讲解

假设我们运行以下代码:

Trie trie = Trie();
trie.insert("apple");
std::cout << trie.search("apple") << std::endl;    // 返回 true
std::cout << trie.search("app") << std::endl;      // 返回 false
std::cout << trie.startsWith("app") << std::endl;  // 返回 true
trie.insert("app");
std::cout << trie.search("app") << std::endl;      // 返回 true

执行步骤

为了更具象地展示前缀树的构建过程,我们可以用图形化的方式模拟每一步的节点构造。

1. 初始化 Trie
Trie trie = Trie();
  • 创建了一个 Trie 对象。此时只有一个根节点,根节点本身是空的,不保存任何字符内容。
2. 插入字符串 "apple"
trie.insert("apple");

在插入 "apple" 时,前缀树会按字符逐个插入:a -> p -> p -> l -> e。详细步骤如下:

  • 第一步:根节点开始插入字符 'a'。根节点没有子节点 'a',于是创建一个节点表示 'a' 并加入子节点。

        (root)|a
    
  • 第二步:在 'a' 节点下插入字符 'p'。节点 'a' 没有子节点 'p',创建并添加节点表示 'p'

        (root)|a|p
    
  • 第三步:继续在 'p' 节点下插入字符 'p'。节点 'p' 没有子节点 'p',创建并添加节点表示 'p'

        (root)|a|p|p
    
  • 第四步:继续在第二个 'p' 节点下插入字符 'l'。节点 'p' 没有子节点 'l',创建并添加节点表示 'l'

        (root)|a|p|p|l
    
  • 第五步:在 'l' 节点下插入字符 'e'。节点 'l' 没有子节点 'e',创建并添加节点表示 'e',并将其标记为单词结尾 (isEndOfWord = true)。

        (root)|a|p|p|l|e (end)
    

到此为止,我们已经成功将 "apple" 插入到了前缀树中。"apple" 的路径为:a -> p -> p -> l -> e

3. 查找 "apple"
trie.search("apple");
  • 从根节点逐字符查找 a -> p -> p -> l -> e
  • 遇到节点 'e',并且 'e' 被标记为单词的结尾 (isEndOfWord = true),因此返回 true
4. 查找 "app"
trie.search("app");
  • 从根节点逐字符查找 a -> p -> p
  • 到达最后一个 p 节点,但它没有被标记为单词的结尾(isEndOfWord = false),表示 "app" 虽然存在路径,但不是一个完整的单词,因此返回 false
5. 查找前缀 "app"
trie.startsWith("app");
  • 从根节点逐字符查找 a -> p -> p
  • 找到 "app" 路径中的所有字符,所以返回 true 表示 "app" 是某个单词的前缀。
6. 插入 "app"
trie.insert("app");

这次插入 "app" 的过程如下:

  • 从根节点逐字符插入 a -> p -> p

  • 到达最后一个 p 节点,因为节点已存在,无需再创建节点。只需将该节点标记为单词结尾 (isEndOfWord = true)。

        (root)|a|p|p (end)|l|e (end)
    

现在 "app" 路径已标记为一个完整的单词。

7. 查找 "app" 再次验证
trie.search("app");
  • 从根节点逐字符查找 a -> p -> p
  • 找到 p 节点,且 p 已标记为单词的结尾 (isEndOfWord = true),因此返回 true 表示 "app" 存在。

图示总结

通过逐字符的插入和标记,我们最终得到了如下的前缀树结构:

      (root)|a|p|p (end)|l|e (end)
  • "app" 是一条有效路径,以 p (end) 为结尾。
  • "apple" 是另一条有效路径,以 e (end) 为结尾。
小结
  1. 插入过程:逐字符插入,每次检查是否存在子节点,不存在则创建。单词结尾的字符节点会被标记为 isEndOfWord = true
  2. 查找过程:逐字符查找,直到找到整个单词路径。如果路径末尾的节点标记为单词结尾,则该单词存在。
  3. 前缀查找:逐字符查找路径中的前缀,只需验证路径存在,不要求路径末尾节点为单词结尾。

这种树形结构使得共享前缀的单词复用相同路径,节省存储空间并提高了查询效率。

复杂度分析

  • 时间复杂度
    • 插入:O(m),其中 m 为字符串的长度。
    • 查找:O(m)。
    • 前缀查找:O(m)。
  • 空间复杂度:插入 N 个字符串,每个长度为 m,最坏情况下空间复杂度为 O(N * m),但由于前缀共享,这个复杂度会降低。

好的,让我们通过更加通俗、易于理解的语言来解释 自动补全拼写检查 的实现过程,便于大家理解。


应用场景

自动补全和拼写检查是前缀树(Trie)的两个经典应用,它们都需要在大量单词中快速查找具有相同前缀或类似拼写的单词。前缀树的结构使得这些功能可以在短时间内完成。

1. 自动补全

自动补全主要用于在用户输入一部分单词(前缀)时,推荐以该前缀开头的完整单词。例如,当用户输入 "app" 时,系统希望能迅速推荐像 "apple""application" 这样的词。

自动补全的步骤:

  • 第一步:查找前缀
    通过 startsWith 方法,从根节点开始,按用户输入的前缀逐字符向下查找。如果每个字符都能在前缀树中找到,说明该前缀存在;否则,表示 Trie 中没有以该前缀开头的单词。

  • 第二步:找出所有可能的单词
    一旦找到前缀的最后一个字符节点,接下来就是从这个节点开始,通过深度优先遍历(DFS)或广度优先遍历(BFS)找到所有以这个前缀开头的单词。在遍历过程中,只要遇到节点被标记为一个完整单词的结尾,就可以将当前路径组合成一个单词,加入到推荐结果中。

示例:

假设前缀树中存有 "apple""app""application""apply" 等单词。当用户输入 "app" 时,系统将:

  1. 使用 startsWith 方法查找前缀 "app",确认前缀存在。
  2. 从最后的 "p" 节点开始,遍历其子节点,逐一找到以 "app" 开头的完整单词:"apple""application""apply"

这就是自动补全的基本过程,最终返回以 "app" 为开头的推荐单词。


2. 拼写检查

拼写检查用于纠正用户的拼写错误。当用户输入单词时,如果单词不存在,拼写检查会推荐一些拼写相似的正确单词。这里使用前缀树来检查是否存在某个单词,并生成近似的拼写推荐。

拼写检查的步骤:

  • 第一步:检查单词是否存在
    使用 search 方法,直接在前缀树中查找用户输入的单词。如果找到,说明拼写正确;如果找不到,则需要生成拼写相似的候选词。

  • 第二步:生成近似候选词
    如果单词不存在,我们就使用一些简单的修改方式生成可能的“拼写相近”的词,然后逐一在前缀树中查找,看看哪些单词存在。常用的修改方式包括:

    • 替换字符:将单词中的某个字母替换为其他字母,生成新单词并查找。例如,将 "appl" 中的最后一个字符替换为 "e",生成 "apple"
    • 删除字符:尝试删除单词的某个字符,生成新单词并查找。例如,删除 "appl" 中的最后一个字符生成 "app"
    • 添加字符:尝试在某个位置插入一个新字符,生成新单词并查找。例如,在 "appl" 后添加 "e",生成 "apple"
    • 交换字符:尝试交换单词中的相邻字符,生成新单词并查找。

示例:

假设用户输入了 "appl"(少了一个字母 e)。Trie 会先尝试直接查找 "appl",发现不存在,然后生成近似词进行查找:

  • 尝试添加字符 "e",生成 "apple",在 Trie 中找到了该单词。
  • 也可以通过删除字符生成 "app",在 Trie 中也存在。

最终,拼写检查可以将 "apple""app" 作为拼写建议返回给用户。

总结

这道题中的 Trie 数据结构为我们提供了一种快速存储和查询大量字符串的方法,特别适合具有公共前缀的字符串。通过这种数据结构,可以高效地完成自动补全、拼写检查等操作。在实现过程中,我们使用了 TrieNodeTrie 类分别表示节点和整个前缀树,采用 unordered_map 存储子节点关系,实现了 O(m) 时间复杂度的插入和查找功能。

相关文章:

从零开始理解 Trie 树:高效字符串存储与查找的利器【自动补全、拼写检查】

题目分析 这道题让我们实现一个 Trie 类&#xff08;也称为前缀树&#xff09;&#xff0c;以便高效地插入和查询字符串。前缀树是一种特殊的树形数据结构&#xff0c;适用于快速存储和检索字符串数据集中的键&#xff0c;比如实现 自动补全 和 拼写检查。 题目要求 Trie 类…...

关于sse、websocket与流式渲染

一、SSE是什么&#xff1f; 网络中的 SSE (Server-Sent Events) 是一种服务器向浏览器单向推送数据的机制&#xff0c;常用于需要实时更新的数据传输&#xff0c;如新闻推送、股票行情、聊天应用等。 SSE 的特点&#xff1a; 单向通信&#xff1a;服务器向客户端推送数据&…...

Python 语法与数据类型详解

Python 语法与数据类型详解 Python 以其简洁易读的语法和丰富多样的数据类型在编程领域占据重要地位。深入理解 Python 的语法和数据类型是掌握这门语言的关键。 一、Python 语法概述 &#xff08;一&#xff09;缩进规则 Python 独特的缩进规则是其语法的重要特征之一。与…...

LeetCode题练习与总结:扁平化嵌套列表迭代器--341

一、题目描述 给你一个嵌套的整数列表 nestedList 。每个元素要么是一个整数&#xff0c;要么是一个列表&#xff1b;该列表的元素也可能是整数或者是其他列表。请你实现一个迭代器将其扁平化&#xff0c;使之能够遍历这个列表中的所有整数。 实现扁平迭代器类 NestedIterato…...

51单片机快速入门之 AD(模数) DA(数模) 转换 2024/10/25

51单片机快速入门之 AD(模数) DA(数模) 转换 2024/10/25 声明:本文图片来源于网络 A模拟信号特点: 电压或者电流 缓慢上升 随着时间连续缓慢上升或下降 D数字信号特点:电压或者电流 保持一段时间的高/低电平 状态 / 突变 (高电压瞬间低电压) 数字电路中 通常将0-1v电压称…...

Typora 、 Minio and PicGo 图床搭建

流程介绍 本地安装Typora笔记工具拥有一台装有docker的服务器配置minio云图床管理控制页面下载PicGo上传工具服务器Docker环境搭建—Ubuntu系统 删除旧docker的所有依赖(非root用户) # 删除docker及安装时自动安装的所有包 sudo apt-get autoremove docker docker-ce docker…...

【计网】UDP Echo Server与Client实战:从零开始构建简单通信回显程序

目录 前言&#xff1a; 1.实现udpserver类 1.1.创建udp socket 套接字 --- 必须要做的 socket&#xff08;&#xff09;讲解 代码实现&#xff1a;​编辑 代码讲解&#xff1a; 1.2.填充sockaddr_in结构 代码实现&#xff1a; 代码解析&#xff1a; 1.3.bind sockfd和…...

微服务网关Zuul

一、Zuul简介 Zuul是Netflix开源的微服务网关&#xff0c;包含对请求的路由和过滤两个主要功能。 1&#xff09;路由功能&#xff1a;负责将外部请求转发到具体的微服务实例上&#xff0c;是实现外部访问统一入口的基础。 2&#xff09;过滤功能&#xff1a;负责对请求的过程…...

BuildCTF线上赛WP

Build::CTF flag不到啊战队--WP 萌新战队&#xff0c;还请多多指教~ 目录 Build::CTF flag不到啊战队--WP Web ez!http find-the-id Pwn 我要成为沙威玛传奇 Misc what is this? 一念愚即般若绝&#xff0c;一念智即般若生 别真给我开盒了哥 四妹&#xff0c;你听…...

《使用Gin框架构建分布式应用》阅读笔记:p143-p207

《用Gin框架构建分布式应用》学习第10天&#xff0c;p143-p207总结&#xff0c;总计65页。 一、技术总结 1.auth0 本人实际工作中未遇到过&#xff0c;mark一下&#xff0c;参考&#xff1a;https://auth0.com/。 2.使用template (1)c.File() (2)router.Static() (3)rou…...

华为网络管理配置实例

目录 组网需求 数据规划 配置思路 操作步骤 结果验证 配置脚本 管理员可以通过eSight网管系统对FW进行监控和管理&#xff0c;接收FW的告警。 组网需求 如图1所示&#xff0c;某企业在网络边界处部署了FW作为安全网关&#xff0c;并部署了eSight网管系统对网络设备进行集中…...

大语言模型数据处理方法(基于llama模型)

文章目录 前言一、基于huggingface的DataCollatorForSeq2Seq方法解读1、DataCollatorForSeq2Seq方法2、batch最长序列填充3、指定长度填充二、构建大语言模型数据加工模块1、数据读取2、数据加工1、数据格式2、预训练(pretrain)数据加工3、微调(sft)数据加工①、sft数据加工…...

爱奇艺大数据多 AZ 统一调度架构

01# 导语 爱奇艺大数据技术广泛应用于运营决策、用户增长、广告分发、视频推荐、搜索、会员营销等场景&#xff0c;为公司的业务增长和用户体验提供了重要的数据驱动引擎。 多年来&#xff0c;随着公司业务的发展&#xff0c;爱奇艺大数据平台已积累了海量数据&#xff0c;这…...

【C++篇】栈的层叠与队列的流动:在 STL 的韵律中探寻数据结构的优雅之舞

文章目录 C 栈与队列详解&#xff1a;基础与进阶应用前言第一章&#xff1a;栈的介绍与使用1.1 栈的介绍1.2 栈的使用1.2.1 最小栈1.2.2 示例与输出 1.3 栈的模拟实现 第二章&#xff1a;队列的介绍与使用2.1 队列的介绍2.2 队列的使用2.2.1 示例与输出 2.3 队列的模拟实现2.3.…...

使用 Flask 实现简单的登录注册功能

目录 1. 引言 2. 环境准备 3. 数据库设置 4. Flask 应用基本配置 5. 实现用户注册 6. 实现用户登录 7. 路由配置 8. 创建前端页面 9. 结论 1. 引言 在这篇文章中&#xff0c;我们将使用 Flask 框架创建一个简单的登录和注册系统。Flask 是一个轻量级的 Python Web 框架…...

计算机毕业设计Python+大模型微博情感分析 微博舆情预测 微博爬虫 微博大数据 舆情分析系统 大数据毕业设计 NLP文本分类 机器学习 深度学习 AI

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 《Python大模型微博情感分析…...

CTF--Misc题型小结

&#xff08;萌新笔记&#xff0c;多多关照&#xff0c;不足之处请及时提出。&#xff09; 不定时更新~ 目录 密码学相关 文件类型判断 file命令 文件头类型 strings读取 隐写术 尺寸修改 文件头等缺失 EXIF隐写 thumbnail 隐写 文件分离&提取 binwalk foremo…...

深度学习系列——RNN/LSTM/GRU,seq2seq/attention机制

1、RNN/LSTM/GRU可参考&#xff1a; https://zhuanlan.zhihu.com/p/636756912 &#xff08;1&#xff09;对于这里面RNN的表示中&#xff0c;使用了输入x和h的拼接描述&#xff0c;其他公式中也是如此 &#xff08;2&#xff09;各符号图含义如下 2、关于RNN细节&#xff0c;…...

通过call指令来学习指令摘要表的细节

E8 cw cw 表示E8后面跟随2 字节 (什么数不知道) rel16 指在与指令同一代码段内的相对地址偏移 D ,指向Instruction Operand Encoding 表中的D列, 他告诉我们 操作数1 是一个0FFSET N.S. 在64位模式下&#xff0c;某些指令需要使用“地址覆盖前缀”&#xff08;address over…...

10分钟使用Strapi(无头CMS)生成基于Node.js的API接口,告别繁琐开发,保姆级教程,持续更新中。

一、什么是Strapi&#xff1f; Strapi 是一个开源的无头&#xff08;headless&#xff09; CMS&#xff0c;开发者可以自由选择他们喜欢的开发工具和框架&#xff0c;内容编辑人员使用自有的应用程序来管理和分发他们的内容。得益于插件系统&#xff0c;Strapi 是一个灵活的 C…...

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现

目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》

在注意力分散、内容高度同质化的时代&#xff0c;情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现&#xff0c;消费者对内容的“有感”程度&#xff0c;正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中&#xff0…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

相机从app启动流程

一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

uniapp微信小程序视频实时流+pc端预览方案

方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度​WebSocket图片帧​定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐​RTMP推流​TRTC/即构SDK推流❌ 付费方案 &#xff08;部分有免费额度&#x…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

免费PDF转图片工具

免费PDF转图片工具 一款简单易用的PDF转图片工具&#xff0c;可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件&#xff0c;也不需要在线上传文件&#xff0c;保护您的隐私。 工具截图 主要特点 &#x1f680; 快速转换&#xff1a;本地转换&#xff0c;无需等待上…...