字典樹 Trie — 前綴搜尋與自動補全的核心資料結構 | 資料結構與演算法

2026/06/24
字典樹 Trie — 前綴搜尋與自動補全的核心資料結構 | 資料結構與演算法

字典樹(Trie,又稱前綴樹) 是一種專門為字串集合設計的多路樹資料結構,以極高的效率支援前綴查詢(Prefix Query)。插入、搜尋、前綴匹配的時間複雜度皆為 O(L)(L 為字串長度),完全不受字典大小影響。理解 Trie 是掌握搜尋引擎自動補全、拼字檢查與路由演算法的關鍵。

前言

想像你在手機上打 “ap”,輸入法立刻推薦 “apple”、“application”、“apply”——這背後是什麼魔法?

答案是 Trie

如果用 HashMap 存所有單詞,每次前綴搜尋都需要遍歷整個字典;如果用排序陣列加二分搜尋,也要 O(log n × L) 的時間。但 Trie 把字串的每個字元作為樹的一層,共享相同前綴的字串走同一條路徑——搜尋前綴 “ap” 只需走兩步,就能找到所有以 “ap” 開頭的詞。

本文學習要點:

  • Trie 節點結構與前綴共享原理
  • 三大核心操作:insertsearchstartsWith
  • TypeScript 完整實作(含 delete、自動補全)
  • C++ 陣列 vs HashMap 雙實作對照
  • 壓縮 Trie(Radix Tree)、XOR Trie、Suffix Trie 延伸
  • 五大面試考點與 LeetCode 五道精選題

核心概念

Trie 節點結構

Trie 的每個節點只需兩個欄位:

TrieNode {
  children: Map<char, TrieNode>   // 子節點映射,key 為字元
  isEnd: boolean                  // 此節點是否為某個字串的結尾
}

節點本身不儲存字元——字元隱含在父節點到此節點的**邊(edge)**上。從根節點到任一節點的路徑,串起來就是對應的字串前綴。

前綴共享原理

Trie 的精髓是「共享前綴,節省空間」。插入 “apple”、“app”、“apply”、“apt”、“banana” 後的樹結構:

                    [root]
                   /      \
                 'a'       'b'
                  |         |
                 'p'       'a'
                / \         |
              'p'  't'     'n'
              |*    |*      |
             'l'   (end)   'a'
            /   \           |
          'e'   'y'        'n'
           |*    |*          |
          (end) (end)       'a'
                             |*
                            (end)

標記 * = isEnd = true
"app"   → root→a→p→p*
"apple" → root→a→p→p→l→e*
"apply" → root→a→p→p→l→y*
"apt"   → root→a→p→t*
"banana"→ root→b→a→n→a→n→a*

“apple”、“apply”、“app” 全部共用 root→a→p→p 這段路徑,只在 p 節點之後才分叉。這正是 Trie 能在 O(L) 完成前綴查詢的原因。

三大核心操作

insert(word):逐字元遍歷,若子節點不存在則新建,遍歷完成後將最後節點的 isEnd 設為 true

search(word):逐字元遍歷,若中途找不到對應子節點則返回 false,遍歷完成後返回當前節點的 isEnd 值(必須是完整單詞,不能只是前綴)。

startsWith(prefix):與 search 邏輯相同,但遍歷完成後只要節點存在即返回 true,不檢查 isEnd。前綴存在就算符合。


JavaScript / TypeScript 實作

基礎 Trie 類別(insert / search / startsWith / delete)

class TrieNode {
  children: Map<string, TrieNode>;
  isEnd: boolean;

  constructor() {
    this.children = new Map();
    this.isEnd = false;
  }
}

class Trie {
  private root: TrieNode;

  constructor() {
    this.root = new TrieNode();
  }

  // ─── 插入:O(L) ───────────────────────────────────────────────────────────
  insert(word: string): void {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) {
        node.children.set(ch, new TrieNode());
      }
      node = node.children.get(ch)!;
    }
    node.isEnd = true;
  }

  // ─── 搜尋(完整單詞):O(L) ───────────────────────────────────────────────
  search(word: string): boolean {
    const node = this._traverse(word);
    return node !== null && node.isEnd; // 必須到達且是完整字串結尾
  }

  // ─── 前綴匹配:O(L) ────────────────────────────────────────────────────────
  startsWith(prefix: string): boolean {
    return this._traverse(prefix) !== null; // 節點存在即可,不需要 isEnd
  }

  private _traverse(str: string): TrieNode | null {
    let node = this.root;
    for (const ch of str) {
      if (!node.children.has(ch)) return null;
      node = node.children.get(ch)!;
    }
    return node;
  }

  // ─── 刪除:後序遞迴,清理只有此字串使用的孤立節點 ─────────────────────────
  delete(word: string): boolean {
    return this._delete(this.root, word, 0);
  }

  private _delete(node: TrieNode, word: string, depth: number): boolean {
    if (depth === word.length) {
      if (!node.isEnd) return false; // 字串不存在
      node.isEnd = false;
      return node.children.size === 0; // 無子節點,可刪除此節點
    }
    const ch = word[depth];
    const child = node.children.get(ch);
    if (!child) return false;

    const shouldDelete = this._delete(child, word, depth + 1);
    if (shouldDelete) {
      node.children.delete(ch);
      // 若此節點也不是其他字串結尾且無子節點,也可以被刪除
      return !node.isEnd && node.children.size === 0;
    }
    return false;
  }
}

// 使用範例
const trie = new Trie();
trie.insert("apple");
trie.insert("app");
trie.insert("apply");

console.log(trie.search("app"));       // true("app" 是完整單詞)
console.log(trie.search("appl"));      // false("appl" 未被插入)
console.log(trie.startsWith("app"));   // true(前綴存在)
console.log(trie.startsWith("apt"));   // false("apt" 未被插入)

trie.delete("app");
console.log(trie.search("app"));       // false(已刪除)
console.log(trie.search("apple"));     // true("apple" 仍存在)

自動補全(帶搜尋排名的 Top-K)

實務中的自動補全除了找出所有前綴符合的詞,還需要按「熱門程度」排序,只返回前 K 個結果。有兩種策略:

  • 預計算 Top-K:插入時在每個前綴節點維護一個排序好的 Top-K 列表,查詢 O(P),但插入較慢。
  • 即時 DFS:查詢時從前綴節點做 DFS 收集所有符合詞後再排序,查詢 O(P + R)(R 為所有結果總字元數)。
interface AutocompleteEntry {
  word: string;
  score: number; // 搜尋頻率或排名分數
}

class RankedTrieNode {
  children: Map<string, RankedTrieNode>;
  entries: AutocompleteEntry[]; // 此節點作為前綴時的 Top-K 結果(預計算)
  isEnd: boolean;
  score: number;

  constructor() {
    this.children = new Map();
    this.entries = [];
    this.isEnd = false;
    this.score = 0;
  }
}

class AutocompleteTrie {
  private root: RankedTrieNode;
  private readonly TOP_K = 3;

  constructor() {
    this.root = new RankedTrieNode();
  }

  // 插入帶分數的詞條,沿途更新每個前綴節點的 Top-K 列表
  insert(word: string, score: number): void {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) {
        node.children.set(ch, new RankedTrieNode());
      }
      node = node.children.get(ch)!;
      this._updateTopK(node, { word, score });
    }
    node.isEnd = true;
    node.score = score;
  }

  private _updateTopK(node: RankedTrieNode, entry: AutocompleteEntry): void {
    const existing = node.entries.findIndex(e => e.word === entry.word);
    if (existing >= 0) {
      node.entries[existing].score = Math.max(node.entries[existing].score, entry.score);
    } else {
      node.entries.push(entry);
    }
    // 按分數降序,保留 Top-K
    node.entries.sort((a, b) => b.score - a.score);
    if (node.entries.length > this.TOP_K) {
      node.entries.length = this.TOP_K;
    }
  }

  // O(P) 時間直接返回預計算的 Top-K 結果
  getTopK(prefix: string): AutocompleteEntry[] {
    let node = this.root;
    for (const ch of prefix) {
      if (!node.children.has(ch)) return [];
      node = node.children.get(ch)!;
    }
    return node.entries;
  }

  // 即時 DFS 版本:動態蒐集所有前綴下的詞條再排序
  getSuggestionsLive(prefix: string): string[] {
    let node = this.root;
    for (const ch of prefix) {
      if (!node.children.has(ch)) return [];
      node = node.children.get(ch)!;
    }
    const results: AutocompleteEntry[] = [];
    this._dfs(node, prefix, results);
    results.sort((a, b) => b.score - a.score);
    return results.slice(0, this.TOP_K).map(e => e.word);
  }

  private _dfs(node: RankedTrieNode, currentWord: string, results: AutocompleteEntry[]): void {
    if (node.isEnd) {
      results.push({ word: currentWord, score: node.score });
    }
    for (const [ch, child] of node.children) {
      this._dfs(child, currentWord + ch, results);
    }
  }
}

// 使用範例:模擬搜尋引擎自動補全
const ac = new AutocompleteTrie();
ac.insert("apple", 100);
ac.insert("app", 80);
ac.insert("apply", 60);
ac.insert("apt", 40);
ac.insert("application", 120);

// 輸入 "app" 時返回前三名
console.log(ac.getTopK("app"));
// [{word:"application",score:120},{word:"apple",score:100},{word:"app",score:80}]

XOR Trie(最大 XOR 值)

XOR Trie 是 Trie 的位元變體,用於在整數集合中尋找最大 XOR 值的數對。每個整數以 32 位元二進制從最高位到最低位插入 Trie,查詢時貪婪選擇與當前位相反的子節點,使 XOR 結果每一位盡量為 1。

class XorTrieNode {
  children: [XorTrieNode | null, XorTrieNode | null];
  constructor() {
    this.children = [null, null];
  }
}

class XorTrie {
  private root: XorTrieNode;
  private readonly BITS = 31; // 覆蓋至多 2^31 - 1

  constructor() {
    this.root = new XorTrieNode();
  }

  insert(num: number): void {
    let node = this.root;
    for (let i = this.BITS; i >= 0; i--) {
      const bit = (num >> i) & 1;
      if (!node.children[bit]) {
        node.children[bit] = new XorTrieNode();
      }
      node = node.children[bit]!;
    }
  }

  // 查詢與 num 能產生最大 XOR 的值,返回最大 XOR 結果
  maxXorWith(num: number): number {
    let node = this.root;
    let maxXor = 0;
    for (let i = this.BITS; i >= 0; i--) {
      const bit = (num >> i) & 1;
      const want = 1 - bit; // 希望走相反位元使 XOR 為 1
      if (node.children[want]) {
        maxXor |= (1 << i);
        node = node.children[want]!;
      } else if (node.children[bit]) {
        node = node.children[bit]!;
      } else {
        break;
      }
    }
    return maxXor;
  }
}

// LeetCode 421: Maximum XOR of Two Numbers in an Array
function findMaximumXOR(nums: number[]): number {
  const trie = new XorTrie();
  let result = 0;
  for (const num of nums) {
    trie.insert(num);
    result = Math.max(result, trie.maxXorWith(num));
  }
  return result;
}

console.log(findMaximumXOR([3, 10, 5, 25, 2, 8])); // 28(5 XOR 25 = 28)

C++ 對照

陣列實作 vs HashMap 實作

#include <bits/stdc++.h>
using namespace std;

// ===== 方法一:陣列 children(純小寫英文,記憶體固定,存取快)=====
struct TrieNodeArray {
    TrieNodeArray* children[26];
    bool isEnd;

    TrieNodeArray() : isEnd(false) {
        fill(begin(children), end(children), nullptr);
    }

    ~TrieNodeArray() {
        for (auto* child : children) delete child;
    }
};

class TrieArray {
    TrieNodeArray* root;
public:
    TrieArray() : root(new TrieNodeArray()) {}
    ~TrieArray() { delete root; }

    void insert(const string& word) {
        auto* node = root;
        for (char ch : word) {
            int idx = ch - 'a';
            if (!node->children[idx]) {
                node->children[idx] = new TrieNodeArray();
            }
            node = node->children[idx];
        }
        node->isEnd = true;
    }

    bool search(const string& word) const {
        auto* node = traverse(word);
        return node && node->isEnd;
    }

    bool startsWith(const string& prefix) const {
        return traverse(prefix) != nullptr;
    }

private:
    TrieNodeArray* traverse(const string& str) const {
        auto* node = root;
        for (char ch : str) {
            int idx = ch - 'a';
            if (!node->children[idx]) return nullptr;
            node = node->children[idx];
        }
        return node;
    }
};

// ===== 方法二:HashMap children(支援任意字元集,靈活但常數較大)=====
struct TrieNodeMap {
    unordered_map<char, TrieNodeMap*> children;
    bool isEnd = false;

    ~TrieNodeMap() {
        for (auto& [ch, child] : children) delete child;
    }
};

class TrieMap {
    TrieNodeMap* root;
public:
    TrieMap() : root(new TrieNodeMap()) {}
    ~TrieMap() { delete root; }

    void insert(const string& word) {
        auto* node = root;
        for (char ch : word) {
            if (!node->children.count(ch)) {
                node->children[ch] = new TrieNodeMap();
            }
            node = node->children[ch];
        }
        node->isEnd = true;
    }

    bool search(const string& word) const {
        auto* node = traverse(word);
        return node && node->isEnd;
    }

    bool startsWith(const string& prefix) const {
        return traverse(prefix) != nullptr;
    }

    // 支援萬用字元 '.' 的搜尋(對應 LeetCode 211)
    bool searchWithDot(const string& word) const {
        return searchHelper(root, word, 0);
    }

private:
    TrieNodeMap* traverse(const string& str) const {
        auto* node = root;
        for (char ch : str) {
            auto it = node->children.find(ch);
            if (it == node->children.end()) return nullptr;
            node = it->second;
        }
        return node;
    }

    bool searchHelper(TrieNodeMap* node, const string& word, int idx) const {
        if (!node) return false;
        if (idx == (int)word.size()) return node->isEnd;
        char ch = word[idx];
        if (ch == '.') {
            // 萬用字元:嘗試所有子節點
            for (auto& [c, child] : node->children) {
                if (searchHelper(child, word, idx + 1)) return true;
            }
            return false;
        }
        auto it = node->children.find(ch);
        if (it == node->children.end()) return false;
        return searchHelper(it->second, word, idx + 1);
    }
};

// ===== 兩種實作的對照 =====
// 特性                陣列實作              HashMap 實作
// 字元範圍            固定(如小寫 26)      任意字元集
// 每節點記憶體        固定 26 × pointer     動態,按實際子節點
// 子節點存取          O(1) 直接索引         O(1) 平均,但有常數開銷
// 快取友好性          較好(連續陣列)        較差(散亂指針)
// 適合場景            競賽、純英文字串        生產環境、Unicode 支援

複雜度分析

操作時間複雜度空間複雜度說明
insertO(L)O(L × Σ)L = 字串長,Σ = 字母表大小
searchO(L)O(1)只讀,不額外分配空間
startsWithO(L)O(1)前綴匹配,不需要 isEnd
deleteO(L)O(1)需後序遞迴清理孤立節點
自動補全列舉O(P + R)O(R)P = 前綴長,R = 結果總字元數
XOR 最大值查詢O(W)O(N × W)W = 位元寬,N = 整數個數

空間使用比較:

  • HashMap children:靈活,適合稀疏字母表(Unicode),但有額外 HashMap 開銷。
  • 陣列 children(大小 26):每節點固定 26 個指針,適合純小寫英文字母,存取 O(1) 且快取友好。

Trie vs HashMap:HashMap 的精確查詢平均 O(1),但不支援前綴搜尋。Trie 的所有操作均為 O(L)(L 為字串長),且天然支援前綴匹配、自動補全、字典序遍歷。需要前綴相關操作就選 Trie。


變體與延伸

壓縮 Trie(Radix Tree / Patricia Trie)

標準 Trie 中,若某條路徑只有單一子節點(且不是字串結尾),會形成長鏈浪費節點。壓縮 Trie(Radix Tree) 將這些單鏈路徑合併為邊上的字串標籤:

標準 Trie(插入 "interview", "internal", "integer")

root→i→n→t→e→r→v→i→e→w*  (interview)
               |
               r→n→a→l*    (internal)
          |
          e→g→e→r*          (integer)

壓縮 Trie(Radix Tree)

root→"int"→"e"→"r"→"view"*  (interview)
                  |
                  "rnal"*     (internal)
            |
            "eger"*           (integer)

優點:節點數從 O(總字元數) 降至 O(字串數)

Patricia Trie 是壓縮 Trie 的一種實作,每個節點儲存一個整數索引(bit position),用於指示下次分支判斷應看哪一位。IP 路由表的最長前綴匹配(Longest Prefix Match)通常用這種結構。

XOR Trie(最大 XOR 對)

XOR Trie 用於在整數集合中尋找最大 XOR 值的數對。每個整數以 32 位元二進制從最高位到最低位插入 Trie,查詢時貪婪選擇相反位元:

插入 3 (011), 5 (101), 10 (1010), 14 (1110) 到 XOR Trie(4-bit)

查詢 q=14 (1110) 的最大 XOR 對:
  bit3: q=1,想選 0 → 有 0 子節點 → 走 0,XOR 貢獻 1
  bit2: q=1,想選 0 → 有 0 子節點 → 走 0,XOR 貢獻 1
  bit1: q=1,想選 0 → 有 0 子節點 → 走 0,XOR 貢獻 1
  bit0: q=0,想選 1 → 有 1 子節點 → 走 1,XOR 貢獻 1
  結果:14 XOR 1 = 15(最大 XOR)

Suffix Trie / Suffix Array

對字串所有後綴建立 Trie(Suffix Trie),可在 O(P) 時間查詢任意子字串是否存在。壓縮版本(Suffix Array + LCP Array)空間效率更高,廣泛應用於 DNA 序列比對、全文搜尋引擎。

Ternary Search Tree(TST)

TST 是 Trie 與二元搜尋樹的結合:每節點有三個子節點(左、中、右),代表當前字元「小於」、「等於」、「大於」三種情況。記憶體比固定陣列 Trie 節省,但搜尋複雜度退化為 O(L log Σ)。


面試考點

常見陷阱一:忘記設置 isEnd

insert 完成後,若未將最後節點的 isEnd 設為 truesearch 永遠返回 false

// 錯誤:忘記 isEnd
function insertWrong(root: TrieNode, word: string): void {
  let node = root;
  for (const ch of word) {
    if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
    node = node.children.get(ch)!;
  }
  // 忘記這行!
  // node.isEnd = true;
}

// 此後 search("word") 永遠返回 false

常見陷阱二:search 與 startsWith 混淆

search("app") 需要 isEnd === true(“app” 必須是完整單詞)。

startsWith("app") 只要節點存在即可,不需要 isEnd

常見錯誤是在 startsWith 中也加上 isEnd 判斷,導致前綴查詢失效。

// 錯誤的 startsWith
startsWith_WRONG(prefix: string): boolean {
  const node = this._traverse(prefix);
  return node !== null && node.isEnd; // 多了 && node.isEnd!
  // 如果 "app" 不是字串結尾,startsWith("ap") 就會錯誤返回 false
}

常見陷阱三:XOR Trie 的位元順序

必須從最高位元向最低位元插入,否則貪婪策略從低位決策,無法最大化高位貢獻。

// 錯誤:從低位開始
for (let i = 0; i <= this.BITS; i++) { ... } // 錯誤

// 正確:從高位開始
for (let i = this.BITS; i >= 0; i--) { ... } // 正確

常見陷阱四:Word Search II 的重複結果

同一個字串在 board 中可能有多條路徑匹配,需在找到後立即清除 Trie 中的 isEnd 標記,防止重複加入結果列表。


LeetCode 練習

LeetCode 208 — Implement Trie(Medium)

直接使用上方 Trie 類別即可,重點在 search 必須檢查 isEndstartsWith 不需要。

class Trie208 {
  private root: TrieNode;
  constructor() { this.root = new TrieNode(); }

  insert(word: string): void {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch)!;
    }
    node.isEnd = true;
  }

  search(word: string): boolean {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) return false;
      node = node.children.get(ch)!;
    }
    return node.isEnd; // 必須是完整單詞
  }

  startsWith(prefix: string): boolean {
    let node = this.root;
    for (const ch of prefix) {
      if (!node.children.has(ch)) return false;
      node = node.children.get(ch)!;
    }
    return true; // 節點存在即可
  }
}
// 時間 O(L) per operation,空間 O(N × L)

LeetCode 211 — Design Add and Search Words(Medium)

支援萬用字元 '.' 的 Trie 搜尋,'.' 可以匹配任意單個字母,需要遞迴 DFS 嘗試所有子節點:

class WordDictionary {
  private root: TrieNode;
  constructor() { this.root = new TrieNode(); }

  addWord(word: string): void {
    let node = this.root;
    for (const ch of word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch)!;
    }
    node.isEnd = true;
  }

  search(word: string): boolean {
    return this._dfs(this.root, word, 0);
  }

  private _dfs(node: TrieNode, word: string, idx: number): boolean {
    if (idx === word.length) return node.isEnd;
    const ch = word[idx];
    if (ch === '.') {
      // 萬用字元:嘗試所有子節點
      for (const child of node.children.values()) {
        if (this._dfs(child, word, idx + 1)) return true;
      }
      return false;
    }
    if (!node.children.has(ch)) return false;
    return this._dfs(node.children.get(ch)!, word, idx + 1);
  }
}
// 時間 O(L) 一般情況,最壞 O(Σ^L)(全萬用字元)

LeetCode 212 — Word Search II(Hard)

將所有 words 建入 Trie,在 board 上 DFS 的同時沿 Trie 走——遇到 isEnd 即收錄,並立刻清除避免重複:

function findWords(board: string[][], words: string[]): string[] {
  // 建立 Trie
  const root = new TrieNode();
  for (const word of words) {
    let node = root;
    for (const ch of word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch)!;
    }
    node.isEnd = true;
    (node as any).word = word; // 在葉節點儲存完整字串,避免回溯重建
  }

  const rows = board.length, cols = board[0].length;
  const result: string[] = [];

  function dfs(r: number, c: number, node: TrieNode): void {
    if (r < 0 || r >= rows || c < 0 || c >= cols) return;
    const ch = board[r][c];
    if (ch === '#' || !node.children.has(ch)) return; // '#' 表示已訪問

    const nextNode = node.children.get(ch)!;
    if (nextNode.isEnd) {
      result.push((nextNode as any).word);
      nextNode.isEnd = false; // 防止重複添加(關鍵!)
    }

    board[r][c] = '#'; // 標記已訪問
    dfs(r + 1, c, nextNode);
    dfs(r - 1, c, nextNode);
    dfs(r, c + 1, nextNode);
    dfs(r, c - 1, nextNode);
    board[r][c] = ch; // 回溯

    // 剪枝:若此節點已無子節點且非結尾,從父節點移除加速後續搜尋
    if (nextNode.children.size === 0 && !nextNode.isEnd) {
      node.children.delete(ch);
    }
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      dfs(r, c, root);
    }
  }

  return result;
}
// 時間 O(M × 4 × 3^(L-1)),M = board 格子數,L = 最長字串長
// 空間 O(N × L) for Trie,N = words 數,L = 平均長度

LeetCode 421 — Maximum XOR of Two Numbers in an Array(Medium)

XOR Trie 貪婪策略,從最高位到最低位逐步選擇與當前位相反的子節點:

function findMaximumXOR(nums: number[]): number {
  const trie = new XorTrie(); // 使用上方 XorTrie 實作
  let result = 0;

  // 先全部插入,再全部查詢(a XOR b = b XOR a,順序不影響結果)
  for (const num of nums) trie.insert(num);
  for (const num of nums) {
    result = Math.max(result, trie.maxXorWith(num));
  }

  return result;
}
// 時間 O(N × 32),空間 O(N × 32)

// 替代解法:位元遮罩(Bit Masking)+ HashSet,時空複雜度相同但無需 Trie 類別
function findMaximumXOR_bitMask(nums: number[]): number {
  let maxXor = 0, mask = 0;

  for (let i = 31; i >= 0; i--) {
    mask |= (1 << i);
    const prefixes = new Set(nums.map(n => n & mask));

    const candidate = maxXor | (1 << i);
    let found = false;
    for (const prefix of prefixes) {
      if (prefixes.has(candidate ^ prefix)) { found = true; break; }
    }
    if (found) maxXor = candidate;
  }

  return maxXor;
}

LeetCode 648 — Replace Words(Medium)

字典中有詞根(root),將句子中所有能被詞根替換的詞替換為最短詞根:

function replaceWords(dictionary: string[], sentence: string): string {
  // 建立詞根 Trie
  const root = new TrieNode();
  for (const root_word of dictionary) {
    let node = root;
    for (const ch of root_word) {
      if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
      node = node.children.get(ch)!;
    }
    node.isEnd = true;
    (node as any).word = root_word;
  }

  return sentence.split(' ').map(word => {
    let node = root;
    for (const ch of word) {
      if (!node.children.has(ch)) break;
      node = node.children.get(ch)!;
      if (node.isEnd) return (node as any).word; // 找到最短詞根,立即替換
    }
    return word; // 無詞根匹配,保留原詞
  }).join(' ');
}
// 範例:dictionary=["cat","bat","rat"], sentence="the cattle was rattled by the battery"
// 輸出:"the cat was rat by the bat"
// 時間 O(N × L),N = 詞數,L = 詞長

總結

字典樹 Trie 是「以空間換時間」的典型代表——用多叉樹的結構共享前綴,讓所有字串操作只需 O(L) 時間,且天然支援前綴搜尋、自動補全等 HashMap 做不到的功能。回顧本文核心知識點:

  1. 節點結構:每個節點只需 children(子節點映射)和 isEnd(是否是字串結尾),字元隱含在邊上,不存在節點本身。

  2. 三大操作insert 逐字元新建節點,search 遍歷後檢查 isEndstartsWith 遍歷後只要節點存在即返回 true。這三者的時間複雜度均為 O(L)。

  3. 實作選擇:純小寫英文用陣列(26 個槽位,存取快、快取友好);需要 Unicode 或動態字元集用 Map(靈活但有 HashMap 開銷)。

  4. 進階變體:壓縮 Trie(Radix Tree)節省記憶體;XOR Trie 解決最大 XOR 對問題;Suffix Trie 支援子字串快速查詢。

  5. 四大陷阱:忘記設 isEnd、混淆 searchstartsWith、XOR Trie 位元方向錯誤、Word Search II 重複收錄同一單詞。

掌握 Trie,你就掌握了自動補全、拼字檢查、IP 路由等無數系統設計題的核心元件。希望這篇文章能幫助你建立清晰的前綴樹思維,在面試中自信地應對各種字串題。如果有任何問題或疑惑,歡迎至 聯絡頁面 留言交流。

下一篇預告:我們將進入 併查集(Union-Find) 的世界,探討如何用近乎 O(1) 的效率解決動態連通性問題,以及在圖論中的廣泛應用。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。