Trie 实现搜索自动补全与敏感词过滤
本节目标
- 理解 Trie(前缀树)的结构与用途
- 手写 Trie 的插入 / 查找 / 前缀匹配
- 用 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)) // '这是***信息含***'名词解释
- Trie(前缀树 / 字典树):按字符分层组织的树,根到节点路径 = 一个前缀。插入/查找/前缀匹配都是 O(词长),与词典大小无关。
- 自动补全(Autocomplete):输入前缀后列出所有以它开头的候选词。Trie 沿前缀走到节点,再 DFS 收集子树所有
isEnd词即可。 - 敏感词过滤:把敏感词建成 Trie,扫描文本时沿 Trie 走,命中
isEnd即整词匹配,适合多模式串同时匹配(比逐个 includes 快)。
课后练习
- 为什么自动补全用 Trie 而不是每次
filter全词典?- 答案:Trie 沿前缀 O(词长) 直达候选子树,只遍历相关词;
filter要扫描全部词、且每词做前缀判断,词典大时慢得多。
- 答案:Trie 沿前缀 O(词长) 直达候选子树,只遍历相关词;
- 敏感词过滤里
while内if(node.isEnd) break有什么用?- 答案:命中较短敏感词(如"广告")就立即停止延伸,避免被更长词前缀干扰,且保证最短匹配优先替换;若无此 break,会一直走到最长,可能漏掉中间短词。
总结
Trie 是"用空间换查询效率"的又一经典:它把一堆字符串按前缀铺成一棵树,使得"是否以某前缀开头""收集所有前缀匹配"都变成沿树的线性游走,复杂度只和词长有关、和词典总量无关。这正是搜索框自动补全、输入法联想、路由前缀匹配背后的结构。我特别想点出它和哈希表的分工:哈希擅长"完整键"的 O(1) 查找,却对"前缀""通配"无能为力;Trie 恰恰补上这块短板。敏感词过滤的例子则展示了 Trie 的"多模式匹配"威力——一个 Trie 同时装下所有词,扫一遍文本就能全数命中,远比"对每个词调用 includes"高效。学 Trie 的关键不是背节点结构,而是建立"前缀共享"的直觉:大量字符串有公共前缀时,Trie 能把重复部分合并存储,省空间又省时间。