You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Trie前缀树实现子串包含、乱序匹配的单词查询功能

多语言适配Trie实现与扩展查询方案

以下是一款可适配任意语言字母/文字系统的Trie实现:

class TrieNode {
  constructor(key) {
    // 节点存储的当前序列字符
    this.key = key;
    // 父节点引用
    this.parent = null;
    // 子节点哈希表
    this.children = {};
    // 标记当前节点是否为单词终点
    this.end = false;
  }

  getWord() {
    let output = [];
    let node = this;

    while (node !== null) {
      output.unshift(node.key)
      node = node.parent
    }

    return output.join('')
  }
}

class Trie {
  constructor() {
    this.base = new TrieNode(null)
  }

  insert(word) {
    let node = this.base

    const points = Array.from(word)

    for (const i in points) {
      const point = points[i]
      if (!node.children[point]) {
        const child = node.children[point] = new TrieNode(point)
        child.parent = node
      }

      node = node.children[point]

      if (i == word.length - 1) {
        node.end = true
      }
    }
  }

  contains(word) {
    let node = this.base

    const points = Array.from(word)

    for (const i in points) {
      const point = points[i]

      if (node.children[point]) {
        node = node.children[point]
      } else {
        return false
      }
    }

    return node.end;
  }

  find(prefix) {
    let node = this.base
    let output = []

    const points = Array.from(prefix)

    for (const i in points) {
      const point = points[i]

      // 校验前缀是否存在对应词条
      if (node.children[point]) {
        node = node.children[point]
      } else {
        // 无匹配直接返回空结果
        return output
      }
    }

    const stack = [node]
    while (stack.length) {
      node = stack.shift()
      // 遍历到单词终点,加入结果集
      if (node.end) {
        output.unshift(node.getWord())
      }

      // 遍历所有子节点递归收集词条
      for (var child in node.children) {
        stack.push(node.children[child])
      }
    }

    return output
  }
}

目前这套Trie结构已经可以高效实现前缀(开头位置)匹配,基于Scrabble词表的测试代码与运行效果如下:

const fs = require('fs')
const Trie = require('./Trie')

// 读取词表文件
const words = fs.readFileSync('tmp/scrabble.csv', 'utf-8')
  .trim()
  .split(/\n+/)
  .map(x => x.trim())

const trie = new Trie()
// 所有词条插入Trie
words.forEach(word => trie.insert(word))

// 查询前缀为"zy"的所有单词
console.log(trie.find('zy'))
// 返回结果:
[
  'zygodactylous', 'zygomorphies', 'zygapophysis',
  'zygapophyses',  'zygomorphic',  'zymologies',
  'zygospores',    'zygosities',   'zygomorphy',
  'zygodactyl',    'zymurgies',    'zymograms',
  'zymogenes',     'zygotenes',    'zygospore',
  'zygomatic',     'zyzzyvas',     'zymosans',
  'zymology',      'zymogram',     'zymogens',
  'zymogene',      'zygotene',     'zygosity',
  'zygomata',      'zyzzyva',      'zymurgy',
  'zymotic',       'zymosis',      'zymoses',
  'zymosan',       'zymogen',      'zymases',
  'zygotic',       'zygotes',      'zygosis',
  'zygoses',       'zygomas',      'zydecos',
  'zymase',        'zygote',       'zygose',
  'zygoma',        'zygoid',       'zydeco',
  'zymes',         'zyme'
]

测试使用的scrabble.csv为公开可用的官方拼字游戏词典。

标准前缀Trie仅对前缀匹配有最优性能,针对「任意位置子串匹配」「乱序字母组词」两类需求,可以基于Trie做改造实现,但存在明确的效率瓶颈,也有更适配场景的替代方案,具体说明如下:

一、包含指定子串匹配

最初设想的「枚举所有可能子串构建Trie」方案逻辑可行,但内存开销为O(n²)量级:对平均长度为5的100万单词,总子串数量可达1500万条,内存压力极大,不适合生产环境大规模使用。

基于Trie的实现思路

可以将全子串Trie优化为后缀Trie(后缀树):插入单词时,仅将单词的所有后缀插入Trie,每个节点额外挂载该后缀对应的原单词ID列表。查询时从根节点逐字符遍历输入子串,走到对应节点后,直接取出节点上挂载的所有原单词即可。该方案内存开销比全子串Trie低一个量级,但仍远高于纯前缀Trie,仅适合10万词条以下的中小规模词表。

效率瓶颈与替代方案

后缀Trie的内存开销会随单词平均长度增长快速升高,且构建速度慢。如果追求百万级词条下的高性能查询,更适配的方案为:

  • 后缀数组+二分查找:将所有单词的所有后缀排序构建数组,查询子串时通过二分定位匹配区间,内存占用仅为后缀Trie的1/5~1/3,查询速度可达毫秒级
  • Aho-Corasick自动机:适合批量多模式子串匹配场景,一次扫描即可返回所有命中的子串结果

二、乱序字母组词匹配

之前尝试的「单词字母排序后存入Trie,查询时排序输入串做包含匹配」的方案存在逻辑漏洞是必然的:排序后的字符串做子串匹配只能覆盖排序后字符连续的场景,无法支持字母重复、子集选取的匹配规则,比如输入caldku匹配duck的场景就无法覆盖。

基于Trie的实现思路

可以构建定序无重复Trie实现该需求:

  • 插入阶段:先将单词按固定规则(如英文字母a-z顺序)排序,插入Trie时要求子节点字符顺序必须大于等于父节点,避免重复路径,每个终止节点挂载对应的原单词
  • 查询阶段:将输入串按相同规则排序,在Trie上做回溯遍历,遍历过程中统计已选字符的计数,只要不超过输入串对应字符的总计数就可以继续深入,每遇到终止节点就将对应单词加入结果集
    该方案逻辑可行,但查询过程需要回溯,性能和输入串长度强相关,输入串长度超过10时性能会明显下降。

效率瓶颈与替代方案

Trie的有序前缀遍历特性本质上和乱序匹配的需求不匹配,回溯遍历的开销很高。更适配的方案为字母计数签名哈希索引:

  • 预处理阶段:为每个单词生成固定长度的计数签名,比如英文场景下生成长度为26的数组,每个位置存储对应字母在单词中的出现次数,将签名序列化为字符串作为哈希表的Key,Value为对应签名的所有单词列表;额外按单词长度、是否包含高频字母等维度做分桶索引,减少查询时的比对量
  • 查询阶段:先统计输入串的各字母计数,遍历所有长度不超过输入串的分桶,比对桶内单词的签名,只要单词每个字母的计数都小于等于输入串对应字母的计数,就将该单词加入结果集
    该方案在百万级词表下的查询速度比改造Trie快一个量级以上,内存开销也更低,是目前拼字游戏、乱序组词类场景的主流实现方案。

内容的提问来源于stack exchange,提问作者Lance Pollard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:45:56