Trie树(字典树&前缀树)

本文介绍了Trie树(前缀树)的基础知识及其两种常见实现方式:二维数组和TrieNode节点。通过比较两者的优缺点,探讨了在空间效率和性能上的权衡,并分析了如何估算二维数组的大小。Trie树常用于快速查询字符串前缀,适用于搜索引擎的词频统计,提供高效的插入、查找和前缀匹配功能。
摘要由CSDN通过智能技术生成

Trie树的基础知识的学习

1. Trie树的基本知识

Trie 树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构。

Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。

Trie树是一种哈希树的变种。

典型应用:用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

优点:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」

它有3个基本性质

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

在这里插入图片描述注:图片来源宫水三叶,链接在后面。

2. Trie树的实现

Trie树有两种实现方法,一种是直接使用二维数组实现,一种是使用TrieNode节点结构实现。

实现的功能包括Trie树的定义,插入,查找,前缀匹配。功能实现来源于leetcode题目:208. 实现 Trie (前缀树)

2.1 二维数组实现

直接使用「二维数组」来实现 Trie 树。

  • 使用二维数组 trie[] 来存储我们所有的单词字符。
  • 使用 index 来自增记录我们到底用了多少个格子(相当于给被用到格子进行编号)。
  • 使用 count[] 数组记录某个格子「被标记为结尾的次数」(当 idx 编号的格子被标记了 n 次,则有 cnt[idx]=n)。

Java代码实现:

class Trie {
    int N = 100009; // 直接设置为十万级
    int[][] trie;
    int[] count;
    int index;

    public Trie() {  // 定义二维数组
        trie = new int[N][26];
        count = new int[N];
        index = 0;
    }
    
    public void insert(String s) {  // 插入操作
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) trie[p][u] = ++index;
            p = trie[p][u];
        }
        count[p]++;
    }
    
    public boolean search(String s) {  // 查找操作
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return count[p] != 0;
    }
    
    public boolean startsWith(String s) { // 前缀匹配
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return true;
    }
}
2.2 TrieNode实现

相比二维数组,更加常规的做法是建立 TrieNode 结构节点。

随着数据的不断插入,根据需要不断创建 TrieNode 节点。

class Trie {
    class TrieNode { // 定义TrieNode节点的
        boolean end;
        TrieNode[] tns = new TrieNode[26];
    }

    TrieNode root;
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {  // 插入操作
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();
            p = p.tns[u]; 
        }
        p.end = true;
    }

    public boolean search(String s) {  // 搜索操作
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return false;
            p = p.tns[u]; 
        }
        return p.end;
    }

    public boolean startsWith(String s) {  // 前缀匹配
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return false;
            p = p.tns[u]; 
        }
        return true;
    }
}
2.3 方法对比

使用「二维数组」的好处是写起来飞快,同时没有频繁 new 对象的开销。但是需要根据数据结构范围估算我们的「二维数组」应该开多少行。

坏处是使用的空间通常是「TrieNode」方式的数倍,而且由于通常对行的估算会很大,导致使用的二维数组开得很大,如果这时候每次创建 Trie 对象时都去创建数组的话,会比较慢,而且当样例多的时候甚至会触发 GC(因为 OJ 每测试一个样例会创建一个 Trie 对象)。

因此还有一个小技巧是将使用到的数组转为静态,然后利用 index 自增的特性在初始化 Trie 时执行清理工作 & 重置逻辑。

这样的做法能够使评测时间降低一半,运气好的话可以得到一个与「TrieNode」方式差不多的时间。

2.4 问题分析

关于「二维数组」是如何工作 & 1e5 大小的估算

  1. 要搞懂为什么行数估算是 1e5,首先要搞清楚「二维数组」是如何工作的

在「二维数组」中,我们是通过 index 自增来控制使用了多少行的。

当我们有一个新的字符需要记录,我们会将 index 自增(代表用到了新的一行),然后将这新行的下标记录到当前某个前缀的格子中。

举个🌰,假设我们先插入字符串 abc 这时候,前面三行会被占掉。

  • 第 0 行 a 所对应的下标有值,值为 1,代表前缀 a 后面接的字符串会被记录在下标为 1 的行内
  • 第 1 行 b 所对应的下标有值,值为 2,代表前缀 ab 后面接的字符串会被记录在下标为 2 的行内
  • 第 2 行 c 所对应的下标有值,值为 3,代表前缀 abc 后面接的字符串会被记录在下标为 3 的行内
  • 当再插入 abcl 的时候,这时候会先定位到 abl 的前缀行(第 3 行),将 l 的下标更新为 4,代表 abcl 被加入前缀树,并且前缀 abcl 接下来会用到第 4 行进行记录。
  • 但当插入 abl 的时候,则会定位到 ab 的前缀行(第 2 行),然后将 l 的下标更新为 5,代表 abl 被加入前缀树,并且前缀abl 接下来会使用第 5 行进行记录。
  1. 当搞清楚了「二维数组」是如何工作之后,我们就能开始估算会用到多少行了。

调用次数为 10^4,传入的字符串长度为 10^3,假设每一次的调用都是 insert,并且每一次调用都会使用到新的 10^3 行。那么我们的行数需要开到 10^7。

但由于我们的字符集大小只有 26,因此不太可能在 10^4 次调用中都用到新的 10^3 行。

而且正常的测试数据应该是 search 和 startsWith 调用次数大于 insert 才有意义的,一个只有 insert 调用的测试数据,任何实现方案都能 AC。

因此我设定了 10^5 为行数估算,当然直接开到 10^6 也没有问题。

参考:

Trie树

【宫水三叶】Trie树一题双解 :「二维数组」&「TrieNode」

海量数据处理之Tire树(字典树)

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值