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

Posted

技术标签:

【中文标题】使用通配符进行单词查找的高效数据结构【英文标题】:Efficient data structure for word lookup with wildcards 【发布时间】:2011-02-18 09:28:51 【问题描述】:

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

所以如果用户输入:

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

现在要注意的是,用户还可以输入通配符或一系列通配符,例如say

"or__ge" which would also match "orange"

关键要求是:

* this should be as fast as possible.

* use the smallest amount of memory to achieve it.  

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

但是鉴于单词列表可能包含潜在的数十万个词条,我认为这是行不通的。

那么,某种“树”是实现此目的的方法吗...?

对此的任何想法或建议将不胜感激!

提前致谢, 马特

【问题讨论】:

我不确定,但我认为后缀树可能是您正在寻找的 - en.wikipedia.org/wiki/Suffix_tree 您必须支持所有 grep 样式的通配符还是只支持 ? (下划线 _ 在你的情况下)? 通配符是只匹配单个字符还是可以匹配任意长度的字符串? 只是下划线,每个下划线代表一个字符。 【参考方案1】:

按照Appel and Jacobsen's paper on the World's Fastest Scrabble Program(哥伦比亚的free copy)中所述,将您的单词列表放入DAWG(有向无环单词图)中。对于您的搜索,您将遍历此图并维护一组指针:在一个字母上,您将确定性地转换为具有该字母的孩子;在通配符上,您将所有子项添加到集合中。

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

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

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

【讨论】:

由于我无法访问这些出版物,我想知道一件事:他们是否为每个不同的长度构建了一个 DAWG?我认为它可以大大加快搜索速度,因为在这种情况下,我们事先知道我们要搜索的单词有多少个字母。 @Matthieu:谷歌会给你这篇论文,但我还添加了一个(可能是临时的)链接。至于每个长度一个 DAWG,您可以这样做,但这是一个时空权衡。 DAWG 将非常有效地存储很长的单词列表,并进行大量共享。每个长度有一个 DAWG,您将失去该共享。至于加速,这是一个实验性问题,根据机器的缓存,实验结果可能会有所不同。 @Norman Ramsey 我一直在研究一个类似的问题(10 多年后!),我发现的两个很好的解决方案是将所有后缀长度的位设置为每个节点,或者每个长度都有一个 DAWG,但在不同长度上共享节点。两者都运行良好,但我最终选择了第二种解决方案(仅比单个 DAWG 大 30%,与我的实现相比)。 @NormanRamsey 对于某些问题,您可以通过为每个节点维护一个出现在该节点任何后缀中的所有字符的位集来进行大量修剪。【参考方案2】:

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

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

基本结构是一棵树,其中:

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

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

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

【讨论】:

【参考方案3】:

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

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

创建一个整数集的二维数组,set<int> table[26][MAX_LEN].

对于字典中的每个单词,将单词索引添加到与单词的每个字母对应的位置的集合中。例如,如果“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]。

【讨论】:

【参考方案4】:

你可以试试字符串矩阵:

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 到 f 迭代 z。 它是通配符并且不是搜索字符串的最新字符吗? 继续循环(全部匹配)。 m 是空的吗? 在从 0 到 g 的所有 x 中搜索匹配长度的 y。 !!一种!! z 字符是否与z 处的搜索字符串匹配?将x 保存到mm 是空的吗?中断循环(不匹配)。 m 不为空吗? 搜索m 的所有元素。 !!乙!! 是否与搜索匹配?从m 中删除。 m 是空的吗?中断循环(不匹配)。

通配符将始终传递“与搜索字符串匹配?”。并且m 的顺序与矩阵相同。

!!A!!: Binary search 搜索字符串的长度。 O(log n) !!B!!:按字母顺序进行二进制搜索。 O(log n)

使用字符串矩阵的原因是您已经存储了每个字符串的长度(因为它使搜索速度更快),但它也为您提供了每个条目的长度(假设其他常量字段),这样您可以很容易地找到矩阵中的下一个条目,以便快速迭代。对矩阵进行排序不是问题:因为这仅在字典更新时完成,而不是在搜索期间。

【讨论】:

【参考方案5】:

如果允许您忽略大小写(我假设),那么首先将字典中的所有单词和所有搜索词都设为相同的大小写。大写或小写没有区别。如果您有一些字词区分大小写而另一些字词不区分大小写,请将这些字词分成两组并分别搜索。

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

现在您可以找到一组单词和一个带有潜在通配符的单词。根据通配符的位置和位置,有几种方法。

如果搜索词没有通配符,则在排序后的数组中进行二分搜索。此时您可以进行哈希运算,这会更快但不会太多。如果您的绝大多数搜索词都没有通配符,那么请考虑使用哈希表或以哈希为键的关联数组。

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

如果搜索词以通配符开头,则排序后的数组没有帮助,您需要进行线性搜索,除非您保留一个按反向字符串排序的数组副本。如果你制作这样一个数组,那么只要尾随文字多于前导文字,就选择它。如果您不允许前导通配符,则没有必要。

如果搜索词既以通配符开头又以通配符结尾,那么您将陷入等长词内的线性搜索。

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

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

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

【讨论】:

“如果搜索词以通配符开头和结尾,那么您将陷入等长词内的线性搜索。”查看我的答案:只有当通配符不是搜索字符串中的最新时,我才跳过通配符(在完全通配符搜索的情况下,这是线性的),这迫使它使用二进制搜索,无论它是否是通配符.【参考方案6】:

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

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

【讨论】:

以上是关于使用通配符进行单词查找的高效数据结构的主要内容,如果未能解决你的问题,请参考以下文章

通配符百分号表示啥意思

通配符百分号表示啥意思 通配符百分号指啥

程序员如何在 HTTPS 中高效配置通配符证书?| 技术头条

Word 查找和替换的通配符

(21)Powershell中的通配符

让通配符在 Microsoft Word 的 VBA 宏中的查找和替换功能中起作用