【设计-1】208.实现Trie (前缀树)

题目描述:

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

解题思路:

方法一:字典树 (26 叉树)

核心思路:

Trie 的结构:
复制代码
root
 ├── a
 │   └── p
 │       └── p
 │           └── l
 │               └── e (isEnd = true)
 └── b
     └── ...

每个节点包含:

  • children[26]:26 个子节点指针(对应 a-z)

  • isEnd:是否是某个单词的结尾

操作逻辑:
操作 逻辑
insert 从根开始,逐个字符向下走,不存在就创建节点,最后标记 isEnd = true
search 从根开始,逐个字符向下走,走不到返回 false,走完检查 isEnd
startsWith 和 search 一样,但不需要检查 isEnd

代码实现:

cpp 复制代码
class Trie {
private:
    struct TrieNode {
        TrieNode* children[26];
        bool isEnd;
        
        TrieNode() {
            for (int i = 0; i < 26; i++) {
                children[i] = nullptr;
            }
            isEnd = false;
        }
    };
    
    TrieNode* root;
    
public:
    Trie() {
        root = new TrieNode();
    }
    
    void insert(string word) {
        TrieNode* node = root;
        for (char c : word) {
            int index = c - 'a';
            if (node->children[index] == nullptr) {
                node->children[index] = new TrieNode();
            }
            node = node->children[index];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        TrieNode* node = root;
        for (char c : word) {
            int index = c - 'a';
            if (node->children[index] == nullptr) {
                return false;
            }
            node = node->children[index];
        }
        return node->isEnd;
    }
    
    bool startsWith(string prefix) {
        TrieNode* node = root;
        for (char c : prefix) {
            int index = c - 'a';
            if (node->children[index] == nullptr) {
                return false;
            }
            node = node->children[index];
        }
        return true;
    }
};

复杂度分析:

设 L 是字符串长度。

操作 时间复杂度 空间复杂度
insert O(L) O(L)(最坏情况新建 L 个节点)
search O(L) O(1)
startsWith O(L) O(1)
整体 - O(总字符数 × 26)

关键细节:

1. 为什么用数组而不是哈希表?
cpp 复制代码
TrieNode* children[26];  // 数组
  • 数组访问 O(1),比哈希表更快

  • 题目限定小写字母,26 个足够

  • 如果字符集更大,可以用 unordered_map<char, TrieNode*>

cpp 复制代码
// search: 必须是一个完整的单词
return node->isEnd;

// startsWith: 只要是前缀就行
return true;

关键 :search 检查 isEnd,startsWith 不检查。

3. 为什么用 isEnd 标记?

因为一个单词可能是另一个单词的前缀:

复制代码
插入 "apple" 后,再插入 "app"
如果不标记 isEnd,search("app") 无法区分 "app" 是完整单词还是 "apple" 的前缀
4. 内存泄漏问题
cpp 复制代码
~Trie() {
    deleteNode(root);
}

void deleteNode(TrieNode* node) {
    for (int i = 0; i < 26; i++) {
        if (node->children[i]) {
            deleteNode(node->children[i]);
        }
    }
    delete node;
}

LeetCode 上不要求,但实际工程中需要释放内存。

方法二:哈希表

为什么用哈希表?

对比维度 数组 children[26] 哈希表 unordered_map
适用字符集 仅小写字母 任意字符集
空间占用 每个节点固定 26 个指针 只存实际存在的子节点
访问速度 O(1) O(1) 平均
代码复杂度 简单 中等

优势:如果字符集很大(如 Unicode),或者子节点很稀疏,哈希表更省空间。

代码实现:

cpp 复制代码
class Trie {
private:
    struct TrieNode {
        unordered_map<char, TrieNode*> children;  // 哈希表存子节点
        bool isEnd;
        
        TrieNode() : isEnd(false) {}
    };
    
    TrieNode* root;
    
public:
    Trie() {
        root = new TrieNode();
    }
    
    void insert(string word) {
        TrieNode* node = root;
        for (char c : word) {
            // 如果子节点不存在,创建
            if (node->children.find(c) == node->children.end()) {
                node->children[c] = new TrieNode();
            }
            node = node->children[c];
        }
        node->isEnd = true;
    }
    
    bool search(string word) {
        TrieNode* node = root;
        for (char c : word) {
            if (node->children.find(c) == node->children.end()) {
                return false;
            }
            node = node->children[c];
        }
        return node->isEnd;
    }
    
    bool startsWith(string prefix) {
        TrieNode* node = root;
        for (char c : prefix) {
            if (node->children.find(c) == node->children.end()) {
                return false;
            }
            node = node->children[c];
        }
        return true;
    }
};

更简洁的写法(用 count):

cpp 复制代码
void insert(string word) {
    TrieNode* node = root;
    for (char c : word) {
        if (!node->children.count(c)) {
            node->children[c] = new TrieNode();
        }
        node = node->children[c];
    }
    node->isEnd = true;
}

bool search(string word) {
    TrieNode* node = root;
    for (char c : word) {
        if (!node->children.count(c)) return false;
        node = node->children[c];
    }
    return node->isEnd;
}

bool startsWith(string prefix) {
    TrieNode* node = root;
    for (char c : prefix) {
        if (!node->children.count(c)) return false;
        node = node->children[c];
    }
    return true;
}

具体过程示例:

插入 "apple" 后:

复制代码
root
 └── children['a'] → node_a
                       └── children['p'] → node_p1
                                             └── children['p'] → node_p2
                                                                   └── children['l'] → node_l
                                                                                         └── children['e'] → node_e (isEnd=true)

哈希表只存实际存在的字符,不像数组要开 26 个位置。

复杂度分析:

设 L 是字符串长度。

操作 时间复杂度 空间复杂度
insert O(L) O(L)
search O(L) O(1)
startsWith O(L) O(1)
整体 - O(总字符数)

空间复杂度对比:

  • 数组:O(总字符数 × 26)

  • 哈希表:O(总字符数)

关键细节:

1. find vs count vs []
写法 行为
children.find(c) == children.end() 判断是否存在
children.count(c) 返回 0 或 1
children[c] 不存在时会创建默认值,慎用

注意 :children[c] 在键不存在时会插入一个默认值 (nullptr),可能改变哈希表大小。

2. 为什么哈希表更省空间?
  • 数组:每个节点固定 26 个指针,共 26 × 8 = 208 字节

  • 哈希表:只存实际存在的子节点,稀疏时节省大量空间

3. 什么时候用数组,什么时候用哈希表?
场景 推荐
仅小写字母 数组(更快)
字符集大(Unicode) 哈希表
子节点稀疏 哈希表
追求极致速度 数组

数组 vs 哈希表对比:

对比维度 数组 哈希表
时间复杂度 O(1) O(1) 平均
空间复杂度 O(26 × 节点数) O(实际子节点数)
适用字符集 固定小字符集 任意字符集
代码复杂度 简单 中等
推荐度 ⭐⭐⭐⭐⭐(小写字母) ⭐⭐⭐⭐(通用)
相关推荐
赛博守夜人3 小时前
跨境业务与出海合规之九:海外买量投放与营销虚假流量(Ad Fraud):跨国机房群控与归因欺诈识别算法
算法·安全
chenbingjie_c3 小时前
C++ STL 适配器:stack & queue,从 vector/list 到 deque 深度解析
数据结构·c++
镜子AI5 小时前
教培机构AI推荐系统实战:概念漂移检测算法——为什么“去年有效的推荐今年可能失效“
人工智能·算法
垆边人似月.6 小时前
华为OD算法题:查找众数及中位数
数据结构·算法·leetcode
hetao17338376 小时前
2026-10-08 hetao1733837 的刷题记录
c++·算法
CoderYanger7 小时前
一轮复习——F.动态规划模型总结(入门篇)
java·开发语言·数据结构·算法·leetcode·职场和发展·动态规划
Frank_refuel7 小时前
【C++】智能指针
开发语言·c++
浅念-8 小时前
动态规划专题:斐波那契数列模型 + 网格路径DP(LeetCode例题全解析)
笔记·算法·leetcode·面试·职场和发展·动态规划·推荐算法
一只旭宝8 小时前
数据结构与算法复习手册:从写过,到讲清楚
开发语言·数据结构·c++·算法
十五年专注C++开发8 小时前
std::filesystem::exists 在有的win10系统抛异常原因分析
c++·文件系统·filesystem