前言
字典树(Trie)是专门处理字符串前缀匹配的数据结构。很多人觉得Trie很复杂,其实Trie就是一棵26叉树,每条从根到叶子的路径代表一个单词。
我并没有能力让你看完就精通所有字符串算法,我只是想让你理解Trie的结构和核心操作。掌握插入、查找、前缀匹配这三个操作,字典树就不再神秘。
摘要
从"实现字典功能"问题出发,剖析字典树的核心结构与操作。通过26叉树的图解演示、插入与查找的详细过程、以及单词搜索问题的Trie优化,揭秘前缀匹配的高效实现。配合LeetCode高频题目与完整代码,给出字典树的解题套路。
一、从字典查找说起
周三早上,哈吉米遇到一道题:
LeetCode 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
哈吉米:"用HashMap不就行了吗?"
南北绿豆:"HashMap可以查找单词,但不能高效查找前缀。"
阿西噶阿西:"比如查找所有以'app'开头的单词,HashMap要遍历所有单词,Trie只需要O(m),m是前缀长度。"
二、生活化场景:新华字典
南北绿豆:"Trie就像新华字典的目录。"
场景:
新华字典查找"苹果":
第1步:找"苹"字的部首"艹"(草字头)
→ 翻到"艹"部的页码
第2步:在"艹"部中找"苹"字
→ 找到"苹"字的条目
第3步:在"苹"字下找"果"字
→ 找到"苹果"词条
Trie树也是这样:
查找"apple":
根节点
→ 找'a'分支
→ 找'p'分支
→ 找'p'分支
→ 找'l'分支
→ 找'e'分支
→ 找到了!
哈吉米:"相当于把字符串拆成一个个字符,用树存储。"
三、Trie树的结构
阿西噶阿西:"Trie是一棵26叉树(假设只有小写字母)。"
3.1 节点结构
class TrieNode {
TrieNode[] children; // 26个子节点(a-z)
boolean isEnd; // 是否是单词结尾
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
3.2 Trie树图示
插入单词:["apple", "app", "bat"]
flowchart TB
Root["根节点"]
A["a"]
P1["p"]
P2["p"]
L["l"]
E["e<br/>isEnd=true"]
P3["p<br/>isEnd=true"]
B["b"]
A2["a"]
T["t<br/>isEnd=true"]
Root --> A
Root --> B
A --> P1
P1 --> P2
P2 --> L
L --> E
P2 --> P3
B --> A2
A2 --> T
style E fill:#e1ffe1
style P3 fill:#e1ffe1
style T fill:#e1ffe1
特点:
- 公共前缀只存一次("app"是"apple"的前缀,共用a→p→p路径)
isEnd=true标记单词结尾
哈吉米:"所以'app'和'apple'共用前面的a→p→p?"
南北绿豆:"对,这就是Trie的优势:节省空间,查找快速。"
四、Trie的核心操作
4.1 插入操作(insert)
思路:从根节点开始,逐个字符往下走,没有路径就创建。
示例:插入"apple"
初始:空Trie
插入'a':
根节点 → children[0] = new TrieNode()
插入'p':
根节点 → a节点 → children[15] = new TrieNode()
...
插入'e':
根节点 → a → p → p → l → e节点
e节点.isEnd = true(标记单词结尾)
代码实现:
Java版本:
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a'; // 字符映射到索引
// 如果路径不存在,创建新节点
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
// 往下走
node = node.children[index];
}
// 标记单词结尾
node.isEnd = true;
}
// 内部类:Trie节点
class TrieNode {
TrieNode[] children;
boolean isEnd;
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
}
C++版本:
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;
}
};
Python版本:
class TrieNode:
def __init__(self):
self.children = {} # 字典存储子节点
self.isEnd = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.isEnd = True
4.2 查找操作(search)
思路:从根节点开始,逐个字符往下走,最后检查isEnd。
Java版本:
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
// 如果路径不存在,说明单词不存在
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
// 检查是否是单词结尾
return node.isEnd;
}
C++版本:
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;
}
Python版本:
def search(self, word):
node = self.root
for c in word:
if c not in node.children:
return False
node = node.children[c]
return node.isEnd
4.3 前缀匹配(startsWith)
思路:和search类似,但不检查isEnd(只要路径存在即可)。
Java版本:
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true; // 不检查isEnd
}
C++版本:
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;
}
Python版本:
def startsWith(self, prefix):
node = self.root
for c in prefix:
if c not in node.children:
return False
node = node.children[c]
return True
五、操作演示
示例:依次插入"apple", "app",然后查找
插入"apple"后的Trie:
root
|
a
|
p
|
p
|
l
|
e (isEnd=true)
插入"app"后的Trie:
root
|
a
|
p
|
p (isEnd=true)
|
l
|
e (isEnd=true)
操作表格:
| 操作 | 参数 | 过程 | 结果 |
|---|---|---|---|
| insert | "apple" | root→a→p→p→l→e,e.isEnd=true | - |
| search | "apple" | root→a→p→p→l→e,e.isEnd=true | true |
| search | "app" | root→a→p→p,p.isEnd=false | false |
| insert | "app" | root→a→p→p,p.isEnd=true | - |
| search | "app" | root→a→p→p,p.isEnd=true | true |
| startsWith | "ap" | root→a→p,路径存在 | true |
哈吉米:"清楚了,search要检查isEnd,startsWith不检查。"
六、例题2:添加与搜索单词
6.1 题目
LeetCode 211 - 添加与搜索单词 - 数据结构设计
请你设计一个数据结构,支持添加新单词和查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
- WordDictionary() 初始化词典对象
- void addWord(word) 将 word 添加到数据结构中
- bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 false 。word 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。
示例:
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]
解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True
6.2 思路分析
南北绿豆:"这题比基础Trie多了个通配符'.',需要用DFS搜索。"
关键点:
- 普通字符:直接往下走
- 通配符'.':尝试所有26个分支(DFS)
阿西噶阿西:"遇到'.'时,遍历当前节点的所有非空子节点。"
6.3 代码实现
Java版本:
class WordDictionary {
private TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
public void addWord(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
return searchHelper(word, 0, root);
}
// DFS搜索
private boolean searchHelper(String word, int index, TrieNode node) {
if (index == word.length()) {
return node.isEnd; // 到达末尾,检查是否是单词
}
char c = word.charAt(index);
if (c == '.') {
// 通配符:尝试所有分支
for (TrieNode child : node.children) {
if (child != null && searchHelper(word, index + 1, child)) {
return true;
}
}
return false;
} else {
// 普通字符:直接往下走
int i = c - 'a';
if (node.children[i] == null) {
return false;
}
return searchHelper(word, index + 1, node.children[i]);
}
}
class TrieNode {
TrieNode[] children;
boolean isEnd;
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
}
C++版本:
class WordDictionary {
private:
struct TrieNode {
TrieNode* children[26];
bool isEnd;
TrieNode() {
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
isEnd = false;
}
};
TrieNode* root;
bool searchHelper(string& word, int index, TrieNode* node) {
if (index == word.size()) {
return node->isEnd;
}
char c = word[index];
if (c == '.') {
for (int i = 0; i < 26; i++) {
if (node->children[i] && searchHelper(word, index + 1, node->children[i])) {
return true;
}
}
return false;
} else {
int i = c - 'a';
if (!node->children[i]) return false;
return searchHelper(word, index + 1, node->children[i]);
}
}
public:
WordDictionary() {
root = new TrieNode();
}
void addWord(string word) {
TrieNode* node = root;
for (char c : word) {
int index = c - 'a';
if (!node->children[index]) {
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->isEnd = true;
}
bool search(string word) {
return searchHelper(word, 0, root);
}
};
Python版本:
class TrieNode:
def __init__(self):
self.children = {}
self.isEnd = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word):
node = self.root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.isEnd = True
def search(self, word):
def dfs(index, node):
if index == len(word):
return node.isEnd
c = word[index]
if c == '.':
# 尝试所有分支
for child in node.children.values():
if child and dfs(index + 1, child):
return True
return False
else:
if c not in node.children:
return False
return dfs(index + 1, node.children[c])
return dfs(0, self.root)
七、例题3:单词搜索II(Trie+回溯)
7.1 题目
LeetCode 212 - 单词搜索 II(Hard)
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words ,
返回所有二维网格上的单词 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,
其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。
同一个单元格内的字母在一个单词中不允许被重复使用。
示例:
输入:board = [["o","a","a","n"],
["e","t","a","e"],
["i","h","k","r"],
["i","f","l","v"]],
words = ["oath","pea","eat","rain"]
输出:["eat","oath"]
7.2 思路分析
南北绿豆:"如果暴力搜索每个单词,会TLE。用Trie优化。"
暴力思路:
对每个单词word:
在board上DFS搜索word
时间复杂度:O(单词数 × 4^(单词长度))
Trie优化思路:
1. 把所有单词插入Trie
2. 在board上DFS,同时在Trie上匹配
3. 如果Trie路径不存在,提前剪枝
为什么快?
阿西噶阿西:"因为单词有公共前缀,Trie避免了重复搜索。比如'eat'和'eats'共用'eat'前缀。"
7.3 代码实现
Java版本:
class Solution {
public List<String> findWords(char[][] board, String[] words) {
// 构建Trie
TrieNode root = new TrieNode();
for (String word : words) {
insert(root, word);
}
List<String> result = new ArrayList<>();
int m = board.length, n = board[0].length;
// 从每个位置开始DFS
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
dfs(board, i, j, root, result);
}
}
return result;
}
private void dfs(char[][] board, int i, int j, TrieNode node, List<String> result) {
char c = board[i][j];
// 越界或已访问或Trie中没有这个字符
if (c == '#' || node.children[c - 'a'] == null) {
return;
}
node = node.children[c - 'a'];
// 找到一个单词
if (node.word != null) {
result.add(node.word);
node.word = null; // 去重:避免重复添加
}
// 标记已访问
board[i][j] = '#';
// 四个方向DFS
if (i > 0) dfs(board, i - 1, j, node, result);
if (i < board.length - 1) dfs(board, i + 1, j, node, result);
if (j > 0) dfs(board, i, j - 1, node, result);
if (j < board[0].length - 1) dfs(board, i, j + 1, node, result);
// 恢复现场
board[i][j] = c;
}
private void insert(TrieNode root, String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.word = word; // 存储完整单词
}
class TrieNode {
TrieNode[] children = new TrieNode[26];
String word = null; // 存储单词(而不是boolean)
}
}
C++版本:
class Solution {
private:
struct TrieNode {
TrieNode* children[26];
string word;
TrieNode() {
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
word = "";
}
};
void insert(TrieNode* root, string& word) {
TrieNode* node = root;
for (char c : word) {
int index = c - 'a';
if (!node->children[index]) {
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->word = word;
}
void dfs(vector<vector<char>>& board, int i, int j, TrieNode* node, vector<string>& result) {
char c = board[i][j];
if (c == '#' || !node->children[c - 'a']) return;
node = node->children[c - 'a'];
if (!node->word.empty()) {
result.push_back(node->word);
node->word = "";
}
board[i][j] = '#';
if (i > 0) dfs(board, i - 1, j, node, result);
if (i < board.size() - 1) dfs(board, i + 1, j, node, result);
if (j > 0) dfs(board, i, j - 1, node, result);
if (j < board[0].size() - 1) dfs(board, i, j + 1, node, result);
board[i][j] = c;
}
public:
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
TrieNode* root = new TrieNode();
for (auto& word : words) {
insert(root, word);
}
vector<string> result;
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
dfs(board, i, j, root, result);
}
}
return result;
}
};
Python版本:
class Solution:
def findWords(self, board, words):
# 构建Trie
root = TrieNode()
for word in words:
node = root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = word
result = []
m, n = len(board), len(board[0])
def dfs(i, j, node):
c = board[i][j]
if c == '#' or c not in node.children:
return
node = node.children[c]
if node.word:
result.append(node.word)
node.word = None # 去重
board[i][j] = '#'
if i > 0: dfs(i - 1, j, node)
if i < m - 1: dfs(i + 1, j, node)
if j > 0: dfs(i, j - 1, node)
if j < n - 1: dfs(i, j + 1, node)
board[i][j] = c
for i in range(m):
for j in range(n):
dfs(i, j, root)
return result
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
八、Trie树总结
8.1 核心特点
南北绿豆总结:
- 结构:26叉树(或哈希表)
- 优势:前缀匹配O(m),m是字符串长度
- 空间:公共前缀只存一次
- 操作:插入O(m)、查找O(m)、前缀匹配O(m)
8.2 Trie vs HashMap
| 对比项 | Trie | HashMap |
|---|---|---|
| 查找单词 | O(m) | O(1) |
| 前缀匹配 | O(m) | O(n×m) |
| 空间 | 公共前缀共享 | 每个单词独立 |
| 适用场景 | 前缀查询、自动补全 | 精确查找 |
8.3 识别技巧
阿西噶阿西:
- 看到前缀、前缀匹配,想Trie
- 看到自动补全、搜索提示,想Trie
- 看到单词、字典,可能用Trie
8.4 通用模板
Java版本:
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return node.isEnd;
}
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true;
}
class TrieNode {
TrieNode[] children;
boolean isEnd;
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
}
参考资料:
- 《算法第四版》- Robert Sedgewick
- 《算法导论》- Thomas H. Cormen
- LeetCode题解 - 字典树专题