【数据结构】字典树介绍 + 手写简单的TrieTree

字典树

什么是字典树

Trie树(前缀树),即字典树,又称单词查找树或键树,是一种树形结构。

用于高效地存储和查找字符串集合。字典树的每个节点表示一个字符串的前缀,从根节点到叶子节点的路径表示一个完整的字符串,因此字典树可以用于前缀匹配和字符串查找等方面。

每一层的字母是被共享的,这样每一层相同的字符就只会占用一份空间,像上图所示: bat, cat, dad共用第二层共用a, bat和cat共用同一个t。

字典树的特点

  1. 字符串查询时间复杂度O(L), L为字符串长度
  2. 空间复杂度较高,需要较多空间复杂维护结构
  3. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  4. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  5. 每个节点的所有子节点包含的字符都不相同。

适用环境

  1. 前缀关键字查询。 用于存储和查询关键词,如搜索引擎、拼音输入法等。
  2. 自动补全。 从已有数据中自动补全字符串, 如浏览器地址栏自动补全。
  3. 字符串筛选。 快速判断一个字符串是否在某集合中。

基本操作

  1. 插入(insert)
  • 将一个新的字符串插入到前缀树中
  • 过程:根据字符串的每个字符,沿对应路径向下插入节点
  • 若路径不存在,则创建新的节点

时间复杂度:O(L),L 表示字符串长度

  1. 查找(Search)
  • 查找一个字符串是否存在于前缀树中
  • 过程:根据字符串的每个字符,判断对应的路径是否存在
  • 只要有一个字符在路径中不存在,则该字符串不在前缀树中

时间复杂度: O(L), L 表示字符串长度

  1. 前缀查找(Prefix Search)
  • 查找前缀树中所有以某个前缀开始的字符串
  • 过程:根据前缀的每个字符,进一步向下搜索对应的路径
  • 时间复杂度和普通查找相同,O(P), P 表示前缀长度

手写字典树

完成后的结果如下图所示:

从上到下依次是:

  • 根节点
  • 静态节点类
  • 构造函数
  • 插入的方法
  • 通过前缀搜索字符串
  • 拼接字符串的方法

静态节点类

ini 复制代码
static class Node{
    private char aChar;
    boolean isEnd;
    Node[] next = new Node[26];
    Node(char aChar){
        this.aChar = aChar;
    }
}

该类是每个节点的具体内容,里面维护:

  • char字符aChar
  • 标识符isEnd表示是否有单词在此完结
  • 维护一个数组,里面只保存26个节点,因为我们存的字符是'a'-'z'
  • 构造函数,传一个字符值

字典树构造函数

ini 复制代码
TrieTree(){
    root = new Node(' ');
}

初始化根节点就行了

插入方法

ini 复制代码
public void insert(String s){
    Node temp = root;
    if(s.length()==0){
        return;
    }
    char[] c=s.toCharArray();
    for(int i=0;i<s.length();i++){
        //如果该分支不存在,创建一个新节点
        if(temp.next[c[i]-'a']==null){
            temp.next[c[i]-'a']=new Node(c[i]);
        }
        temp=temp.next[c[i]-'a'];
    }
    temp.isEnd = true;
}
  1. 遍历传入的字符串,一个一个字符进行读取。
  2. 进行判断,判断当前节点的next[c[i] - 'a'] ( 该索引不会超过25)是否存在。
  3. 如果不存在,就创建新节点。
  4. 将当前节点指向子节点。
  5. 循环完毕,这时指针指向传入字符串的最后一个字符,将该字符标记为true,表示有以该字符结尾的单词。

根据前缀获得字符串

ini 复制代码
public List<String> searchPrefix(String prefix) {
    Node temp = root;
    char[] chars = prefix.toCharArray();
    for (char c : chars) {
        int idx = c - 'a';
        // 匹配为空
        if (idx > temp.next.length || idx < 0 || temp.next[idx] == null) {
            return Collections.emptyList();
        }
        temp = temp.next[idx];
    }
    // 上面循环结束,指向最后一个前缀字符
    ArrayList<String> res = new ArrayList<>();
    // 模糊匹配:根据前缀的最后一个字符,递归遍历所有的单词
    collect(temp, prefix, res, 15);
    return res;
}

protected void collect(Node trieNode, String pre, List<String> queue, int resultLimit) {
    // 找到单词
    if (trieNode.isEnd) {
        trieNode.word = pre;
        // 保存检索到的单词到 queue
        queue.add(pre);
        if (queue.size() >= resultLimit) {
            return;
        }
    }
    // 递归调用,查找单词
    for (int i = 0; i < 26; i++) {
        char c = (char) ('a' + i);
        if (trieNode.next[i] != null) {
            collect(trieNode.next[i], pre + c, queue, resultLimit);
        }
    }
}
  1. 首先肯定是要对前缀进行遍历,寻找是否存在以该前缀开头的单词,没有就返回空串。
  2. 根据前缀开头的每一个字符值来判断当前节点是否存在,不存在就返回空串。
  3. 循环完毕后,表明存在该前缀开头的单词, 可以进行下一步操作。
  4. 然后进入拼接函collect函数
  5. 下面是具体的拼接函数:
    • 参数: 当前正在被拼接的节点,当前字符前面的字符(包括自己),最后要返回的列表,返回列表的限定长度。
    • 然后判断isEnd,如果为true,则将当前字符串加入到返回列表中
    • 然后进行当前节点的子节点遍历,如果不为空,则拼接字符串,此时传入的pre参数为现在它前面的字符串加上自己的字符(pre + c),因为传入的字符串要包括自己。这样进行递归调用,直到所有的子节点都为空。

测试代码

typescript 复制代码
public static void main(String[] args) {
    TrieTree trieTree = new TrieTree();
    trieTree.insert("wdnmd");
    trieTree.insert("wdhhh");
    trieTree.insert("hhh");
    List<String> list = trieTree.searchPrefix("wd");
    for (String s : list) {
        System.out.println(s);
    }
}

先插入几个字符串,然后去根据前缀查询打印结果。

输出及分析

wdhhh

wdnmd

可以看到我们成功完成了对前缀"wd"的查询并输出所有字符串。

总结

  1. 字典树又叫前缀树,它是一种树形的结构
  2. 它的查询和插入都比较快,时间复杂度为O(L), L为字符串长度.(这里的查询指查找一个确定的)
  3. 它的内存可能会稍微有点大,因为它需要为可能出现的子节点预留空间,当然你可以使用动态扩容数组ArrayList,我这里使用的是固定长数组,开一个26单位的,但是只能存'a'-'z'。
  4. 如果要查询以某个前缀开头的所有字符,需要广度遍历,每一层都要找到所有的子节点;时间复杂度为 O(26^L),每一个子节点遍26次,最深为L层,我觉得还是挺慢的。(主要是我使用的是深度优先,可能空间复杂度比较高?问题是我这里只会使用DFS,因为我需要不断保存前缀的字符串)
  5. 适用于前缀关键字查询,如果仅仅是查询是否存在还是很快的。
  6. 手写时注意查询所有的字符串时需要使用到递归,其实是一种DFS算法(递归是最先执行最后放入的方法,会维护一个方法栈,所以空间复杂度高)。
相关推荐
zengy530 分钟前
Effective C++中文版学习记录(三)
数据结构·c++·学习·stl
千里码aicood34 分钟前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455661 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
架构师吕师傅2 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
bug菌2 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
夜月行者4 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
Yvemil74 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
sdg_advance4 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
&梧桐树夏4 小时前
【算法系列-链表】删除链表的倒数第N个结点
数据结构·算法·链表