目录

字典树的概念

字典树的逻辑

字典树的实现

易混点剖析

代码示例


字典树的概念

字典树(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来说,其本身就是一个哈希容器,与使用数组的本质是一样的,所以当然可行啦。

易混点剖析

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

2、不要将当前字母和当前节点弄混淆了。字典树的每一个节点都是一个“字母表”,并不是代指哪一个具体的字母。同时这也是字典树的特点:像字典一样,每次遍历到字符 ch 时,当前字典树节点不是直接代表这个 ch 的,而是表示当前节点的下一个字母是ch。不过,我们可以将其子节点看作代表一个固定的字符,如果是这样,那么字典树的根节点不代表任何字符。所以如果word字符串和trieTree(字典树)同时遍历的话,那么当前节点就表示在“字典”中,当前字符是否存在,而真正代表这个字符的是当前节点对应的一个字节点(next)。

3、 字典树的增加操作很好解决,但删除和析构的维护成本就比较大了。因为C/C++不想java那样是完全封装好的,需要自己手动释放掉被删除的节点,而字典树的节点又异常的庞大(一般至少有26个next指针),所以字典树的释放就极大的增加了字典树的维护难度。

代码示例

#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;class trieTree
{
private:int passBy;bool isEnd;trieTree* nexts[26];
public://构造函数trieTree() :passBy(0), isEnd(0), nexts() {}//插入单词void insert(string word);//查找单词是否存在bool contains(string word);//查找前缀数量int pre_size(string pre);//删除单词void erase(string word);
};
void test1()
{trieTree* my_trie = new trieTree();my_trie->insert("apple");my_trie->insert("apple");my_trie->insert("banana");my_trie->insert("orange");my_trie->insert("applogise");my_trie->insert("app");my_trie->erase("applogise");cout << my_trie->contains("apple") << endl;cout << my_trie->contains("appl") << endl;cout << my_trie->contains("banana") << endl;cout << my_trie->contains("pear") << endl;cout << my_trie->pre_size("") << endl;cout << my_trie->pre_size("app") << endl;cout << my_trie->pre_size("appl") << endl;cout << my_trie->pre_size("ore") << endl;
}
class trieTree_byHash
{
private:int pass;bool end;unordered_map<char, trieTree_byHash*> nexts;
public:trieTree_byHash() :pass(0), end(0), nexts() {}//插入单词void insert(string word);//查找单词是否存在bool contains(string word);//查找前缀数量int pre_size(string pre);//删除单词void erase(string word);
};
void test2()
{trieTree_byHash* my_trie = new trieTree_byHash();my_trie->insert("apple");my_trie->insert("apple");my_trie->insert("banana");my_trie->insert("orange");my_trie->insert("applogise");my_trie->insert("app");my_trie->erase("applogise");cout << my_trie->contains("app") << endl;cout << my_trie->contains("appl") << endl;cout << my_trie->contains("banana") << endl;cout << my_trie->contains("pear") << endl;cout << my_trie->pre_size("") << endl;cout << my_trie->pre_size("app") << endl;cout << my_trie->pre_size("appl") << endl;cout << my_trie->pre_size("ore") << endl;}int main()
{test1();cout << endl;test2();return 0;
}/*固定数组实现前缀树*/
//插入单词
void trieTree::insert(string word)
{//不重复插入单词if (this->contains(word))return;this->passBy++;trieTree* cur = this;for (char ch : word){if (cur->nexts[ch - 'a'] == nullptr)cur->nexts[ch - 'a'] = new trieTree();cur = cur->nexts[ch - 'a'];cur->passBy++;}cur->isEnd = true;
}
//查找单词是否存在
bool trieTree::contains(string word)
{trieTree* cur = this;for (char ch : word){if (cur->nexts[ch - 'a'] == nullptr)return false;cur = cur->nexts[ch - 'a'];}return cur->isEnd;
}
//查找前缀数量
int trieTree::pre_size(string pre)
{trieTree* cur = this;for (char ch : pre){if (cur->nexts[ch - 'a'] == nullptr)return 0;cur = cur->nexts[ch - 'a'];}return cur->passBy;
}
//删除单词
void trieTree::erase(string word)
{if (!this->contains(word))return;trieTree* cur = this;cur->passBy--;int index = 0;while (--cur->nexts[word[index] - 'a']->passBy != 0){cur = cur->nexts[word[index++] - 'a'];}//由于不会出现一样的单词,所以最后至少有一个节点的passBy为1//接着从index位置开始,将后面的节点全部deletetrieTree* next = cur->nexts[word[index] - 'a'];cur->nexts[word[index] - 'a'] = nullptr;cur = next;while (cur != nullptr){next = index + 1 < word.size() ? cur->nexts[word[++index] - 'a'] : nullptr;delete cur;cur = next;}
}/*哈希表实现前缀树*/
//插入单词
void trieTree_byHash::insert(string word)
{if (this->contains(word))return;this->pass++;trieTree_byHash* cur = this;for (char ch : word){if (cur->nexts.find(ch) == cur->nexts.end())cur->nexts[ch] = new trieTree_byHash();cur = cur->nexts[ch];cur->pass++;}cur->end = true;
}
//查找单词是否存在
bool trieTree_byHash::contains(string word)
{trieTree_byHash* cur = this;for (char ch : word){if (cur->nexts.find(ch) == cur->nexts.end())return false;cur = cur->nexts[ch];}return cur->end;
}
//查找前缀数量
int trieTree_byHash::pre_size(string pre)
{trieTree_byHash* cur = this;for (char ch : pre){if (cur->nexts.find(ch) == cur->nexts.end())return 0;cur = cur->nexts[ch];}return cur->pass;
}
//删除单词
void trieTree_byHash::erase(string word)
{if (!this->contains(word) && this->pass == 0)return;trieTree_byHash* cur = this;cur->pass--;int index = 0;while (index < word.size() && --cur->nexts[word[index]]->pass != 0){cur = cur->nexts[word[index++]];}trieTree_byHash* next = cur->nexts[word[index]];cur->nexts[word[index]] = nullptr;cur = cur->nexts[word[index]];while (cur != nullptr){next = index + 1 < word.size() ? cur->nexts[word[++index]] : nullptr;delete cur;cur = next;}
}

字典树(Trie/前缀树)相关推荐

  1. 字典树/Trie/前缀树-LeetCode总结:720词典中最长的单词;127. 单词接龙;677. 键值映射;面试题 17.17. 多次搜索;648. 单词替换

    MyTrie结构体和相关操作函数 typedef struct MyTrie {bool is_word;vector<MyTrie*> next;MyTrie():is_word(fal ...

  2. leetcode 676. Implement Magic Dictionary | 676. 实现一个魔法字典(DFS+Trie 前缀树)

    题目 https://leetcode.com/problems/implement-magic-dictionary/description/ 题解 题意理解 前缀树问题,大意是是让你在字典中找到是 ...

  3. 分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)

    目录 Trie树(字典树.前缀树)的基础知识 字典树的节点表示 字典树构造的例子 字典树的前序遍历 获取字典树中全部单词 字典树的整体功能 字典树的插入操作 字典树的搜索操作 字典树的前缀查询 字典树 ...

  4. 实现字典树(前缀树、Trie树)并详解其应用

    今天看到一个比较好的数据结构,字典树,做一下记录,以供自己后期复习和读者朋友的参考. 1.定义 字典树又称单词查找树.前缀树.Trie树等,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序 ...

  5. trie(字典树、前缀树)

    trie(字典树.前缀树) 1. trie原理 原理 trie树,又被称为字典树.前缀树,是一种高效地存储和查找字符串集合的数据结构. 一般来说,用到trie的题目中的字母要么全是小写字母,要么全是大 ...

  6. 208. 实现 Trie (前缀树)

    208. 实现 Trie (前缀树) Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键.这一数据结构有相当多的应用情景,例 ...

  7. Leetcode 208.实现 Trie (前缀树)(Implement Trie (Prefix Tree))

    Leetcode 208.实现 Trie (前缀树) 1 题目描述(Leetcode题目链接)   实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三 ...

  8. Leetcode典型题解答和分析、归纳和汇总——T208(实现Trie前缀树)

    问题描述: 实现一个Trie前缀树,包含insert.search和startsWith这三个操作. 问题分析: 这类的题目与堆栈的最小元素查找类似,将所有功能进行集中处理. 首先我们需要明确一下tr ...

  9. LeetCode 208. 实现 Trie (前缀树) —— 提供一套前缀树模板

    208. 实现 Trie (前缀树) Ideas 前缀树嘛,直接套模板咯,把之前写的拿过来抄一遍. 提供一下我的模板. Code Python class TrieNode:def __init__( ...

  10. leetcode 677. Map Sum Pairs | 677. 键值映射(Trie前缀树,BFS)

    题目 https://leetcode.com/problems/map-sum-pairs/ 题解 基于前缀树实现,可以参考:leetcode 208. Implement Trie (Prefix ...

最新文章

  1. linux tmux离线安装,linux环境下安装tmux
  2. tcpdump的简单选项介绍
  3. 苹果电脑安装python-在Mac上安装Python环境
  4. Spring Boot 发起 HTTP 请求
  5. 082_Timing事件
  6. mysql go命令行_Go语言调用mysql.exe和mysqldump命令行导入导出数据库
  7. nx set 怎么实现的原子性_【redis进阶(1)】redis的Lua脚本控制(原子性)
  8. appium---【Mac】Appium-Doctor提示WARN:“ opencv4nodejs cannot be found”解决方案
  9. 一步步编写操作系统 28 cpu乱序执行
  10. 2021年五月下旬推荐文章(2)
  11. java系列5:如何使用创建的类
  12. B00012 C++算法库的sort()函数
  13. JVM第一节:内存结构
  14. 【九天教您南方cass 9.1】 08 绘制等高线及对其处理
  15. 爬取百度贴吧发帖信息并保存到scv文件中
  16. 专访徐小平:AI已进入日常生活 没有泡沫只有彩虹
  17. 使用plupload压缩图片
  18. python毕业设计总结范文大全_java毕业设计总结报告(精选范文3篇)
  19. linux学习记录(二)
  20. 错误Could not locate executable null\bin\winutils.exe in the Hadoop binaries的解决方案

热门文章

  1. Liunx ubuntu 局域网IP设置(需重启服务器)
  2. ElasticSearch学习篇2_Rest格式操作(索引、文档)、文档的简单操作(增、删、改、查)、复杂查询操作(排序、分页、高亮)
  3. ai怎么画循环曲线_Illustrator绘制循环的矢量印花图稿
  4. 全媒体呼叫中心解决方案缔造企业品牌价值
  5. 蘑菇街2016研发工程师_投篮游戏
  6. three.js案例解析之游戏帧碰撞检测
  7. 广东金融学院计算机实验报告二,广东金融学院实验报告[多媒体2]
  8. 正规网上兼职赚钱日结,来看看小心别被骗!
  9. C语言中perm函数的作用,C语言中有关处理系统时间的知识
  10. 压力情绪管理测试软件,15款超好用的健康类APP测评:减压、调节情绪的好帮手!...