Trie字典树详解
创始人
2024-02-08 05:36:27
0

字典树

    • 📖1. 什么是Trie树
    • 📖2. Trie树的一些应用场景
    • 📖3. Trie树的优缺点
    • 📖4. Trie树的节点怎样定义
    • 📖5. 代码实现
    • 📖6. 字典树的优化

📖1. 什么是Trie树

Trie树,又叫字典树,前缀树(Prefix Tree),单词查找树,是一种多叉树的结构.

image-20221122001519100

上图就是一颗Trie树,表示了关键字集合{"pool", "prize", "prepare", "preview", "produce", "progress"}.

字典树的基本性质如下:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同.

算法核心:利用字符串的公共前缀来减少查询时间,最大限度的减少无畏字符串的比较.

使用范围:

  1. 单词检索
  2. 统计和排序字符串
  3. 字符串前缀搜索

我们来看一个场景:

image-20221122002220430

当我们在浏览器的搜索框中打出一个字符串的前缀时,它便实时的显示出了以这个输入为前缀的一些字符串,也就是说,它帮我们搜索到了以这个输入为前缀的所有字符串,并且显示出了搜索频率较高的一些,这就是字典树的一个应用场景:单词自动补齐.

📖2. Trie树的一些应用场景

除了自动补齐的例子外,Trie树还有一些其他的应用场景:

  1. 串的快速检索

    给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词.

  2. 单词自动完成

    编辑代码时,输入字符,自动提示可能的关键字,变量或函数等信息.

  3. 最长公共前缀

    对所有串建立字典树,对于两个串的最长公共前缀的长度即它们所在的节点的公共祖先个数,问题就转化为最近公共祖先问题

  4. 串排序方面的应用

    给定N个互不相同的仅由一个单词构成的英文名,将他们按字典序从小到大输出. 用字典树进行排序,这棵树的每个节点的所有子节点很明显的按照其字母大小排序,对这棵树进行前序遍历即可.

📖3. Trie树的优缺点

Trie树的核心思想是空间换时间,利用字符串的公共前缀来减少无畏字符串的比较以达到提高查询效率的目的.

优点:插入和查询的效率高,都为O(m),其中m是待插入/查询的字符串的长度.

关于查询,可能有人会说,哈希表的时间复杂度为O(1),岂不是更快?

  1. 哈希表的效率通常取决于哈希函数的好坏,若一个坏的哈希函数可能会导致发生许多冲突,将具有相似前缀的字符串映射到同一个哈希桶中,在进行这个哈希桶中进行查找时,就会导致这些相同前缀的重复比较,效率并不一定比Trie树高.
  2. Trie树中的不同关键字不会产生哈希冲突
  3. Trie树可以对关键字按字典序排序

缺点当所有关键字都不具有相同或类似的前缀,空间消耗过大.

📖4. Trie树的节点怎样定义

对于Trie树,它是一个多叉树的结构,所有我们可以在节点中定义map来存储孩子节点字符数据和节点指针的对应关系.

在查找的过程中,我们怎样标定字符串的结尾呢?

image-20221122005701772

所以节点的定义:

struct TrieNode
{//节点中存储的字符数据char ch_;//单词的末尾字符存储单词的数量int freqs_;//存储孩子节点字符数据和节点指针的对应关系std::map nodeMap_;
};

📖5. 代码实现

#include
#include
#include
#include
#includeusing namespace std;class TrieTree
{
public:TrieTree(){root_ = new TrieNode('\0', 0);}
public://添加单词void add(const string& word){TrieNode* cur = root_;for (int i = 0; i < word.size(); ++i){auto childIt = cur->nodeMap_.find(word[i]);if (childIt == cur->nodeMap_.end()){//相应的字符的节点没有,创建它TrieNode* child = new TrieNode(word[i], 0);cur->nodeMap_.emplace(word[i], child);cur = child;}else{//相应的字符节点已经存在,移动cur指向对应的节点cur = childIt->second;}}//cur指向了word单词的最后一个节点cur->freqs_++;}//查询单词int query(const string& word){TrieNode* cur = root_;for (int i = 0; i < word.size(); ++i){auto childIt = cur->nodeMap_.find(word[i]);if (childIt == cur->nodeMap_.end()){return 0;}//移动cur指向下一个单词的字符节点上cur = childIt->second;}//最终指向了这个单词的最后一个字符return cur->freqs_;}//删除单词void remove(const string& word){TrieNode* cur = root_;TrieNode* del = root_;  //从哪个节点开始删除char delch = word[0];for (int i = 0; i < word.size(); ++i){auto childIt = cur->nodeMap_.find(word[i]);if (childIt == cur->nodeMap_.end())return;//pool po情况二和情况三if (cur->freqs_ > 0 || cur->nodeMap_.size() > 1){del = cur;delch = word[i];}//cur移动到子节点cur = childIt->second;}//cur指向了末尾节点if (cur->nodeMap_.empty()){//开始删除TrieNode* child = del->nodeMap_[delch];del->nodeMap_.erase(delch);//释放相应的节点内存queue que;que.push(child);while (!que.empty()){TrieNode* front = que.front();que.pop();//把当前节点的孩子全部入队列for (auto& pair : front->nodeMap_){que.push(pair.second);}//释放当前节点资源delete front;}}else{//情况1//当前单词末尾字符后面还有字符节点,不做任何节点删除操作cur->freqs_ = 0;}}//前序遍历字典树void preOrder(){string word;vector wordList;preOrder(root_, word, wordList);for (auto word : wordList){cout << word << endl;}cout << endl;}//串的前缀搜索vector queryPrefix(const string& prefix){TrieNode* cur = root_;for (int i = 0; i < prefix.size(); ++i){auto childIt = cur->nodeMap_.find(prefix[i]);if (childIt == cur->nodeMap_.end()){return {};}cur = childIt->second;}//cur指向了前缀的最后一个字符节点了vector wordList;preOrder(cur, prefix.substr(0, prefix.size() - 1), wordList);return wordList;}private:struct TrieNode{TrieNode(char ch, int freqs): ch_(ch), freqs_(freqs){}//节点中存储的字符数据char ch_;//单词的末尾字符存储单词的数量int freqs_;//存储孩子节点字符数据和节点指针的对应关系std::map nodeMap_;};private:void preOrder(TrieNode* cur, string word, vector& wordList){//前序遍历 根 左 右if (cur != root_){word.push_back(cur->ch_);if (cur->freqs_ > 0){wordList.push_back(word);}}//递归处理子节点for (auto pair : cur->nodeMap_){preOrder(pair.second, word, wordList);}}private:TrieNode* root_; //指向树的根结点
};
//功能测试:
#include "TrieTree.h"int main()
{TrieTree trie;trie.add("hello");trie.add("hello");trie.add("helloo");trie.add("hel");trie.add("hel");trie.add("hel");trie.add("china");trie.add("ch");trie.add("ch");trie.add("heword");trie.add("hellw");cout << "数量统计: " << endl;cout << trie.query("hello") << endl;cout << trie.query("helloo") << endl;cout << trie.query("hel") << endl;cout << trie.query("china") << endl;cout << trie.query("ch") << endl;cout << "=====================" << endl;cout << "前序遍历: " << endl;trie.preOrder();cout << "=====================" << endl;vector words = trie.queryPrefix("he");cout << "前缀串搜索: " << endl;for (auto word : words){cout << word << endl;}cout << endl;trie.remove("hellw");cout << "=====================" << endl;cout << "删除测试: " << endl;trie.preOrder();return 0;
}

📖6. 字典树的优化

上面我们提到过,字典树有一个缺点:占用内存空间过大.

所以,我们可以将字典树进行优化

字符节点后面没有其他单词,可以把字符节点压缩到一个节点中进行存储.

image-20221122160851946

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...