使用通配符进行单词查找的高效数据结构

时间:2010-05-11 23:12:56

标签: performance algorithm data-structures memory-management

我需要将一系列用户输入的单词与大型单词词典进行匹配(以确保输入的值存在)。

因此,如果用户输入:

"orange" it should match an entry "orange' in the dictionary.

现在的问题是,用户还可以输入通配符或一系列通配符,例如“

"or__ge" which would also match "orange"

关键要求是:

* this should be as fast as possible.

* use the smallest amount of memory to achieve it.  

如果单词列表的大小很小,我可以使用包含所有单词的字符串并使用正则表达式。

然而,鉴于单词列表可能包含数十万个企业,我认为这不会起作用。

因此某种“树”是这样的方式......?

对此的任何想法或建议都将完全赞赏!

提前致谢, 马特

6 个答案:

答案 0 :(得分:16)

将您的单词列表放在DAWG(有向无环字图)中,如Appel and Jacobsen's paper on the World's Fastest Scrabble Program(哥伦比亚的free copy)所述。对于你的搜索,你将遍历这个图形,保持一组指针:在一封信上,你向那个字母的孩子做出确定性的过渡;在通配符上,将所有子项添加到集合中。

效率与Thompson对grep的NFA解释大致相同(它们是相同的算法)。 DAWG结构非常节省空间 - 远远超过仅存储单词本身。它很容易实现。

最坏情况下的成本将是字母表(26?)的大小提升到通配符数量的幂。但除非您的查询通配符开头,否则简单的从左到右搜索在实践中效果很好。我建议禁止查询以太多通配符开头,否则创建多个dawgs,例如,dawg用于镜像,dawg用于旋转左三个字符,依此类推。

匹配任意一系列通配符,例如______总是很昂贵,因为有很多组合的解决方案。 dawg将很快列举所有解决方案。

答案 1 :(得分:4)

无论您选择哪种算法,都需要在速度和内存消耗之间进行权衡。

如果你能负担~O(N * L)内存(其中N是你字典的大小而L是一个单词的平均长度),你可以试试这个非常快的算法。为简单起见,假设拉丁字母表有26个字母,MAX_LEN表示单词的最大长度。

创建一组整数集合set<int> table[26][MAX_LEN].

对于词典中的每个单词,将单词index添加到与单词的每个字母对应的位置中的集合。例如,如果“orange”是字典中的第12345个单词,则将12345添加到与[o] [0],[r] [1],[a] [2],[n] [ 3],[g] [4],[e] [5]。

然后,要检索对应于“or..ge”的单词,可以在[o] [0],[r] [1],[g] [4],[e] [找到集合的交集。 5]。

答案 2 :(得分:2)

我首先测试正则表达式解决方案并查看它是否足够快 - 您可能会感到惊讶! : - )

但是,如果这还不够好,我可能会使用前缀树。

基本结构是树,其中:

  • 顶层的节点都是可能的第一个字母(即假设您使用完整的字典,可能是a-z的26个节点)。
  • 下一个级别包含每个给定首字母的所有可能的第二个字母
  • 依此类推,直至找到每个单词的“词尾”标记

测试字典中是否包含带有通配符的给定字符串只是一个简单的递归算法,您可以对每个字符位置进行直接匹配,或者在通配符的情况下检查每个可能的分支。< / p>

在最坏的情况下(所有通配符,但只有一个字在字典的末尾有正确的字母数),你会遍历整个树,但这仍然只是字典大小的O(n)所以不比完整的正则表达式扫描差。在大多数情况下,找到匹配或确认不存在这样的匹配只需要很少的操作,因为搜索树的大分支被每个连续的字母“修剪”。

答案 3 :(得分:1)

您可以尝试字符串矩阵:

0,1: A
1,5: APPLE
2,5: AXELS
3,5: EAGLE
4,5: HELLO
5,5: WORLD
6,6: ORANGE
7,8: LONGWORD
8,13:SUPERLONGWORD

让我们称之为一个参差不齐的索引矩阵,以节省一些内存。按长度订购,然后按字母顺序订购。为了解决角色,我使用符号x,y:zx是索引,y是条目的长度,z是位置。字符串的长度为fg是字典中的条目数。

  • 创建列表m,其中包含潜在匹配索引x
  • 从{0}到z迭代f
    • 是否为通配符且搜索字符串的最新字符?
      • 继续循环(全部匹配)。
    • m是空的吗?
      • 搜索所有x从0到g的{​​{1}}匹配长度。 !!一个!!
        • y字符是否与z处的搜索字符串匹配?保存z中的x
      • m是空的吗?中断循环(不匹配)。
    • m不是空的吗?
      • 搜索m的所有元素。 !!乙!!
        • 与搜索匹配吗?从m
        • 中删除
      • m是空的吗?中断循环(不匹配)。

通配符将始终通过“与搜索字符串匹配?”。并且m与矩阵一样有序。

!!一个!!:Binary search关于搜索字符串的长度。 m
!! B !!:按字母顺序排列的二进制搜索。 O(log n)

使用字符串矩阵的原因是你已经存储了每个字符串的长度(因为它使搜索更快),但它也给你每个条目的长度(假设其他常量字段),这样你可以轻松找到矩阵中的下一个条目,以便快速迭代。对矩阵进行排序不是问题:因为这只是在字典更新后才进行,而不是在搜索时进行。

答案 4 :(得分:0)

如果你被允许忽略我认为的情况,那么在你的字典和所有搜索条件之前的所有单词都是相同的情况。大小写没什么区别。如果您有一些区分大小写的单词而其他单词不区分大小写,请将单词分成两组并分别搜索。

您只是匹配单词,因此您可以将字典分解为字符串数组。由于您只对已知长度进行精确匹配,因此将字数组拆分为每个字长的单独数组。所以byLength [3]是长度为3的所有单词的数组。每个单词数组都应该排序。

现在你有一系列单词和一个带有潜在外卡的单词可供查找。取决于更好的和通配符的位置,有几种方法。

如果搜索字词没有通配符,则在已排序的数组中执行二进制搜索。你可以在这一点上做一个哈希,这会更快但不多。如果绝大多数搜索词都没有通配符,那么请考虑哈希表或哈希键控的关联数组。

如果搜索词在某些文字字符后面有通配符,则在已排序的数组中进行二进制搜索以找到上限和下限,然后在该边界中进行线性搜索。如果通配符都在尾随,那么找到非空范围就足够了。

如果搜索词以通配符开头,则排序后的数组没有帮助,除非您保留按向后字符串排序的数组副本,否则您需要进行线性搜索。如果你创建了这样一个数组,那么只要有超过前导文字的结尾,就选择它。如果您不允许使用前导通配符,则无需使用。

如果搜索词的开头和结尾都是通配符,那么你就会在等长的单词中进行线性搜索。

所以是字符串数组的数组。每个字符串数组都已排序,并包含相等长度的字符串。可选地,对于前导通配符的情况,使用基于向后字符串的排序复制整个结构。

整个空间是每个单词一两个指针加上单词。如果您的语言允许,您应该能够将所有单词存储在单个缓冲区中。当然,如果你的语言不允许,那么grep可能更快。对于一百万个单词,数组为4-16MB,实际单词类似。

对于没有通配符的搜索词,性能会非常好。使用通配符时,偶尔会对大量单词进行线性搜索。通过按长度细分和单个主角,即使在最坏的情况下,也不应该搜索超过字典总数的百分之几。只比较已知长度的整个单词总是比通用字符串匹配更快。

答案 5 :(得分:0)

如果字典将与查询序列匹配,请尝试构建Generalized Suffix Tree。有线性时间算法可用于构建这样的树(Ukkonen Suffix Tree Construction)。

您可以通过遍历根节点轻松匹配(它的O(k),其中k是查询的大小)每个查询,并使用通配符匹配任何字符,如后缀树中的典型模式查找。 / p>