如何高效地存储和查找大量字符串或前缀?比如自动补全、拼写检查、敏感词过滤等场景,都对字符串的处理速度有很高要求。哈希表虽然查找快,但并不擅长前缀匹配;普通树结构虽然灵活,但对于大量字符串的处理效率并不理想。

这时候,Trie(发音类似“try”,又称前缀树、字典树)作为一种专为字符串检索优化的数据结构,成为了解决这类问题的利器。它不仅能高效完成字符串的插入、查找、前缀搜索,还能拓展到处理整数、支持合并等高级应用。

什么是 Trie 树?

Trie 树(前缀树、字典树)是一种多叉树结构,主要用于高效地存储和检索字符串集合,尤其擅长处理前缀相关的查询问题。每个节点通常表示一个字符,根节点为空。树中的每一条从根到叶子的路径,都对应一个字符串。

Trie 的结构特点

  • 每个节点代表一个字符:但节点本身不保存字符串,只记录字符和子节点。
  • 从根节点到某一节点的路径,拼接起来即为某个字符串的前缀
  • 单词的结束可以用布尔标记或计数来表示:通常会用一个布尔变量isEndcount等来表示以当前节点结尾的字符串数量。

在实际生产中, Trie 的在自动补全、拼写检查与纠错、敏感词检测与过滤、前缀计数、单词统计和处理与二进制相关的高效查询(如最大异或值)中都有应用

Trie 树的 C++ 实现

假定仅使用 a-z 字符集。Trie 树主要有三种常用操作:插入(Insert)、查询(Search)和前缀判断(StartsWith),有些场景下还需要删除(Delete)和合并(Merge)等高级操作。其核心思想是将每个字符串按字符拆分,逐层存储在多叉树中。

  • 插入操作:从根节点开始,依次遍历字符串的每个字符。如果对应字符的子节点不存在,则新建节点。插入结束后,将最后一个字符节点标记为“单词结尾”。
  • 查询操作:同样从根节点出发,依次按字符查找子节点,若全部存在且最后一个节点是“单词结尾”,则表示单词存在。
  • 前缀查询:与查找类似,但只要能走到最后一个字符即可,不必判断是否为“单词结尾”。
  • 删除操作:需要回溯删除冗余节点,但实际场景中较少使用。
  • 合并操作:将两个 Trie 树合并为一个,常用于多源数据的合并处理。

节点结构设计

struct TrieNode {
TrieNode* children[26]; // 指向26个字母的子节点
bool isEnd; // 是否为一个单词的结尾 TrieNode() : isEnd(false) {
for (int i = 0; i < 26; ++i) children[i] = nullptr;
}
};

Trie 类的基本操作

class Trie {
private:
TrieNode* root; public:
Trie() {
root = new TrieNode();
} // 插入单词
void insert(const std::string& word) {
TrieNode* node = root;
for (char ch : word) {
int idx = ch - 'a';
if (!node->children[idx])
node->children[idx] = new TrieNode();
node = node->children[idx];
}
node->isEnd = true;
} // 查找完整单词
bool search(const std::string& word) {
TrieNode* node = root;
for (char ch : word) {
int idx = ch - 'a';
if (!node->children[idx])
return false;
node = node->children[idx];
}
return node->isEnd;
} // 判断是否有某个前缀
bool startsWith(const std::string& prefix) {
TrieNode* node = root;
for (char ch : prefix) {
int idx = ch - 'a';
if (!node->children[idx])
return false;
node = node->children[idx];
}
return true;
} // 删除单词(可选,简化版)
bool remove(const std::string& word) {
return remove(root, word, 0);
} private:
// 递归删除单词辅助函数
bool remove(TrieNode* node, const std::string& word, int depth) {
if (!node) return false;
if (depth == word.size()) {
if (!node->isEnd) return false;
node->isEnd = false;
return isEmpty(node); // 是否可以安全删除该节点
}
int idx = word[depth] - 'a';
if (remove(node->children[idx], word, depth + 1)) {
delete node->children[idx];
node->children[idx] = nullptr;
return !node->isEnd && isEmpty(node);
}
return false;
} // 判断节点是否没有任何子节点
bool isEmpty(TrieNode* node) {
for (int i = 0; i < 26; ++i)
if (node->children[i]) return false;
return true;
}
};

Trie 树的合并

在某些应用中,我们可能需要将两个 Trie 树合并。可以使用递归合并两个节点。

// 将 src 的内容合并到 dest 上
void mergeTrie(TrieNode* dest, TrieNode* src) {
if (!src) return;
if (src->isEnd) dest->isEnd = true;
for (int i = 0; i < 26; ++i) {
if (src->children[i]) {
if (!dest->children[i])
dest->children[i] = new TrieNode();
mergeTrie(dest->children[i], src->children[i]);
}
}
} mergeTrie(trie1.root, trie2.root);

复杂度分析

在限定一个较小字符集的情况下,字典树的复杂度是线性的:

  • 插入、查询、前缀判断的时间复杂度均为 \(O(L)\),L 为字符串长度,与集合规模无关。
  • 空间复杂度最坏为 $O(N * L) $,N为单词数,L为平均长度。

当然可以!下面是**Trie 树在整数上的应用(01-Trie)**这一部分的详细讲解:

01-Trie 树处理整数

Trie 不仅可以用于字符串处理,其思想同样可以用来高效处理整数序列,尤其是涉及二进制位运算的问题。这里常见的做法是将每个整数按二进制位拆解,从高位到低位依次插入到 Trie 树中,这种结构被称为01-Trie(或二进制 Trie)。

01-Trie 的原理

假定我们要处理一些 32 位无符号整数,可以认为:将其二进制表示(一个32位长的01字符串)视为一个字符串存储

  • 节点含义:每个节点有两个子节点,分别代表 0 和 1 两种可能(即当前二进制位是 0 还是 1)。
  • 存储过程:将每个整数拆为固定长度(如 32 位)的二进制序列,从最高位(31)到最低位(0)插入(特殊情况下也可能从低到高)。
  • 查找过程:与字符串 Trie 类似,通过遍历对应的二进制位进行路径选择。

典型应用:最大异或对

给定一个整数数组,找出数组中任意两个数的最大异或值。

核心思路

  • 对每个数,将其二进制形式插入到 Trie 树;
  • 查询时,希望每一位都取与当前位相反的分支,以获取更大的异或值;
  • 对每个数分别查询并更新最大异或结果。
struct TrieNode {
TrieNode* children[2];
TrieNode() { children[0] = children[1] = nullptr; }
}; class Trie01 {
private:
TrieNode* root;
public:
Trie01() { root = new TrieNode(); } // 插入一个数的二进制表示
void insert(int num) {
TrieNode* node = root;
for (int i = 31; i >= 0; --i) { // 以32位为例
int bit = (num >> i) & 1;
if (!node->children[bit])
node->children[bit] = new TrieNode();
node = node->children[bit];
}
} // 查询与num异或结果最大的数
int query(int num) {
TrieNode* node = root;
int res = 0;
for (int i = 31; i >= 0; --i) {
int bit = (num >> i) & 1;
int desired = bit ^ 1; // 希望找相反的位
if (node->children[desired]) {
res |= (1 << i);
node = node->children[desired];
} else {
node = node->children[bit];
}
}
return res;
}
};

01-Trie 分支固定为2(0/1)。用于二进制位、最大异或、区间问题、计数相关的问题。可以在节点中记录通过该节点的数字个数,实现删除、计数等高级操作。对于负数,可以通过补码直接处理。

好的,下面是Trie 树如何处理大字符集这一部分的详细讲解:


Trie 树如何处理大字符集

在前面的实现中,我们使用的是仅包含 26 个小写字母的 Trie。此时,每个节点只需维护 26 个指针(children 数组),空间和查询效率都很可控。但如果字符集变大,比如:

  • 包含大小写英文字母(A-Z, a-z):52
  • 包含所有 ASCII 可见字符:128
  • 支持 Unicode 或中日韩字符:几千甚至几万

那么,Trie 的空间复杂度会随字符集大小 \(C\) 线性增长

  • 每个节点需要 \(O(C)\) 的空间
  • 假设有 \(N\) 个字符串,每个字符串长度为 L,则最坏空间复杂度为 $ O(N * L * C)$ 。

在超大字符集下,Trie 的空间浪费会非常明显。即使实际数据量远小于全部可能字符,仍然需要为每个节点预留完整的 children 数组。

优化 Trie 的常用方法

1. 动态结构替代定长数组

  • unordered_map<char, TrieNode*>map<char, TrieNode*>

    用哈希表或平衡树来动态存储存在的子节点,只为出现过的字符分配空间,极大降低空间浪费。

    struct TrieNode {
    unordered_map<char, TrieNode*> children;
    bool isEnd = false;
    };
  • 对于字符集非常稀疏或不连续的情况,这种方式尤其有效。

2. 压缩 Trie(又称字典树压缩,Radix Tree/Patricia Trie)

当我们用压缩 Trie(又称 Radix Tree 或 Patricia Trie)时,Trie 节点不再仅仅保存单个字符,而是保存一段字符串。其基本思想是:

  • 在 Trie 中遇到只有一个子节点的“链路”时,可以将这段连续的字符合并成一个节点,节点保存字符串片段(比如 "abc"),而不是一个字符。
  • 只有遇到分叉(即出现多个分支)时才拆分。

结构变更如下:

struct RadixNode {
string label; // 当前节点代表的字符串片段
unordered_map<char, RadixNode*> children;
bool isEnd = false;
};

插入和查找时,需要在每一步将目标字符串与节点的 label 进行最长公共前缀匹配,然后再判断是完全匹配、部分匹配还是完全不匹配。若部分匹配,则需要将当前节点分裂成两部分。

这能有效减少链式节点和极度稀疏节点,节省空间。查询时实际访问节点数大幅减少,提升长串的处理效率。

(root)
├── "ap"
│ ├── "ple" (isEnd)
│ └── "ricot" (isEnd)
└── "bee" (isEnd)

压缩 Trie 特别适用于存储大量有公共前缀的长字符串数据,可以让 Trie 在空间与速度上都更高效。

3. 混合使用

  • 小字符集用定长数组(查询速度快)。
  • 大字符集用哈希表或平衡树(整体略慢于定长数组,但可节省大量空间。)。

Trie 字典树的原理和应用解析的更多相关文章

  1. 标准Trie字典树学习二:Java实现方式之一

    特别声明: 博文主要是学习过程中的知识整理,以便之后的查阅回顾.部分内容来源于网络(如有摘录未标注请指出).内容如有差错,也欢迎指正! 系列文章: 1. 标准Trie字典树学习一:原理解析 2.标准T ...

  2. 萌新笔记——C++里创建 Trie字典树(中文词典)(一)(插入、遍历)

    萌新做词典第一篇,做得不好,还请指正,谢谢大佬! 写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示 ...

  3. Trie字典树 动态内存

    Trie字典树 #include "stdio.h" #include "iostream" #include "malloc.h" #in ...

  4. 算法导论:Trie字典树

    1. 概述 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树. Trie一词来自retrieve,发音为/tr ...

  5. 817E. Choosing The Commander trie字典树

    LINK 题意:现有3种操作 加入一个值,删除一个值,询问pi^x<k的个数 思路:很像以前lightoj上写过的01异或的字典树,用字典树维护数求异或值即可 /** @Date : 2017- ...

  6. C++里创建 Trie字典树(中文词典)(一)(插入、遍历)

    萌新做词典第一篇,做得不好,还请指正,谢谢大佬! 写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示 ...

  7. 数据结构 -- Trie字典树

    简介 字典树:又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种. 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高. 性质:   1.  根节 ...

  8. 踹树(Trie 字典树)

    Trie 字典树 ~~ 比 KMP 简单多了,无脑子选手学不会KMP,不会结论题~~ 自己懒得造图了OI WIKI 真棒 字典树大概长这么个亚子 呕吼真棒 就是将读进去的字符串根据当前的字符是什么和所 ...

  9. 标准Trie字典树学习一:原理解析

    特别声明: 博文主要是学习过程中的知识整理,以便之后的查阅回顾.部分内容来源于网络(如有摘录未标注请指出).内容如有差错,也欢迎指正! 系列文章: 1. 字典树Trie学习一:原理解析 2.字典树Tr ...

  10. Trie(字典树)解析及其在编程竞赛中的典型应用举例

    摘要: 本文主要讲解了Trie的基本思想和原理,实现了几种常见的Trie构造方法,着重讲解Trie在编程竞赛中的一些典型应用. 什么是Trie? 如何构建一个Trie? Trie在编程竞赛中的典型应用 ...

随机推荐

  1. 窗体添加按钮--java进阶day03

    1.组件.面板对象 窗体中的图片.按钮.文本都是组件,光创建出了窗体没有组件肯定不行,但是这些组件该放到窗体的哪个位置? 很明显是窗体中空白的位置,但是我们需要知道,这块空白位置在窗体中是一个被封装的 ...

  2. 康谋方案 | 基于AI自适应迭代的边缘场景探索方案

    构建巨量的驾驶场景时,测试ADAS和AD系统面临着巨大挑战,如传统的实验设计(Design of Experiments, DoE)方法难以有效覆盖识别驾驶边缘场景案例,但这些边缘案例恰恰是进一步提升 ...

  3. python初学之random()模块

    ##python小脚本 random()是不能直接访问的,需要导入 random 模块,然后通过 random 静态对象调用该方法. random.random()用于生成 一个指定范围内的随机符点数 ...

  4. GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架

    GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架 项目介绍 GoView 是一个Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件 ...

  5. Spring基于xml的CRUD

    目录 基于xml的CRUD 代码实现 测试 基于xml的CRUD 源码 使用C3P0连接池 使用dbutils包中的QueryRunner类来对数据库进行操作 代码实现 pom.xml <?xm ...

  6. 改进NeteaseCloudMusicGtk4:添加移除歌曲按钮

    之前已经发了一篇博客简述了如何阅读这个项目,尽管这个项目已经开源很久了,但我找了很久都没有找到怎么从播放列表移除歌曲,那就自己动手实现,再提个 PR 吧. 运行起来应用后通过 Inspector(Ct ...

  7. 【记录】Python3|Python出现循环引用模块怎么办?(又称循环依赖)

    前言 在Python开发过程中,尤其是在大型项目中,我们经常会遇到模块间相互依赖的情况.这种相互依赖,即所谓的"循环引用",往往会导致代码难以维护,并可能引发各种运行时问题.在这篇 ...

  8. FMEA方法,排除架构可用性隐患的利器

    极客时间:<从 0 开始学架构>:FMEA方法,排除架构可用性隐患的利器 FMEA 方法,就是保证我们做到全面分析的一个非常简单但是非常有效的方法. 1.FMEA 介绍 FMEA(Fail ...

  9. TGCTF-misc全解

    TGCTF-misc方向wp next is the end 下载压缩包,拉出第一层文件夹,直接嵌套读取内容找flag import os def is_last_level_dir(director ...

  10. Itex+freemarker 导出PDF文件时✓无法正常显示

    在使用Itex+freemarker 导出PDF文件时✓无法正常显示 在网上看到了以下思路:经过实验后是靠谱的 1.首先打开一个word文件,输入这个特殊字符,然后在字体选择框里看见这个特殊字符所用的 ...