【每日算法】LeetCode 208. 实现 Trie (前缀树)

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 208. 实现 Trie (前缀树) 详解

1. 题目描述

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix,返回 true;否则,返回 false

示例:

复制代码
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True

提示:

  • 1 <= word.length, prefix.length <= 2000
  • wordprefix 仅由小写英文字母组成
  • insertsearchstartsWith 调用次数 总计 不超过 3 * 10^4

2. 问题分析

Trie 是一种多叉树结构,每个节点包含以下部分:

  1. 指向子节点的指针数组,通常长度为26(对应26个小写英文字母)。
  2. 一个布尔值,表示该节点是否为某个单词的结束。

Trie 树的根节点不包含字符,每个节点(除根节点)都只包含一个字符。从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。

对于前端开发者来说,理解 Trie 树的结构类似于嵌套对象,每个节点可以看作一个对象,其属性对应子节点,同时有一个属性标记是否为单词结尾。这种数据结构在前端中常用于搜索建议、路由匹配等场景。

3. 解题思路

Trie 树的实现主要涉及三个操作:插入、查找和前缀匹配。每个操作都从根节点开始,沿着字符串的每个字符对应的子节点向下移动。

  • 插入:遍历字符串的每个字符,如果当前节点不存在对应字符的子节点,则创建一个新的子节点。继续向下,直到处理完所有字符,然后将最后一个节点标记为单词结束。
  • 查找:遍历字符串的每个字符,如果当前节点不存在对应字符的子节点,则返回 false。如果遍历完所有字符,并且最后一个节点被标记为单词结束,则返回 true,否则返回 false。
  • 前缀匹配:与查找类似,但不需要检查最后一个节点是否被标记为单词结束,只要所有字符都能在树中找到对应的节点即可。

复杂度分析(n为字符串长度):

  • 插入:时间复杂度 O(n),空间复杂度 O(n)(最坏情况下需要创建新节点)。
  • 查找和前缀匹配:时间复杂度 O(n),空间复杂度 O(1)。

最优解:上述思路即为最优解,因为每个操作都需要遍历整个字符串,无法在时间复杂度上进一步优化。

4. 代码实现(JavaScript)

4.1 Trie 节点定义

首先,我们定义一个 Trie 节点类。每个节点包含一个 children 对象(或数组)和一个 isEnd 布尔标记。

4.2 完整代码实现

javascript 复制代码
// 定义 Trie 节点类
class TrieNode {
    constructor() {
        // 使用数组存储子节点,索引0-25对应a-z
        this.children = new Array(26).fill(null);
        // 标记当前节点是否为单词结尾
        this.isEnd = false;
    }
}

// 定义 Trie 类
class Trie {
    constructor() {
        // 根节点不存储字符
        this.root = new TrieNode();
    }

    // 向 Trie 中插入单词
    insert(word) {
        let node = this.root;
        for (let i = 0; i < word.length; i++) {
            const char = word[i];
            const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
            // 如果当前字符对应的子节点不存在,则创建新节点
            if (!node.children[index]) {
                node.children[index] = new TrieNode();
            }
            // 移动到子节点
            node = node.children[index];
        }
        // 标记单词结束
        node.isEnd = true;
    }

    // 搜索单词是否在 Trie 中
    search(word) {
        let node = this.root;
        for (let i = 0; i < word.length; i++) {
            const char = word[i];
            const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
            // 如果当前字符对应的子节点不存在,则返回 false
            if (!node.children[index]) {
                return false;
            }
            node = node.children[index];
        }
        // 检查最后一个节点是否被标记为单词结束
        return node.isEnd;
    }

    // 检查 Trie 中是否有以 prefix 为前缀的单词
    startsWith(prefix) {
        let node = this.root;
        for (let i = 0; i < prefix.length; i++) {
            const char = prefix[i];
            const index = char.charCodeAt(0) - 'a'.charCodeAt(0);
            if (!node.children[index]) {
                return false;
            }
            node = node.children[index];
        }
        // 只要 prefix 的每个字符都能在树中找到,则返回 true
        return true;
    }
}

// 测试代码
const trie = new Trie();
trie.insert("apple");
console.log(trie.search("apple"));   // true
console.log(trie.search("app"));     // false
console.log(trie.startsWith("app")); // true
trie.insert("app");
console.log(trie.search("app"));     // true

4.3 步骤分解说明

  1. 初始化:创建根节点,根节点不存储字符,但包含26个子节点(初始为null)和一个 isEnd 标志。
  2. 插入操作
    • 从根节点开始,对于要插入的字符串中的每个字符,计算其索引(0-25)。
    • 检查当前节点是否具有该索引的子节点,如果没有则创建一个新的 TrieNode。
    • 移动到该子节点,继续处理下一个字符。
    • 在字符串的所有字符处理完后,将当前节点的 isEnd 标记为 true。
  3. 查找操作
    • 从根节点开始,对于要查找的字符串中的每个字符,计算其索引。
    • 如果当前节点没有对应索引的子节点,则返回 false。
    • 如果有,则移动到该子节点,继续处理下一个字符。
    • 当所有字符处理完后,检查当前节点的 isEnd 是否为 true,是则返回 true,否则返回 false。
  4. 前缀匹配
    • 与查找操作类似,但在所有字符处理完后,不需要检查 isEnd,直接返回 true。

5. 复杂度、优缺点对比表格

操作 时间复杂度 空间复杂度 优点 缺点
插入 O(n) O(n)(最坏情况) 插入速度快,与已存单词数量无关 每个节点需要固定大小的数组,可能浪费空间
查找 O(n) O(1) 查找速度快,与已存单词数量无关
前缀匹配 O(n) O(1) 匹配速度快,与已存单词数量无关

说明

  • n 为字符串的长度。
  • 空间复杂度:插入操作最坏情况下需要为每个字符创建新节点,因此空间复杂度为 O(n)。而查找和前缀匹配只需要常数空间。
  • 使用数组存储子节点,访问速度快,但可能浪费空间(因为每个节点都有26个元素的数组,即使很多是空)。也可以使用对象(Map)来动态存储子节点,以空间换时间。

6. 总结

6.1 通用解题模板

Trie 树的实现有固定的模式:

  1. 定义节点类,包含子节点集合和结束标志。
  2. 初始化根节点。
  3. 插入、查找、前缀匹配操作都是遍历字符串,沿着树向下移动。

对于前端开发,我们可以将 Trie 树应用于:

  • 搜索框的自动补全。
  • 路由匹配(例如,Vue Router 的路由表匹配)。
  • 单词拼写检查。

6.2 类似题目

  1. LeetCode 211. 添加与搜索单词 - 数据结构设计:在 Trie 的基础上,搜索时支持 '.' 通配符。
  2. LeetCode 212. 单词搜索 II:结合 Trie 和回溯,在二维网格中查找单词。
  3. LeetCode 720. 词典中最长的单词:使用 Trie 存储单词,然后查找最长单词。
  4. LeetCode 676. 实现一个魔法字典:在 Trie 上实现模糊搜索(允许一个字符不同)。
相关推荐
代码游侠2 小时前
应用——MPlayer 媒体播放器系统代码详解
linux·运维·笔记·学习·算法
学编程就要猛2 小时前
算法:3.快乐数
java·算法
GSDjisidi2 小时前
国内IT软考证报考流程及前期准备,一篇解读
面试·职场和发展
AI科技星2 小时前
统一场论框架下万有引力常数的量子几何涌现与光速关联
数据结构·人工智能·算法·机器学习·重构
仰泳的熊猫2 小时前
1109 Group Photo
数据结构·c++·算法·pat考试
未来之窗软件服务2 小时前
幽冥大陆(五十八)php1024位密码生成—东方仙盟筑基期
开发语言·算法·仙盟创梦ide·东方仙盟
不解风水3 小时前
【教程笔记】KalmanFilter
笔记·学习·算法·矩阵·ekf
2401_841495643 小时前
【数据结构】最短路径的求解
数据结构·动态规划·贪心·ipython·最短路径·迪杰斯特拉算法·弗洛伊德算法
西安同步高经理3 小时前
秒表实现自动化测量助力时频测量行业发展、秒表检定仪、毫秒表测量仪
人工智能·算法