字典树Trie:从前缀匹配到单词搜索的完整实现

前言

字典树(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':
根节点 → app → 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=truetrue
search"app"root→a→p→p,p.isEnd=falsefalse
insert"app"root→a→p→p,p.isEnd=true-
search"app"root→a→p→p,p.isEnd=truetrue
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 核心特点

南北绿豆总结:

  1. 结构:26叉树(或哈希表)
  2. 优势:前缀匹配O(m),m是字符串长度
  3. 空间:公共前缀只存一次
  4. 操作:插入O(m)、查找O(m)、前缀匹配O(m)

8.2 Trie vs HashMap

对比项TrieHashMap
查找单词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题解 - 字典树专题