【数据结构】初入数据结构的字典树 ( Trie Tree ) 及实现

本文详细介绍了前缀树(TrieTree)的概念、优点、应用场景及其实现。通过实例展示了如何用Java实现字典树,包括插入单词、查找、词频统计和联想词等功能。字典树是一种高效的数据结构,适用于字符串的快速查找和联想,尤其在搜索引擎和敏感词过滤等方面有广泛应用。

初入数据结构的前缀/字典树 ( Trie Tree ) 及实现

如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里

  • 前提概念
    • 什么是字典树?
    • 字典树的优缺点?
  • 实现
    • 约束和需求
    • 代码实现

前提概念


什么是字典树?

字典树 (Trie Tree),又称前缀树,单词查找树。典型应用是用于统计,排序和保持大量字符串,所以经常被搜索引擎系统用于文本词频统计。它的核心优势就在于利用字符串的公共前缀来减少查询时间,最大限度的减少无谓的的字符串比较
字典树 - @百度百科

特征

  • 字典树是一颗多路树
  • 根结点不包含字符,除根结点外的所有结点都包含一个字符

应用场景

  • K/V 存储
  • 敏感词过滤
    • 采用字典树进行敏感词过滤,相比哈希表可以节省空间
  • 词频统计
    • 同样相比哈希表,更加的节省空间
  • 字典序排序
    • 字典树前序遍历
  • 前缀匹配
    • 搜索引擎,搜索提示

字典树的优缺点

从字典树的介绍,我们知道了什么是字典树。那么字典树相比其他数据结构,有什么好处和作用?

字典树说白了就是一种以空间换时间的特定场合数据结构,在特定的场合下,可以有比较良好的查询优势

敏感词过滤 (n 个敏感词)

  • 线性表查找时间复杂度 O(n)
  • 二叉树查找时间 O(logn)
  • 哈希表查找时间 O(1)
  • 字典树查找时间复杂度 O(h), h 为单词长度
    通常情况下 h < n , 所以字典树的查询效率肯定是远与线性表和二叉树的。 但略低于哈希表。当然也不一定,因为哈希表存在哈希冲突,在哈希冲突大的情况下,哈希表的查询效率不一定比字典树要高 (当冲突时的链表长度或树高度大于单词长度)

联想词

  • 线性表需要扫全表 O(n)
  • 二叉树需要扫全树 O(logn)
  • 哈希表不太好实现
  • 字典树查找时间最坏情况 O((h + x * y) , h 是单词长度, x 是关联词延伸多少个字符, y 是有多少种字符
    假设100w 的单词数据,单词平均长度 h = 10, 单词只有 26 个字符,只联系延伸 3 个字符, 字典树只需要最坏查询 88 次即可,相比线性表的 10w 次和 log10w 次,简直是性能大大提升,所以联想词其实属于字典树的优势场景

不适合的场景

  • 如果输入的数据,长度比较长,比如有千个,万个单位长度,其实就不适用使用字典树去存储,这会导致树深过大

实现


约束与需求

约束
简单点实现一个字典树,所以先放上约束

  • 该字典树仅存储 [a-z] 26 个字符的数据
  • 单个单词长度 1 < len <= 100

需求

  • 词频统计 count()
  • 是否存在 contains()
  • 是否存在以 xxx 开头的词 startWith()
  • 提供相关词 search()

代码实现

字典树结点定义

    public static class TrieNode {
   
   
    
        private boolean end;
        private char data;
        private int count;
        private TrieNode[] next = new TrieNode[26];

        public TrieNode(char data) {
   
   
            this.data = data;
        }
    }
  • end 代表根结点到当前结点,是否构成一个单词
  • data 代表该结点存储的字符数据 [a-z]
  • count 如果该结点 end = true, 那么 count 就有意义了,代表该单词在该字典树出现的次数
  • next 代表 26 个子结点的指针
    • 为什么是 26 个子结点,因为一个结点的后继字符,有 26 种可能;又因为我们基于数组 (静态定长), 所以只能一开始就分配 26 长度子结点指针数组
    • 如果不基于数组实现,其实也可以引入动态数组,List,甚至 HashMap 或是 TreeMap

字典树定义


public class TrieTree {
   
   

    /**
     * root node,meaningless dummy node
     */
    private TrieNode root;

    public TrieTree() {
   
   
        root = new TrieNode('*');
    }
    
}

字典树的定义非常简单

  • 一个的根结点,不存储任何的数据
    • 可以理解为一个没有数据的哑结点
    • 作用就仅仅是让我们找到这颗树的头部
  • 一个构造函数,用于创建一棵空的字典树,然后就可以为所欲为了

插入单词

    public void insert(String word) {
   
   
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
   
   
            int index = word.charAt(i) - 'a';
            if (node.next[index] == null) {
   
   
                node.next[index] = new TrieNode(word.charAt(i));
            }
            node = node.next[index];
        }
        node.end = true;
        ++node.count;
    }

插入单词的逻辑也很简单,基本思路就是逐个字符寻址字典树的结点路径

  • 循环 word 的字符长度次数,因为如果字典树存在该单词,那么该单词至少有 word.length() + 1 个结点构成 (1 是根结点)
  • 然后通过 word.charAt(i) - 'a' 得到该 word.chatAt(i) 字符在当前结点的 26 个子结点的数组索引
    • 因为 [a-z] 26 个字符中,是有字典序的,a - a = 0, b - a = 1, 那么 a 字符就是 next[0] 结点,b 字符就是 next[1] 结点
    • 如果我们使用动态数组,就不需要通过这样的方式来获取索引
  • 在迭代的过程中,判断是否有 node.next[index] == null
    • 如果为真,则代表字典树中,存在 word 的部分字符,但不存在该单词,则需要继续构造剩余字符的结点
    • 如果不为真,则代表字典树中,存在 word 的当前字符,需要继续迭代判断
  • 循环结束后,则代表 word 本身就存在字典中,或是本来不存在,但已经构造完成。则 end = true, count++
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值