前缀树(Trie,又称字典树或单词查找树)是一种专为字符串处理设计的树形数据结构,由 Edward Fredkin 于 1960 年提出。它通过共享字符串的前缀来高效存储和检索字符串集合,在 autocomplete(自动补全)、拼写检查、IP 路由等场景中有着广泛应用。
前缀树的基本概念与结构
定义与特点
前缀树是一种多叉树,每个节点代表一个字符串的前缀,其特点如下:
- 节点结构:每个节点包含一个布尔值isEnd(标记该节点是否为某个字符串的结尾)和若干个子节点(通常对应 26 个英文字母,也可扩展到其他字符)。
- 根节点:为空节点,不存储任何字符,是所有字符串的起点。
- 前缀共享:具有相同前缀的字符串会共享前缀部分的节点,显著减少存储空间。
- 高效操作:插入、查找、前缀匹配的时间复杂度均为O(L),其中L是字符串的长度,与字符串总数无关。
结构图示
以下是存储字符串 "apple"、"app"、"banana" 的前缀树结构:
前缀树的核心操作
插入操作
思路:从根节点开始,逐个字符处理字符串:
1. 对于当前字符,若节点的子节点中不存在该字符,则创建新节点。
2. 移动到对应子节点,继续处理下一个字符。
3. 字符串处理完毕后,将最后一个节点标记为结尾(`isEnd = true`)。
Java 代码:
class TrieNode {
public TrieNode[] children; // 子节点(假设只包含小写字母)
public boolean isEnd; // 是否为单词结尾
public TrieNode() {
children = new TrieNode[26]; // 26个小写字母
isEnd = false;
}
}
public class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode(); // 根节点为空
}
// 插入字符串
public void insert(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
int index = c - 'a'; // 计算字符索引(0-25)
if (node.children[index] == null) {
node.children[index] = new TrieNode(); // 新建节点
}
node = node.children[index]; // 移动到子节点
}
node.isEnd = true; // 标记单词结尾
}
}
查找操作
思路:从根节点开始遍历字符串的每个字符:
- 若当前字符的子节点不存在,返回false。
- 移动到对应子节点,继续处理下一个字符。
- 字符串处理完毕后,返回最后一个节点的isEnd(确保是完整单词)。
Java 代码:
// 查找字符串是否存在于前缀树中
public boolean search(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
int index = c - 'a';
if (node.children[index] == null) {
return false; // 字符不存在,查找失败
}
node = node.children[index];
}
return node.isEnd; // 必须是单词结尾
}
2.3前缀匹配操作
思路:与查找类似,但无需检查最后一个节点是否为结尾,只需确认前缀存在:
// 检查是否存在以prefix为前缀的字符串
public boolean startsWith(String prefix) {
TrieNode node = root;
for (int i = 0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
int index = c - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true; // 前缀存在
}
LeetCode 例题实战
例题 1:208. 实现 Trie (前缀树)(中等)
题目描述:实现一个 Trie 类,包含 insert、search 和 startsWith 方法。
示例:
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
解题思路
直接实现前缀树的三个核心操作,如上文所述。
代码实现
class Trie {
private class TrieNode {
TrieNode[] children;
boolean isEnd;
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
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;
}
}
复杂度分析
- 时间复杂度:插入、查找、前缀匹配均为O(L),L是字符串长度。
- 空间复杂度:O(L*N),N是插入的字符串数量,L是平均长度(每个字符可能创建新节点)。
例题 2:211. 添加与搜索单词 - 数据结构设计(中等)
题目描述:设计一个支持以下两种操作的数据结构:
void addWord(word)
bool search(word)
search 可以搜索文字或正则表达式字符串,字符串只包含字母.或a-z。.可以表示任何一个字母。
示例:
addWord("bad")
addWord("dad")
addWord("mad")
search("pad") → false
search("bad") → true
search(".ad") → true
search("b..") → true
解题思路
- 数据结构:使用前缀树存储单词,节点结构与基础 Trie 相同。
- 搜索操作:对于.通配符,需要递归检查当前节点的所有非空子节点,其他字符则正常匹配。
搜索过程
(搜索 ".ad")
代码实现
class WordDictionary {
private class TrieNode {
TrieNode[] children;
boolean isEnd;
public TrieNode() {
children = new TrieNode[26];
isEnd = false;
}
}
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);
}
// 递归辅助函数:从index位置开始搜索,当前节点为node
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 childIndex = c - 'a';
TrieNode child = node.children[childIndex];
return child != null && searchHelper(word, index + 1, child);
}
}
}
复杂度分析
- 时间复杂度:
- 插入:O(L),L是单词长度。
- 搜索:最坏O(26^L)(全是通配符,需检查所有可能路径),平均O(L)。
- 空间复杂度:O(L*N),与基础 Trie 相同。
考研 408 例题解析
例题 1:概念辨析题(选择题)
题目:关于前缀树(Trie),下列说法错误的是( )。
A. 前缀树的插入和查找时间复杂度均为O(L),L是字符串长度
B. 前缀树适合存储大量具有相同前缀的字符串
C. 前缀树的空间复杂度通常优于哈希表
D. 前缀树可以高效支持前缀匹配操作
答案:C
解析:
- A 正确:前缀树的操作时间与字符串长度线性相关。
- B 正确:相同前缀的字符串共享节点,节省空间。
- C 错误:对于前缀差异大的字符串,前缀树可能产生大量节点,空间复杂度劣于哈希表。
- D 正确:前缀匹配是前缀树的核心功能,效率远高于哈希表(需遍历所有键)。
例题 2:算法设计题(408 高频考点)
题目:设计一个前缀树,统计每个字符串的出现次数,并实现一个方法getFrequency(String word),返回单词的出现次数。
解题思路
- 节点扩展:在 TrieNode 中增加count字段,记录以该节点为结尾的字符串的出现次数。
- 插入操作:插入时,若单词已存在,只需递增count;否则创建节点并初始化count = 1。
- 查询频率:查找单词路径,若存在则返回count,否则返回 0。
代码实现
class CountTrie {
private class TrieNode {
TrieNode[] children;
int count; // 出现次数
public TrieNode() {
children = new TrieNode[26];
count = 0;
}
}
private TrieNode root;
public CountTrie() {
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.count++; // 递增计数
}
public int getFrequency(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
return 0; // 单词不存在
}
node = node.children[index];
}
return node.count;
}
}
复杂度分析
- 时间复杂度:插入和查询均为O(L),与基础前缀树一致。
- 空间复杂度:O(L*N),额外存储count字段不影响量级。
前缀树的扩展与应用
实际应用场景
- 搜索引擎自动补全:输入前缀时,快速返回所有匹配的搜索词(如 Google 搜索提示)。
- 拼写检查:检查单词是否存在于词典中,或推荐相似单词(如 Word 文档的拼写纠错)。
- IP 路由表:最长前缀匹配算法用于路由选择(将 IP 地址视为字符串)。
- 词频统计:如例题 2,统计文本中单词出现的次数。
与其他数据结构的对比
数据结构 |
插入时间 |
查找时间 |
前缀匹配 |
空间效率(前缀重复时) |
前缀树 |
O(L) |
O(L) |
高效支持 |
优(共享前缀) |
哈希表 |
O(L) |
O(L) |
不支持 |
劣(存储完整字符串) |
红黑树 |
O(LlogN) |
O(LlogN) |
需遍历 |
中(存储完整字符串) |
考研 408 备考要点
- 核心考点:前缀树的结构、插入 / 查找操作、时间与空间复杂度分析。
- 重点掌握:
- 前缀树与哈希表的优缺点对比(适用场景)。
- 前缀树在前缀匹配问题中的不可替代性。
- 带扩展功能的前缀树设计(如计数、通配符匹配)。
- 常见错误:
-
- 忽略isEnd标记,导致混淆前缀和完整单词。
-
- 处理通配符时未考虑所有可能的子节点,导致漏匹配。
总结
前缀树是一种专为字符串处理优化的数据结构,其核心优势在于前缀共享和高效的字符串操作。本文通过 LeetCode 例题(208 题和 211 题)展示了基础前缀树和带通配符的扩展应用,通过考研 408 例题解析了概念辨析和功能扩展思路,并结合 SVG 图示直观呈现了树的结构与操作过程。
掌握前缀树的关键在于:
- 理解其节点结构与前缀共享机制。
- 熟练实现插入、查找、前缀匹配等核心操作。
- 能够根据问题需求扩展前缀树功能(如计数、通配符)。
在考研备考中,需重点关注前缀树与其他数据结构的对比,以及其在实际场景中的应用,这不仅有助于应对理论题,也能提升算法设计能力。
希望本文能够帮助读者更深入地理解前缀树算法,并在实际项目中发挥其优势。谢谢阅读!
希望这份博客能够帮助到你。如果有其他需要修改或添加的地方,请随时告诉我。