简记字典树Trie

《简记字典树Trie》

  Trie也称字典树、单词查找树或键树,是一种树形结构。典型的应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。其优点是最大限度的减少无谓的字符串比较,查询效率比哈希表高。

Key Words:字典树、构建字典树、字典树的应用


Beijing, 2020

作者:RaySue

Agile Pioneer  


树的回顾

  • 结点:树中的数据元素
  • 结点的度(degree):结点拥有的子树的数目称为度
  • 叶子节点:节点的度为0,称为叶子节点leaf、终端节点、末端结点
  • 分支结点:结点度不为0,称为非终端节点或分支结点
  • 双亲结点(父结点):一个结点是它各子树的根结点的双亲
  • 兄弟结点:具有相同双亲结点的结点
  • 结点的层次(Level):根节点为第一层,根的孩子为第二层,以此内推

字典树(Trie)

   下图给出了字典树的具体实现,字典树的每个结点都是一个数组或者是其他存储结构,每个节点只表示一个字符,然后其子节点也是Trie树,和树的递归结构是一样的。

   但是为了清晰的展示字典树,我们通常省略掉其存储的细节,如下图:

Trie的基本性质

  1. 结点本身不存在完整单词

  2. 从根结点到某一结点,路径上经过的字符串连接起来,为该节点对应的字符串;

  3. 每个结点的所有子节点路径代表的字符串都不相同;

Trie的核心思想

  以空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

Trie基本的功能

插入字符串 (insert)
void insert(string word)
{
    Trie *node = this;  // 这里是类中的方法,this相当于链表的head
    for(char c:word)
    {
        int idx = static_cast<int>(c - 'a');  // 假设只有26个字母的情况
        if (node->next[idx] == nullptr)
        {
            node->next[idx] = new Trie();
        }
        node = node->next[idx];
    }
    node->isEnd = true;  // 结束字符
}
查找字符串 (search)
bool search(string word)
{
    Trie *node = this;
    for(char c:word)
    {
        int idx = static_cast<int>(c - 'a');
        if (node->next[idx] == nullptr)
        {
            return false;
        }
        node = node->next[idx];
    }
    return node->isEnd;
}
前缀查询 (startsWith)
bool startsWith(string prefix)
{
    Trie *node = this;
    for (char c:prefix)
    {
        int idx = static_cast<int>(c - 'a');
        if (node->next[idx] == nullptr)
        {
            return false;
        }
        node = node->next[idx];
    }
    return True;
}

*删除字符串 (delete)
  • 删除操作较为复杂,有三种情况:
  1. 单词是另一个单词的前缀,那么就把待删除单词的isEnd改为false即可,表示这里不再是单词的结束
  1. 如果单词的所有字母的都没有多个分支,直接删除整个单词
  1. 如果单词除了后面几个字母,其余的都有分支,那么就删除后面这几个字母
// 待续



Trie的应用场景

词频统计

Trie树常被搜索引擎系统用于文本词频统计。
思路:为了实现词频统计,我们可以修改节点结构,将isKey用一个整型变量count来表示该节点为结尾的关键字的词频。对每一个关键字执行插入操作,若已存在,计数加1,若不存在,插入后count置1。

struct TrieNode
{
    int count; // 记录该节点代表的单词的个数
    TrieNode *children[26]; // 各个子节点
};
最长公共前缀

前缀匹配,查找N个单词的最长公共前缀

去除重复单词

建立字典树的过程就是给字符串去重的过程,search方法就和unordered_set的count方法一致。

字符串排序

Trie树可以对大量字符串按字典序进行排序,思路也很简单:遍历一次所有关键字,将它们全部插入trie树,树的每个结点的所有儿子很显然地按照字母表排序,然后先序遍历输出Trie树中所有关键字即可。

词频感应

  通常我们在搜索引擎中输入前缀后,会出现很多提示,这就是基于字典树的词频感应。字典树根据你给出的 prefix 进行搜索得到出现频率靠前的结果展示出来,这是一个高效的搜索方式,其速度比哈希表还快。


Trie的复杂度:

Trie具有 线性复杂度:

  • 时间复杂度:

    • 假设所有字符串长度之和为n,构建字典树的时间复杂度为O(n)
    • 假设要查找的字符串长度为k,查找的时间复杂度为O(k)
  • 空间复杂度:

    • 字典树每个节点都需要用一个数组来存储子节点的指针,即便实际只有两三个子节点,但依然需要一个完整大小的数组。所以,字典树比较耗内存,空间复杂度较高。

字典树的简单实现

Python实现
class Trie:

    def __init__(self):
        self.root = {}
        self.end_of_word = '#'

    def insert(self, word):
        node = self.root
        for w in word:
            node.setdefault(w, {})
            node = node[w]
        node[self.end_of_word] = self.end_of_word

    def search(self, word):
        node = self.root
        for char in word:
            if char not in node:
                return False
            node = node[char]
        return self.end_of_word in node

    def startWith(self, prefix):
        node = self.root
        for char in prefix:
            if char not in node:
                return False
            node = node[char]
        return True
Cpp实现
  • 基于 unordered_map 的实现
class Trie
    {
    private:
        int count; // 带词频统计
        unordered_map<char, Trie *> root;

    public:
        /** Initialize your data structure here. */
        Trie()
        {
            count = 0;
        }
        /** Inserts a word into the trie. */
        void insert(string word)
        {
            Trie *node = this;  // 类似链表中的 ListNode *ptr = head;
            for (char c:word)
            {
                if (!node->root.count(c))
                {
                    node->root[c] = new Trie();
                }
                node = node->root[c];
            }
            node->count++;
        }

        /** Returns if the word is in the trie. */
        bool search(string word)
        {
            Trie *node = this;
            for (char c:word)
            {
                if (!node->root.count(c)) return false;
                node = node->root[c];
            }
//            cout << word << " " << node->count << endl;
            return node->count > 0;
        }

        /** Returns if there is any word in the trie that starts with the given prefix. */
        bool startsWith(string prefix)
        {
            Trie *node = this;
            for (char c:prefix)
            {
                if (!node->root.count(c)) return false;
                node = node->root[c];
            }
            return true;
        }
    };

参考

https://blog.csdn.net/yuzhiqiang666/article/details/80711441

https://blog.csdn.net/lucky_greenegg/article/details/9141759

https://blog.csdn.net/sunny_ss12/article/details/47683715


Q&A

Q1: Trie树的优缺点:

A1:

  • 优点:
  1. 查询快。对于长度为m的键值,最坏情况下只需花费O(m)的时间;而BST需要O(m log n)的时间。 虽然hash 表时间复杂度是O(1),但是,哈希搜索的效率通常取决于 hash 函数的好坏,若一个坏的 hash 函数导致很多的冲突,效率并不一定比Trie树高。

  2. 当存储大量字符串时,Trie耗费的空间较少。因为键值并非显式存储的,而是与其他键值共享子串。

  • 缺点:
  1. 当查询的范围较大时,字典树比较耗内存,空间复杂度较高。而且实现高质量,快且小的 Trie 非常困难。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值