Trie 实现搜索自动补全与敏感词过滤

本节目标

Trie(前缀树):每条边是一个字符,从根到某节点的路径构成一个前缀。适合"大量字符串的前缀匹配"。

// 运行环境:Node.js 14+
// 保存为 dsa-l23.js,执行:node dsa-l23.js

class TrieNode {
  constructor() { this.children = {}; this.isEnd = false }
}
class Trie {
  constructor() { this.root = new TrieNode() }
  insert(word) {                         // 逐字符下钻,末尾标 end
    let node = this.root
    for (const ch of word) {
      if (!node.children[ch]) node.children[ch] = new TrieNode()
      node = node.children[ch]
    }
    node.isEnd = true
  }
  startsWith(prefix) {                   // 前缀是否存在
    let node = this.root
    for (const ch of prefix) {
      if (!node.children[ch]) return false
      node = node.children[ch]
    }
    return true
  }
  collect(node, prefix, out) {           // 收集以 prefix 开头的所有词(自动补全)
    if (node.isEnd) out.push(prefix)
    for (const ch in node.children) this.collect(node.children[ch], prefix + ch, out)
  }
  autoComplete(prefix) {
    let node = this.root
    for (const ch of prefix) {
      if (!node.children[ch]) return []   // 前缀不存在
      node = node.children[ch]
    }
    const out = []
    this.collect(node, prefix, out)
    return out
  }
}

// 调用示例:搜索框补全
const t = new Trie()
;['app', 'apple', 'apply', 'apt', 'banana'].forEach(w => t.insert(w))
console.log(t.autoComplete('ap'))   // ['app','apple','apply','apt']
console.log(t.startsWith('ban'))    // true

// 敏感词过滤:遍历文本,Trie 匹配到完整词则打码
function filterSensitive(text, trie) {
  let result = '', i = 0
  while (i < text.length) {
    let node = trie.root, j = i, matched = ''
    while (j < text.length && node.children[text[j]]) {
      node = node.children[text[j]]; matched += text[j]; j++
      if (node.isEnd) break            // 命中完整敏感词
    }
    if (node.isEnd) { result += '***'; i = j } // 替换为 ***
    else { result += text[i]; i++ }
  }
  return result
}
const bad = new Trie(); ['垃圾', '广告'].forEach(w => bad.insert(w))
console.log(filterSensitive('这是垃圾信息含广告', bad)) // '这是***信息含***'

名词解释

课后练习

  1. 为什么自动补全用 Trie 而不是每次 filter 全词典?
    • 答案:Trie 沿前缀 O(词长) 直达候选子树,只遍历相关词;filter 要扫描全部词、且每词做前缀判断,词典大时慢得多。
  2. 敏感词过滤里 while 内 if(node.isEnd) break 有什么用?
    • 答案:命中较短敏感词(如"广告")就立即停止延伸,避免被更长词前缀干扰,且保证最短匹配优先替换;若无此 break,会一直走到最长,可能漏掉中间短词。

总结

Trie 是"用空间换查询效率"的又一经典:它把一堆字符串按前缀铺成一棵树,使得"是否以某前缀开头""收集所有前缀匹配"都变成沿树的线性游走,复杂度只和词长有关、和词典总量无关。这正是搜索框自动补全、输入法联想、路由前缀匹配背后的结构。我特别想点出它和哈希表的分工:哈希擅长"完整键"的 O(1) 查找,却对"前缀""通配"无能为力;Trie 恰恰补上这块短板。敏感词过滤的例子则展示了 Trie 的"多模式匹配"威力——一个 Trie 同时装下所有词,扫一遍文本就能全数命中,远比"对每个词调用 includes"高效。学 Trie 的关键不是背节点结构,而是建立"前缀共享"的直觉:大量字符串有公共前缀时,Trie 能把重复部分合并存储,省空间又省时间。