云计算百科
云计算领域专业知识百科平台

【算法分享】字典树 —插入、查询与状态标记详解

字典树(Trie):高效的字符串处理利器 🌳

在处理大量字符串数据时,我们经常需要进行前缀匹配、单词查找、自动补全等操作。如果使用普通的数组或者哈希表,虽然能够解决问题,但是效率都不是很理想。那我们来学习一种专门为字符串而设计的数据结构——字典树(Trie)。

什么是字典树? 🤔

字典树,又称前缀树或单词查找树,是一种有序树结构,用于保存关联数组,其中的键通常是字符串。它的名字来源于retrieval(检索),因为它能够快速检索字符串。

字典树的特点 ✨

  • 前缀共享 🔗:具有公共前缀的字符串会共享相同的路径
  • 空间优化 💾:相比存储完整字符串,可以节省大量存储空间
  • 查找高效 ⚡:查找时间复杂度为O(m),其中m为字符串长度
  • 支持前缀操作 🔍:天然支持前缀匹配、自动补全等功能

字典树的结构 🏗️

字典树是一个多叉树,每个节点包含:

  • 指向子节点的指针数组(通常为26个,对应a-z)
  • 一个标记位,表示该节点是否为某个单词的结尾

struct node {
bool end = false; // 标记位
int count = 0;
node *n[26] = {nullptr};
};

示例:插入单词 “cat”, “car”, “card”, “care”, “careful”

root
|
c
|
a
/ \\
t r
| |
(end)
/ \\
d e
| |
(end) (end)
|
f
|
u
|
l
|
(end)

字典树的基本操作 ⚙️

1. 插入操作 (Insert) ➕

从根节点开始,按照字符串的每个字符逐层向下构建路径。如果路径不存在,则创建新节点。

时间复杂度:O(m),m为字符串长度 ⏱️

void add(node* root, string s) {
node *p = root;
for (char c : s) {
int i = c 'a';

// 如果节点路径不存在,就创建一个新的节点
if (!p->n[i])
p->n[i] = new node();
p = p->n[i];
}
// 标记单词结尾
p->end = true;
}

2. 查找操作 (Search) 🔎

从根节点开始,沿着字符路径向下查找。只有当路径存在且最后节点标记为单词结尾时,才表示找到了完整单词。

时间复杂度:O(m) ⏱️

bool search(node* root, string s) {
node *p = root;
for (char c : s) {
int i = c 'a';

// 单词路径不存在
if (!p->n[i])
return false;

// 继续往下一个节点走
p = p->n[i];
}

// 必须检查是否为单词结尾
return p->end;
}

3. 前缀查找 (StartsWith) 🔍

与完整单词查找类似,但不需要检查最后一个节点是否为单词结尾,只需要路径存在即可。

关键区别:

  • 完整单词查找:return node->end – 必须是完整单词
  • 前缀查找:return true – 只要路径存在即可

实际例子: 假设字典树中有单词:[“car”, “card”, “care”, “careful”]

  • search("car") → true("car"是完整单词)
  • search("ca") → false("ca"不是完整单词)
  • startsWith("ca") → true(存在以"ca"开头的单词)
  • startsWith("car") → true(存在以"car"开头的单词)

bool startsWith(node* root, string s) {
node *p = root;
for (char c : s) {
int i = c 'a';
if (!p->n[i])
return false;
p = p->n[i];
}

// 只要能走完整个前缀路径就返回true,不需要检查是否是end
return true;
}

例题示例 🚀

题目概述(于是他错误的点名开始了)

题目背景 一个化学竞赛教练在点名时因一边玩游戏一边点名,导致连续点到同一同学两次。现在需要写程序判断点名是否正确。

具体需求 给定班级合法学生名单(名字互不相同)。

按顺序读取点名的名字。

对每个名字输出:

  • OK:名字在名单中,且是第一次出现。
  • REPEAT:名字在名单中,但已经出现过。
  • WRONG:名字不在名单中。

代码实现 💻

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef unsigned long long ll;

// 定义结点结构
struct node {
bool end = false;
bool isre = false;
node *n[26] = {nullptr};
};

// 添加字符到结点
void add(node *root, string s) {
node *p = root;
for (char c : s) {
int i = c 'a';
if (!p->n[i]) {
p->n[i] = new node();
}
p = p->n[i];
}
p->end = true;
}

// 查找
int search(node* root, string s) {
node *p = root;
for (char c : s) {
int i = c 'a';
if (!p->n[i]) // 路径断了 -> 单词不存在
return 0;
p = p->n[i];
}

// 路径存在,但不是单词结尾
if (!p->end) {
return 0;
}

// 已经被查询过
if (p->isre) {
return 2;
}

p->isre = true;
return 1;
}

// 创建根节点
node* createT() {
return new node();
}

int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);

// 初始化 Trie 根节点
node *root = createT();

int n;
cin >> n;
for (int i = 0; i < n; i++) {
string s;
cin >> s;
add(root, s); // 插入单词到字典树
}

int m;
cin >> m;
while (m) {
string s;
cin >> s;
int l = search(root, s);
if (l == 0)
cout << "WRONG" << endl;
else if (l == 1)
cout << "OK" << endl;
else
cout << "REPEAT" << endl;
}
return 0;
}

复杂度分析 📊

操作时间复杂度空间复杂度
插入 O(m) O(ALPHABET_SIZE × N × M)
查找 O(m)
前缀查找 O(m)

其中:

  • m:字符串长度
  • N:字典树中的单词数量
  • M:单词的平均长度
  • ALPHABET_SIZE:字符集大小(如26个英文字母)

总结 📝

字典树是一种非常实用的数据结构,特别适合处理字符串相关的问题。虽然在某些情况下可能占用较多内存,但其在前缀操作上的高效性能使其在很多场景下都是最佳选择。

理解和掌握字典树不仅能帮助我们解决实际问题,也为学习更复杂的字符串算法(如AC自动机、后缀树等)打下坚实基础。


赞(0)
未经允许不得转载:网硕互联帮助中心 » 【算法分享】字典树 —插入、查询与状态标记详解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!