Trie(发音同”try”),又称前缀树或字典树,是一种高效的多叉树形数据结构,专门用于处理字符串的存储与检索。其核心思想是利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。从搜索引擎的关键词提示到IDE的代码补全,Trie 树无处不在。本文将用 Java 从零实现一套完整的 Trie 树,涵盖插入、精确搜索、前缀匹配三大核心操作,并基于它构建一个命令行自动补全系统。
一、Trie 树的核心思想
假设需要存储单词:cat、car、cars、dog、door。若使用普通列表存储,搜索是否存在 “cars” 需要逐个比对,时间复杂度为 O(N×L)(N 为单词数,L 为单词长度)。而 Trie 树将这些单词按字符层级展开:
- 根节点为空,第一层分叉为 ‘c’ 和 ‘d’
- ‘c’ 下再分 ‘a’,’a’ 下再分 ‘t’ 和 ‘r’
- ‘r’ 下再继续分 ‘s’
如此,搜索 “cars” 只需沿着 c→a→r→s 走 4 步即可判定存在与否,时间复杂度降至 O(L)。前缀匹配同样高效:查找所有以 “ca” 开头的单词,只需先走到 ‘c’→’a’ 节点,再遍历其整个子树即可。
二、核心数据结构
2.1 Trie 节点
每个节点保存一个字符到子节点的映射,以及一个标记位 isEndOfWord,表示从根到该节点是否构成一个完整单词。
import java.util.HashMap;
import java.util.Map;
/**
* Trie 树的节点类
* 每个节点代表一个字符,通过 children 映射维护后续字符分支
*/
public class TrieNode {
// 子节点映射:字符 -> 子节点
private final Map<Character, TrieNode> children;
// 标记从根到当前节点是否构成一个完整单词
private boolean isEndOfWord;
// 记录经过该节点的单词数量(用于前缀频率统计)
private int passCount;
public TrieNode() {
this.children = new HashMap<>();
this.isEndOfWord = false;
this.passCount = 0;
}
public boolean containsChild(char ch) {
return children.containsKey(ch);
}
public TrieNode getChild(char ch) {
return children.get(ch);
}
public void putChild(char ch, TrieNode node) {
children.put(ch, node);
}
public boolean isEndOfWord() {
return isEndOfWord;
}
public void setEndOfWord(boolean endOfWord) {
isEndOfWord = endOfWord;
}
public int getPassCount() {
return passCount;
}
public void incrementPassCount() {
passCount++;
}
public void decrementPassCount() {
passCount--;
}
public Map<Character, TrieNode> getChildren() {
return children;
}
}
2.2 Trie 树主体
import java.util.ArrayList;
import java.util.List;
/**
* Trie 前缀树实现
* 支持:插入、搜索、前缀匹配、删除、自动补全
*/
public class Trie {
private final TrieNode root;
private int size; // 存储的单词总数
public Trie() {
this.root = new TrieNode();
this.size = 0;
}
/**
* 插入一个单词到 Trie 树
* 时间复杂度:O(L),L 为单词长度
*/
public void insert(String word) {
if (word == null || word.isEmpty()) {
return;
}
TrieNode current = root;
for (char ch : word.toCharArray()) {
if (!current.containsChild(ch)) {
current.putChild(ch, new TrieNode());
}
current = current.getChild(ch);
current.incrementPassCount();
}
// 如果该单词已存在,不重复计数
if (!current.isEndOfWord()) {
current.setEndOfWord(true);
size++;
}
}
/**
* 精确搜索:判断单词是否存在于 Trie 树中
* 时间复杂度:O(L)
*/
public boolean search(String word) {
if (word == null || word.isEmpty()) {
return false;
}
TrieNode node = searchNode(word);
return node != null && node.isEndOfWord();
}
/**
* 前缀搜索:判断是否存在以指定前缀开头的单词
* 时间复杂度:O(P),P 为前缀长度
*/
public boolean startsWith(String prefix) {
if (prefix == null || prefix.isEmpty()) {
return false;
}
return searchNode(prefix) != null;
}
/**
* 获取指定前缀的出现频率(有多少单词以此前缀开头)
*/
public int prefixCount(String prefix) {
TrieNode node = searchNode(prefix);
return node != null ? node.getPassCount() : 0;
}
/**
* 自动补全:返回所有以指定前缀开头的完整单词
* 时间复杂度:O(P + M),P 为前缀长度,M 为子树节点总数
*/
public List<String> autoComplete(String prefix) {
List<String> results = new ArrayList<>();
if (prefix == null) {
return results;
}
TrieNode prefixNode = searchNode(prefix);
if (prefixNode == null) {
return results;
}
// 从 prefixNode 开始 DFS 遍历所有子树路径
collectAllWords(prefixNode, new StringBuilder(prefix), results);
return results;
}
/**
* 删除单词(可选高级操作)
* 采用自底向上的递归清理空分支
*/
public boolean delete(String word) {
if (word == null || word.isEmpty() || !search(word)) {
return false;
}
deleteHelper(root, word, 0);
size--;
return true;
}
public int size() {
return size;
}
// ========== 私有辅助方法 ==========
/**
* 沿着字符串路径查找最终节点
*/
private TrieNode searchNode(String str) {
TrieNode current = root;
for (char ch : str.toCharArray()) {
if (!current.containsChild(ch)) {
return null;
}
current = current.getChild(ch);
}
return current;
}
/**
* DFS 收集以当前节点为根的所有完整单词
*/
private void collectAllWords(TrieNode node, StringBuilder prefix, List<String> results) {
if (node.isEndOfWord()) {
results.add(prefix.toString());
}
for (Map.Entry<Character, TrieNode> entry : node.getChildren().entrySet()) {
prefix.append(entry.getKey());
collectAllWords(entry.getValue(), prefix, results);
prefix.deleteCharAt(prefix.length() - 1);
}
}
/**
* 递归删除单词并清理无用节点
*/
private boolean deleteHelper(TrieNode current, String word, int index) {
if (index == word.length()) {
current.setEndOfWord(false);
current.decrementPassCount();
// 如果没有子节点,可以删除
return current.getChildren().isEmpty();
}
char ch = word.charAt(index);
TrieNode child = current.getChild(ch);
boolean shouldDeleteChild = deleteHelper(child, word, index + 1);
if (shouldDeleteChild) {
current.getChildren().remove(ch);
current.decrementPassCount();
// 如果当前节点既不是单词结尾也没有其他子节点,也可删除
return !current.isEndOfWord() && current.getChildren().isEmpty();
}
current.decrementPassCount();
return false;
}
}
三、自动补全系统
基于 Trie 树构建一个实用的命令行自动补全系统,模拟 IDE 或搜索引擎的输入提示效果。
import java.util.List;
import java.util.Scanner;
/**
* 基于 Trie 的自动补全演示系统
*/
public class AutoCompleteSystem {
private final Trie trie;
public AutoCompleteSystem() {
this.trie = new Trie();
}
/**
* 批量初始化词典
*/
public void loadDictionary(String[] words) {
for (String word : words) {
trie.insert(word.toLowerCase());
}
System.out.println("词典加载完成,共 " + trie.size() + " 个单词");
}
/**
* 查询自动补全建议
*/
public void query(String prefix) {
prefix = prefix.toLowerCase().trim();
if (prefix.isEmpty()) {
System.out.println("请输入非空前缀");
return;
}
List<String> suggestions = trie.autoComplete(prefix);
int prefixFreq = trie.prefixCount(prefix);
System.out.println("\n前缀 '" + prefix + "' 的匹配结果:");
System.out.println(" 匹配单词数:" + suggestions.size());
System.out.println(" 前缀频率:" + prefixFreq);
if (suggestions.isEmpty()) {
System.out.println(" (无匹配结果)");
} else {
// 最多展示 10 条建议
int limit = Math.min(suggestions.size(), 10);
for (int i = 0; i < limit; i++) {
System.out.println(" " + (i + 1) + ". " + suggestions.get(i));
}
if (suggestions.size() > limit) {
System.out.println(" ... 还有 " + (suggestions.size() - limit) + " 条结果");
}
}
}
/**
* 交互式命令行界面
*/
public void interactiveMode() {
Scanner scanner = new Scanner(System.in);
System.out.println("\n=== Trie 自动补全系统 ===");
System.out.println("输入前缀查看补全建议,输入 'quit' 退出");
while (true) {
System.out.print("\n> ");
String input = scanner.nextLine().trim();
if ("quit".equalsIgnoreCase(input)) {
System.out.println("再见!");
break;
}
query(input);
}
}
public static void main(String[] args) {
AutoCompleteSystem system = new AutoCompleteSystem();
// 模拟编程语言关键词词典
String[] dictionary = {
"abstract", "assert", "boolean", "break", "byte",
"case", "catch", "char", "class", "const",
"continue", "default", "do", "double", "else",
"enum", "extends", "final", "finally", "float",
"for", "goto", "if", "implements", "import",
"instanceof", "int", "interface", "long", "native",
"new", "package", "private", "protected", "public",
"return", "short", "static", "strictfp", "super",
"switch", "synchronized", "this", "throw", "throws",
"transient", "try", "void", "volatile", "while",
// 额外补充一些常见类名和方法前缀
"string", "stringbuilder", "stringbuffer",
"system", "scanner", "random", "math",
"arraylist", "linkedlist", "hashmap", "hashset",
"treeset", "treemap", "priorityqueue", "arraydeque",
"comparable", "comparator", "runnable", "callable",
"exception", "runtimeexception", "throwable", "error"
};
system.loadDictionary(dictionary);
// 演示几个查询
System.out.println("\n--- 演示查询 ---");
system.query("st");
system.query("str");
system.query("th");
system.query("java");
// 进入交互模式
system.interactiveMode();
}
}
四、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 插入 (insert) | O(L) | O(L) | L 为单词长度,最坏情况下每个字符都创建新节点 |
| 精确搜索 (search) | O(L) | O(1) | 只需沿着路径遍历,无需额外空间 |
| 前缀匹配 (startsWith) | O(P) | O(1) | P 为前缀长度 |
| 自动补全 (autoComplete) | O(P + M) | O(M) | P 为前缀长度,M 为子树节点数;结果收集需遍历子树 |
| 删除 (delete) | O(L) | O(1) | 递归清理,仅修改引用 |
与哈希表的对比
| 场景 | Trie | 哈希表 |
|---|---|---|
| 精确查找 | O(L) | O(1) 平均 |
| 前缀匹配 | O(P) 极快 | 需遍历所有键 O(N) |
| 自动补全 | O(P + M) 高效 | 需遍历所有键 O(N) |
| 空间占用 | 公共前缀共享,较节省 | 每个键独立存储 |
结论:Trie 树在前缀相关操作上有绝对优势,是自动补全、拼写检查、IP 路由查找等场景的首选数据结构。
五、扩展方向
- 压缩 Trie (Radix Tree):将只有一个子节点的链压缩为单条边,进一步节省空间,常用于 Linux 内核的路由表和 Redis 的集群槽位管理。
- 后缀树 (Suffix Tree):存储字符串的所有后缀,可在 O(L) 时间内解决最长重复子串、最长公共子串等问题。
- AC 自动机 (Aho-Corasick):在 Trie 基础上增加失败指针,实现多模式串的并行匹配,广泛应用于敏感词过滤和病毒特征码扫描。
- 持久化 Trie:每次修改创建新路径复用旧节点,支持高效的历史版本查询,常见于区块链状态存储和函数式编程语言。
六、项目结构
trie-demo/
├── src/
│ ├── TrieNode.java
│ ├── Trie.java
│ └── AutoCompleteSystem.java
└── README.md
七、总结
Trie 前缀树通过将字符串按字符层级展开,将前缀匹配的时间复杂度从 O(N) 降低到 O(P),是处理字符串检索问题的利器。本文用 Java 完整实现了 Trie 的核心操作,并构建了一个可交互的自动补全系统。理解 Trie 的设计思想,不仅能帮助解决实际的字符串检索问题,也为学习更高级的结构(如后缀树、AC 自动机)打下坚实基础。