【前缀树 (Trie)】详解与实现
一、问题理解
前缀树 (Trie) 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。它的核心特点是:
- 每个节点表示一个字符
- 从根节点到某一节点的路径上的字符连接起来,构成该节点对应的字符串
- 常用于自动补全、拼写检查、字典查询等场景
本题要求实现一个 Trie 类,具备以下三个核心功能:
insert(String word)→ 插入一个单词search(String word)→ 查找一个完整的单词是否存在startsWith(String prefix)→ 查找是否存在以某个前缀开头的单词
二、数据结构设计
前缀树的核心在于节点设计。每个节点需要:
- 一个布尔标记
isEnd:表示从根节点到当前节点的路径是否构成一个完整的单词。 - 一个子节点数组
children:用于存储下一个可能出现的字符(通常长度为 26,对应 26 个小写英文字母)。
java
class TrieNode {
boolean isEnd;
TrieNode[] children;
public TrieNode() {
isEnd = false;
children = new TrieNode[26]; // 默认每个元素为 null
}
}
初始化时:
- 创建根节点
root,它是一个虚拟节点,不存储实际字符。 isEnd初始为false。children数组初始化为null数组,表示还没有任何子节点。
三、方法实现详解
1. 插入操作 insert(String word)
插入一个单词时,从根节点出发,依次处理每个字符:
- 计算字符在
children数组中的索引:index = ch - 'a' - 如果当前节点没有对应的子节点,则新建一个节点
- 移动到子节点,继续处理下一个字符
- 处理完最后一个字符后,将当前节点的
isEnd标记为true
java
public void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEnd = true;
}
2. 查找完整单词 search(String word)
查找一个完整单词的过程与插入类似,但有两个关键区别:
- 如果路径中某个字符对应的子节点不存在,直接返回
false - 遍历到最后一个字符后,需要检查该节点的
isEnd是否为true
java
public boolean search(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return node.isEnd;
}
3. 查找前缀 startsWith(String prefix)
查找前缀的过程与 search 类似,但不需要检查 isEnd:
- 只要路径上的所有字符都存在对应的子节点,就说明存在以该前缀开头的单词
java
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char ch : prefix.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true;
}
四、示例演示
假设我们要依次插入以下单词:
"apple""app"
插入过程:
插入 "apple":
root -> a -> p -> p -> l -> e (isEnd = true)
插入 "app":
root -> a -> p -> p (isEnd = true)
查询过程:
search("app")→ 返回true(因为app是一个完整单词)search("ap")→ 返回false(因为路径存在,但isEnd为false)startsWith("ap")→ 返回true(因为存在以ap开头的单词)startsWith("appl")→ 返回true(因为apple以此开头)
五、总结与心得
核心思路:
- 节点设计是关键 :
isEnd标记 +children数组 - 根节点是虚拟节点:不存储字符,只作为起点
- 插入时建路径:按字符逐层创建或复用节点
- 查找时走路径 :路径存在 +
isEnd标记决定结果
记忆点:
- 节点结构固定:
boolean isEnd+TrieNode[26] children - 插入末尾要标记
isEnd = true - 查找完整单词时要检查
isEnd - 查找前缀时只需路径存在即可
为什么归为"图论"?
虽然 Trie 本质是一棵树,但它可以看作一种有向图(节点为状态,边为字符转移)。在算法题分类中,有时将树视为特殊的图,因此将 Trie 归入图论范畴。
六、完整代码模板(Java)
java
class Trie {
class TrieNode {
boolean isEnd;
TrieNode[] children;
public TrieNode() {
isEnd = false;
children = new TrieNode[26];
}
}
private TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - '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 ch : word.toCharArray()) {
int index = ch - '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 ch : prefix.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true;
}
}
学会了 Trie,你就掌握了一种高效处理字符串集合的工具。它不仅是一个经典的数据结构,更是很多高级算法(如 AC 自动机、后缀树)的基础。多写几次,你会发现它的逻辑其实非常直观!