数据结构与算法:Trie字典树


Trie

什么是Trie

字典树:又称单词查找树/前缀树/Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

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

Trie树的3个基本性质

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

在这里插入图片描述

Trie的应用

  1. 自动补全
    在这里插入图片描述

  2. 拼写检查
    在这里插入图片描述

文字处理软件中的拼写检查
  1. IP路由(最长前缀匹配)
    使用Trie树的最长前缀匹配算法,Internet 协议(IP)路由中利用转发表选择路径
使用Trie树的最长前缀匹配算法,Internet 协议(IP)路由中利用转发表选择路径
  1. T9(九宫格)打字预测
    在这里插入图片描述
九宫格输入
  1. 单词游戏
    在这里插入图片描述
Trie 树可通过剪枝搜索空间来高效解决 Boggle 单词游戏

Trie与其他相关数据结构的对比

还有其他的数据结构,如平衡树和哈希表,使我们能够在字符串数据集中搜索单词。为什么我们还需要 Trie 树呢?尽管哈希表可以在 O ( 1 ) O(1) O(1)时间内寻找键值,却无法高效的完成以下操作:

  • 找到具有同一前缀的全部键值。
  • 按词典序枚举字符串的数据集。

Trie 树优于哈希表的另一个理由是,随着哈希表大小增加,会出现大量的冲突,时间复杂度可能增加到 O ( n ) O(n) O(n),其中 n n n是插入的键的数量。与哈希表相比,Trie 树在存储多个具有相同前缀的键时可以使用较少的空间。此时 Trie 树只需要 O ( m ) O(m) O(m) 的时间复杂度,其中 m m m为键长。而在平衡树中查找键值需要 O ( m l o g n ) O(mlogn) O(mlogn) 时间复杂度。

Trie的局限性

最大的问题:空间!

更多字符串问题

  • 子串查询-----KMP、Boyer-Moore、Rabin-Karp
  • 文件压缩-----哈夫曼算法
  • 模式匹配----正则表达式
  • 编译原理
  • DNA

实现Trie

Trie的节点结构

Trie 树是一个有根的树,其结点具有以下字段:

  • 指向子结点的链接next,其中每个链接对应字母表数据集中的一个字母,本文通过Map来存储,key是当前节点对应的字符,value是子节点
  • 布尔字段isWord,以指定节点是对应键的结尾还是只是键前缀
    private class Node {
        public boolean isWord;

        public TreeMap<Character, Node> next;

        public Node(boolean isWord) {
            this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node() {
            this(false);
        }
    }

向Trie中添加一个新的单词word(不会重复)

通过搜索 Trie 树来插入一个单词。从根节点开始搜索它对应于第一个字符的链接。有两种情况:

  • 链接存在。沿着链接移动到树的下一个子层。算法继续搜索下一个字符。
  • 链接不存在。创建一个新的节点,并将它与父节点的链接相连,该链接与当前单词的字符相匹配。

重复以上步骤,直到到达单词的最后一个字符,然后将当前节点标记为结束节点,插入完成

     public void add(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.next.get(c) == null) {
                cur.next.put(c, new Node());
            }
            cur = cur.next.get(c);
        }

        // 判断是否是新单词
        if ( !cur.isWord) {
            cur.isWord = true;
            size++;
        }
     }

复杂度分析

  • 时间复杂度: O ( m ) O(m) O(m),其中 m m m 为单词长度。在每次迭代中,要么检查要么创建一个节点,直到到达词尾。只需要 m m m 次操作。

  • 空间复杂度: O ( m ) O(m) O(m)。最坏的情况下,新插入的单词和 Trie 树中已有的单词没有公共前缀。此时需要添加 m m m 个结点,使用 O ( m ) O(m) O(m) 空间

查询单词word是否在Trie中

每个单词在 Trie 中表示为从根节点到内部节点或叶的路径。我们从根节点开始,检查当前节点中与该单词字符对应的链接。有两种情况:

  • 存在链接。移动到该链接后面路径中的下一个节点,并继续搜索下一个键字符。
  • 不存在链接。若已遍历完单词,且当前结点标记为 isWord,则返回 true。否则有两种情况,均返回 false :
    • 单词未遍历结束, 但无法跟随Trie树的字符路径,即Trie中不包含此单词
    • 单词已遍历完毕,但是该单词只是Trie中另一个单词的前缀
     public boolean contains(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.next.get(c) == null) {
                return false;
            }
            cur = cur.next.get(c);
        }
        return cur.isWord;
     }

复杂度分析

  • 时间复杂度 : O ( m ) O(m) O(m)。算法的每一步均搜索下一个字符。最坏的情况下需要 m m m 次操作。
  • 空间复杂度 : O ( 1 ) O(1) O(1)

查询是否在Trie中有单词以prefix为前缀

与上面提到的“查询单词”算法唯一的区别是,到达待查找前缀的末尾时,总是返回 true。不需要考虑当前 Trie 节点是否用 “isWord” 标记,因为我们搜索的是前缀,而不是整个单词。

     public boolean isPrefix(String prefix) {
         Node cur = root;
         for (int i = 0; i < prefix.length(); i++) {
             char c = prefix.charAt(i);
             if (cur.next.get(c) == null) {
                 return false;
             }
             cur = cur.next.get(c);
         }
         return true;
     }

复杂度分析

  • 时间复杂度 : O ( m ) O(m) O(m)
  • 空间复杂度 : O ( 1 ) O(1) O(1)

完整代码

Github仓库

练习题目

LeetCode 208. 实现Trie(前缀树)

LeetCode 211. 添加与搜索单词 - 数据结构设计

Reference

  1. LeetCode
  2. 慕课网《玩转数据结构》
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xylitolz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值