当前位置: 首页 > news >正文

【UCB CS 61B SP24】Lecture 28 - Tries 学习笔记

本文介绍并用 Java 实现了用于存储字符串的高效数据结构 Trie,Trie 可以在近乎常数的时间复杂度下进行字符串的插入与查询操作,且效率不受单词的数量影响。

1. Trie 字典树介绍

Trie(前缀树或字典树)是一种树形数据结构,用于高效存储和检索字符串集合中的键。其核心思想是通过共享公共前缀来减少存储开销,支持快速插入、搜索和前缀匹配操作。

Trie 的主要特点如下:

  • 高效的查找:查找一个字符串的时间复杂度与字符串长度成正比,也就是 O ( m ) O(m) O(m)与存储的单词数量无关
  • 公共前缀共享:相同前缀的字符串共享同一路径,降低冗余存储。
  • 适用场景:字典实现、自动补全、拼写检查、前缀匹配等。

2. 实现 Trie 字典树

首先来画个图看看 Trie 如何表示字符串,Trie 中的每个节点表示一个字符,例如下面这棵 Trie 树可以表示 sadsamsame 等字符串:

在这里插入图片描述

但是我们如何表示 awsa 这类不存在的字符串呢?因此每个节点还需要用一个变量 isEnd 标记是否为字符串的结尾,例如下面这棵 Trie 树表示我们存储了 6 个字符串分别为 aawlssadsamsapsame

在这里插入图片描述

接下来我们来看一下具体实现 Tire 的流程:

  • 插入(Insert):从根节点出发,依次根据字符串中的每个字符寻找(或创建)相应的子节点;遍历完所有字符后,将最后一个节点标记为结束节点。
  • 查找(Search):按照单词中的每个字符逐层遍历,如果某个字符对应的子节点不存在,则说明该单词不存在;遍历结束后,检查最后一个节点是否标记为结束节点,如果是则单词存在,否则不存在。
  • 前缀匹配(StartsWith):与查找类似,但只需判断从根到最后一个字符的路径是否存在,而不关心最后节点是否为完整单词的结束标志。

假设我们处理只有小写字母的单词,那么我们可以让每个节点包含一个大小为 26 的数组来存储所有可能的子节点。但是这样做可能会造成空间的浪费,如果 Trie 树的每一层都只有一个节点,那么每一层的数组中都会有 25 个位置是 null,如下图所示:

在这里插入图片描述

我们可以用 Lecture 19 中学到的哈希表来完成字符到子节点的映射来优化这个问题,此处我们直接调用 java.util.HashMap 实现,使用哈希表后的内存空间使用如下:

在这里插入图片描述

Java 实现 Trie 代码如下:

package CS61B.Lecture28;

import java.util.HashMap;

public class Trie {
    private static class TrieNode {
        HashMap<Character, TrieNode> children;
        boolean isEnd;

        public TrieNode() {
            children = new HashMap<>();
            isEnd = false;
        }
    }

    private final TrieNode root;

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

    /** 核心操作:插入,将 word 插入 Trie 中 */
    public void insert(String word) {
        TrieNode p = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            p = p.children.computeIfAbsent(c, key -> new TrieNode());
        }
        p.isEnd = true;
    }

    /** 核心操作:查找,返回完整单词 word 是否存在 */
    public boolean search(String word) {
        TrieNode prefix = searchPrefix(word);
        return prefix != null && prefix.isEnd;
    }

    /** 核心操作:前缀匹配,返回前缀 prefix 是否存在 */
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null;
    }

    /** 辅助方法:查找并返回 word 所在的最后一个节点 */
    private TrieNode searchPrefix(String word) {
        TrieNode p = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (p.children.get(c) == null) return null;
            p = p.children.get(c);
        }
        return p;
    }

    /** 测试 */
    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("sad");
        trie.insert("sam");
        System.out.println(trie.search("sad"));  // true
        System.out.println(trie.startsWith("sa"));  // true
        System.out.println(trie.search("sa"));  // false
    }
}

注意:在 insert 方法中使用的 computeIfAbsent(key, mappingFunction) 方法是 Java 8 在 Map 接口中引入的一个默认方法,它的主要作用是根据给定的 key 获取其对应的 value,如果该 key 在 Map 中不存在,则使用提供的映射函数(Mapping Function)计算一个新的 value,并将这个 key-value 对存入 Map 中,然后返回计算得到的 value。也就是说等价于下面这种写法:

if (p.children.get(c) == null) p.children.put(c, new TrieNode());
p = p.children.get(c);

相关文章:

  • 使用 Elastic-Agent 或 Beats 将 Journald 中的 syslog 和 auth 日志导入 Elastic Stack
  • Go语言实战,HTTP和gRPC多服务启动与关闭的最佳实践
  • aws(学习笔记第三十二课) 深入使用cdk(API Gateway + event bridge)
  • 数据结构常见面试题
  • Java后端高频面经——计算机网络
  • Java线程池深度解析,从源码到面试热点
  • sudo systemctl restart docker 重启docker失败
  • 【数据结构】关键路径
  • 高等数学-第七版-上册 选做记录 习题3-6
  • 3.组合模式
  • C++ Qt创建计时器
  • gazebo官方模型库物品
  • 通俗易懂的介绍LLM大模型技术常用专业名词(专业版)
  • 全原子 MD 结合自适应采样技术揭示 Hsp70 构象循环突变的分子机制
  • 前端学习——CSS
  • Scaled_dot_product_attention(SDPA)使用详解
  • 1.1Vue 3 核心优势与架构革新
  • Mac本地安装运行FastDFS
  • CSK6大模型语音开发板接入DeepSeek排错流程参考
  • java后端开发day29--常见算法(一)----查找与排序
  • 国内做外贸网站的有哪些资料/app推广怎么做
  • 网站正在建设中 html 模板/seo官网
  • 火山开发软件可以开发网站吗/营销策略的思路
  • 广西壮族自治区医保网上服务大厅/北京seo地址
  • 商用图片的网站/seo招聘职责
  • 学做网站论坛/seo优化便宜