Trie树

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Trie树

一切知识都从一个小问题开始:

给定一些命令,如set, setbit, setnx, strlen,get, getbit, exists 实现自动补全功能,例如输入se, 提示set, setbit, setnx

此视频为录制的redis客户端命令操作,很显然redis-cli就实现了这样的功能

假设我们运用现有的知识,应当如何解决这个问题呢?

  1. MySQL
    直接使用MySQL的 like 'se%' 命令即可,确实,作为一个每天沉迷业务的程序员很快就能想到这样的方式,但是就这么个小问题,需要使用上MySQL吗,不至于不至于

  2. B+Tree
    我们知道,MySQL的数据结构使用的是B+Tree, (什么?你不知道?现在你知道了吧「滑稽.jpg」),它构建好后就像这样

    查找过程如下:
    se -> 转ASCII码, 与根结点比较,小于setbit的ASCII码,走左边,定位到set,依次从左往右遍历,一一比较,输出所有前缀为se的字符串。
  3. 有序数组
    虽然b+数的查找效率确实很好,但是构建过程却是比较复杂的,有没有一个更加简单的数据结构呢?通过分析b+树的解决方式,我们可以发现,这里用到了两个特性:二分,有序,通过二分的方式快速定位到最小结点,就能有序的从左往右取出值了,考虑到我们的命令不是很多,如果我们只使用有序这个特性会怎样呢?就像这样:
[exists, get, getbit, set, setbit, setnx, strlen]
  1. 查找过程如下:
    输入se,从左往右遍历匹配,找到符合se前缀的值set开始输出,直到遇到不符合的值strlen,停止遍历

这样的方式就非常简单了,但是效率确实不如b+tree

所以到底有没有更加好的方式呢?我们先来总结一下,更加好的方式需要满足的特性:

  • 二分,可以快速定位
  • 有序,顺序的取出符合要求的元素
  • 简单,结构简单

其实,这就是一颗有序的且结构简单的树(最常见的能二分的结构其实就是树了),也就是本次的主角:Trie树

概念

计算机科学中,trie,又称前缀树字典树,是一种有序,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

构建过程

根据维基百科的概念,我们可以总结出以下特性

  1. 根节点对应空字符串。
  2. 一个节点的所有子孙都有相同的前缀。
  3. 不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

所以我们将set, setbit, setnx, strlen, get, getbit, exists构建好后,它的样子如图

可以对比一下这张图和几个特性

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

我给大家介绍一下这颗树

  • 上面的黄色结点表示终点(构建时打上标记),指一个完整的字符串,如第二条链中的get,getbit, 一般情况下除了黄色结点是不会有值的,这里只是为了更好的展现出来。
  • 树的每一层都是按照字母顺序(a-z)从左到右排列

那么,我先现在就尝试查找一个字符串是否存在,比如查找get

  1. g -> 定位到 g字母对应的叉 -getbit
  2. e -> 定位到 -etbit
  3. t -> 判断t点是否是终点 -> 是 -> 存在

时间复杂度为O(n) n为需要查找的字符串长度

为什么是O(n)? get定位了3次,难道每次定位一个字符都是O(1)? 怎么做到的?

查询前缀同理

所以我们想要做一个自动补全功能时,比如输入se,流程如下:

  1. s -> 找到s对应的叉
  2. e -> 找到s链的e结点对应的叉
  3. 遍历e结点下的整颗树
  4. 当遇见黄点时输出该值

到这里,原理相信大家都已经明白了,那么代码应当如何实现呢?

代码

树的实现无非两种,数组 或者 链表,通过观察上面的树,可以发现,每一层的结点最多有26个(a-z),我们可以直接开辟26个长度的数组存储这些结点,又因为每一层的字符是有序的,于是定位一个字符c的时候只需要c-'a'就能得出下标,如定位g

'g' - 'a' = 6

数据结构如下:

public class TrieNode {
  // 是否是终点
  boolean isEnd;
  // 字符串的值,isEnd = true 时才有
  String value;
  // 子结点
  TrieNode[] childrenNodes = new TrieNode[26];
}

完整代码:

public class TrieTree {
    TrieNode trieNode;
    public TrieTree() {
        this.trieNode = new TrieNode();
    }
    /**
     * 增加一个结点
     * 
     * @param word 单词
     */
    public void add(String word) {
        char[] chars = word.toLowerCase().toCharArray();
        TrieNode next = trieNode;
        for (char c : chars) {
            TrieNode trieNode = next.childrenNodes[c - 'a'];
            if (trieNode == null) {
                trieNode = new TrieNode();
                next.childrenNodes[c - 'a'] = trieNode;
            }
            next = trieNode;
        }
        next.isEnd = true;
        next.value = word;
    }
    /**
     * 查询单词是否存在
     * 
     * @param word 单词
     * @return 是否存在
     */
    public boolean search(String word){
        TrieNode trieNode = searchNode(word);
        return trieNode != null && trieNode.isEnd;
    }
    /**
     * 判断前缀是否存在
     * 
     * @param prefix 前缀
     * @return 是否存在
     */
    public boolean starsWith(String prefix){
        return searchNode(prefix) != null;
    }
    /**
     * 查询结点
     * 
     * @param word 单词
     * @return 结点
     */
    private TrieNode searchNode(String word){
        char[] chars = word.toLowerCase().toCharArray();
        TrieNode next = trieNode;
        for (char c : chars) {
            TrieNode trieNode = next.childrenNodes[c - 'a'];
            if (trieNode == null) {
                return null;
            }
            next = trieNode;
        }
        return next;
    }
    /**
     * 自动补全
     * @param prefix 前缀
     * @param list 结果集
     */
    public void autocomplete(String prefix, List<String> list){
        TrieNode trieNode = searchNode(prefix);
        if (trieNode != null) {
            autocomplete(trieNode, list);
        }
    }
    private void autocomplete(TrieNode trieNode, List<String> list){
        TrieNode[] trieNodes = trieNode.childrenNodes;
        for (TrieNode node : trieNodes) {
            if (node != null) {
                if (node.isEnd) {
                    list.add(node.value);
                }
                autocomplete(node, list);
            }
        }
    }
    public class TrieNode {
        // 是否是终点
        boolean isEnd;
        // 字符串的值,isEnd = true 时才有
        String value;
        // 子结点
        TrieNode[] childrenNodes = new TrieNode[26];
    }
    public static void main(String[] args) {
        TrieTree trieTree = new TrieTree();
        trieTree.add("apple");
        trieTree.add("apply");
        trieTree.add("application");
        System.out.println(trieTree.search("apple"));
        System.out.println(trieTree.search("app"));
        System.out.println(trieTree.starsWith("app"));
        List<String> list = new ArrayList<>();
        trieTree.autocomplete("app", list);
        System.out.println(list);
    }
}

测试结果

true
false
true
[apple, application, apply]

[apple, application, apply]

小结

本篇介绍了什么是Trie树,以及通过Trie树如何实现一个自动补全功能,下面我们计算一下它的时间复杂度和空间复杂度

时间复杂度:O(n) n为需要查找的字符串长度

空间复杂度:26^n

26^n这个字眼着实令人咂舌,这也是大家常说trie树是一颗空间换时间的树,而且这还只是只有英文字符的情况,倘若加上数字呢,中文呢?

那么有没有什么优化的办法呢?

有的,比如查看上图就能发现,虽然把所有前缀相同的单词合并到了一起,但是还是有很多重复的,比如 setbit, getbit, 这两个单词出入开头的s和g不同,其他的都是相同的,如果我们能把他们共用,岂不也能大大缩小空间,如何实现,下次再说!

Trie树的应用场景

  1. 词语查找,比如在中文分词器中,就可以使用用户字典构建一颗Trie树,使用时输入句子进行匹配分词
  2. 自动补全
  3. 拼写检查

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
5月前
|
存储 算法
Trie字典树
Trie字典树
49 1
|
8月前
|
搜索推荐
前缀树Trie
前缀树Trie
|
8月前
|
存储 C++
leetcode-208:实现 Trie (前缀树/字典树)
leetcode-208:实现 Trie (前缀树/字典树)
60 0
|
存储 Python
字典树(Trie,
字典树(Trie,也称为前缀树或单词查找树)是一种用于存储字符串的树形数据结构。它是一种特殊的多叉树,其中每个节点都包含一个字符和一个指向其子节点的指针数组。字典树的主要作用是用于快速查找字符串和处理字符串的前缀。
70 6
|
存储 自然语言处理 算法
一种好用的树结构:Trie树
一种好用的树结构:Trie树
185 0
一种好用的树结构:Trie树
|
搜索推荐
字典树 trie
字典树 trie
69 0
理解前缀树
理解前缀树
75 0
|
存储 机器学习/深度学习 算法
|
存储
前缀树
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
86 0
前缀树
|
存储 自然语言处理 算法
字典树详解
字典树,顾名思义,是关于“字典”的一棵树。即:它是对于字典的一种存储方式(所以是一种数据结构而不是算法)。
182 0
字典树详解