目录
1121
6 分钟
字典树:前缀匹配的终极数据结构

概述#

Trie,也叫字典树或前缀树,是一种专门处理字符串前缀的数据结构。它把单词拆成字符路径,共享相同前缀的单词会共用同一段节点。

如果需要频繁做“是否存在某个单词”“是否存在某个前缀”“根据输入前缀自动补全”,Trie 比逐个字符串扫描更自然。

它的核心思想是:用树的路径表示字符串。

前置知识

  • 树结构:每条边表示一个字符
  • 哈希表 / 数组:节点子链接可用 Map 或数组存储
  • 前缀匹配:查询过程沿字符逐层向下

问题定义#

假设有三个单词:

cat
car
dog

Trie 会把它们组织成:

root
├─ c
│  └─ a
│     ├─ t (word)
│     └─ r (word)
└─ d
   └─ o
      └─ g (word)

这样查询前缀 ca 时,只需要走 c -> a,就能知道有哪些单词共享这个前缀。

Trie 适合的问题:

需求普通数组Trie
判断单词是否存在O(n * L)O(L)
判断前缀是否存在O(n * L)O(L)
自动补全需要扫描所有单词定位前缀后遍历子树

其中 L 是单词长度,n 是单词数量。


核心原理:分步图解#

插入单词#

插入 cat

root -> c -> a -> t

如果路径不存在,就创建节点;如果路径已存在,就复用节点。最后在 t 节点标记“这里是一个完整单词”。

查询单词#

查询 car

  1. 从 root 出发;
  2. 依次查找 car
  3. 如果路径断开,单词不存在;
  4. 如果路径存在,还要检查最后节点是否标记为完整单词。

查询前缀#

查询前缀只需要路径存在,不要求最后节点是完整单词。

这就是 search("ca")startsWith("ca") 的区别。


算法精细步骤#

Trie 节点通常包含:

children: 当前字符到子节点的映射
isWord:   是否在这里结束一个完整单词

插入流程:

  1. 从根节点开始;
  2. 遍历单词的每个字符;
  3. 如果当前字符没有对应子节点,就创建;
  4. 移动到子节点;
  5. 遍历结束后标记 isWord = true

查询流程类似,只是不创建节点;遇到缺失路径直接返回失败。


TypeScript 实现#

class TrieNode {
  readonly children = new Map<string, TrieNode>();
  isWord = false;
}

class Trie {
  private readonly root = new TrieNode();

  insert(word: string): void {
    let node = this.root;

    for (const char of word) {
      let next = node.children.get(char);

      if (!next) {
        next = new TrieNode();
        node.children.set(char, next);
      }

      node = next;
    }

    node.isWord = true;
  }

  search(word: string): boolean {
    const node = this.findNode(word);
    return node?.isWord ?? false;
  }

  startsWith(prefix: string): boolean {
    return this.findNode(prefix) !== null;
  }

  suggest(prefix: string, limit = 10): string[] {
    const start = this.findNode(prefix);
    if (start === null) return [];

    const result: string[] = [];

    const dfs = (node: TrieNode, path: string): void => {
      if (result.length >= limit) return;
      if (node.isWord) result.push(path);

      for (const [char, child] of node.children) {
        dfs(child, path + char);
      }
    };

    dfs(start, prefix);
    return result;
  }

  private findNode(text: string): TrieNode | null {
    let node = this.root;

    for (const char of text) {
      const next = node.children.get(char);
      if (!next) return null;
      node = next;
    }

    return node;
  }
}

工程优化:压缩 Trie#

标准 Trie 的节点数可能很多,尤其当字符串很长且分支较少时,会产生大量只有一个子节点的节点。

例如:

root -> a -> p -> p -> l -> e

如果中间节点没有分叉,可以压缩成:

root -> "apple"

这类结构叫压缩 Trie 或 Radix Tree。它减少节点数量,但实现更复杂,需要处理字符串片段的拆分和匹配。

工程中是否压缩,取决于数据规模、内存压力和查询性能要求。


应用与局限#

典型应用#

  • 搜索框自动补全;
  • 敏感词匹配;
  • 拼写检查;
  • 路由匹配;
  • IP 前缀匹配;
  • 单词游戏和词典查询。

局限性#

  • 相比哈希表,Trie 占用更多节点对象;
  • 字符集越大,子节点映射越复杂;
  • 删除单词需要清理无用节点;
  • 只适合前缀相关问题,不适合任意子串匹配。

总结#

graph LR
    A[插入字符串] --> B[逐字符建边]
    B --> C[标记单词结束]
    C --> D[查询单词或前缀]
  • Trie 用树路径表示字符串。
  • 共享前缀是 Trie 的核心优势。
  • search 要求完整单词存在,startsWith 只要求路径存在。
  • 自动补全可以先定位前缀节点,再遍历子树。
  • 数据量大时要关注节点数量和压缩策略。
字典树:前缀匹配的终极数据结构
https://www.hehonglei.cn/posts/trie/
作者
Honglei He
发布于
2026-04-26
许可协议
CC BY-NC-SA 4.0