【问题标题】:Good algorithm and data structure for looking up words with missing letters?用于查找缺少字母的单词的良好算法和数据结构?
【发布时间】:2010-12-29 12:18:36
【问题描述】:

所以我需要编写一个有效的算法来查找字典中缺少字母的单词,并且我想要一组可能的单词。

例如,如果我有 th??e,我可能会取回这些,那些,主题,there.etc。

我想知道是否有人可以建议一些我应该使用的数据结构或算法。

谢谢!

编辑:Trie 空间效率太低,而且速度太慢。还有其他想法修改吗?

更新:最多有两个问号,当两个问号出现时,它们将按顺序出现。

目前我使用 3 个哈希表来表示完全匹配、1 个问号和 2 个问号。 给定一本字典,我散列所有可能的单词。例如,如果我有单词 WORD。我散列 WORD、?ORD、W?RD、WO?D、WOR?、??RD、W??D、WO??。进字典。然后我使用链接列表将冲突链接在一起。所以说 hash(W?RD) = hash(STR?NG) = 17. hashtab(17) 将指向 WORD 而 WORD 指向 STRING 因为它是一个链表。

平均查找一个词的时间约为2e-6s。我希望做得更好,最好是 1e-9 的顺序。

编辑:我没有再次查看这个问题,但插入 3m 条目需要 0.5 秒,查找 3m 条目需要 4 秒。

谢谢!

【问题讨论】:

  • 为什么不将这些转换成正则表达式并进行搜索?你希望什么?你有什么期望?你有什么限制?
  • 正则表达式的速度有多快?我知道它们是什么,但我不知道它们实际上是如何工作的。我可以遍历整个字典,但那将是 Theta(N)。我想知道我是否可以做得更好。
  • @Danny:更新问题。请不要评论您拥有的问题。你拥有这个问题。您可以更新它以包含所有信息。请更新问题。
  • 听起来您添加到问题中的解决方案等同于 Anna 的第一个建议(哈希),只是您可能会遇到不必要的冲突。如果您只是切换到她的建议,您将使用大约相同数量的内存(即很多),但您不必每次都检查整个哈希桶是否有冲突,这应该会让您更快。
  • 1e-9 是一纳秒 - 这与普通 PC 添加两个数字所需的时间差不多。你的算法没有问题,你需要的是一台超级计算机。

标签: algorithm data-structures


【解决方案1】:

您可以在aspell 中查看它是如何完成的。它会针对拼写错误的单词提示正确单词的建议。

【讨论】:

【解决方案2】:

如果你生成所有可能匹配模式的单词(arate、arbte、arcte ... zryte、zrzte),然后在字典的二叉树表示中查找它们,这将具有 O(e^N1 * log(N2)) 其中 N1 是问号的数量,N2 是字典的大小。对我来说似乎已经足够好了,但我相信有可能找出更好的算法。

编辑:如果您要说的不仅仅是三个问号,请查看 Phil H 的答案和他的字母索引方法。

【讨论】:

  • 你做过这个之前你怎么知道它的O((N1^2)*log(N2))?
  • 你不应该一次全部生成它们,而是只在你需要它们时,从第一个开始?从左边。
  • 你能详细说明你的算法是什么吗?
  • Danny:二叉树 loup 通常为 O(log N),如果您使用英文字母表并且有两个问号,则您有 26^2 次查找的可能性,因此 e^ N,你查找每个单词,因此 O(e^N1 * log(N2))
  • Debilski:是的,这似乎是一个好主意,可以有效地将平均执行时间减半(当然不是完全减半)
【解决方案3】:

首先,我们需要一种将查询字符串与给定条目进行比较的方法。让我们假设一个使用正则表达式的函数:matches(query,trialstr)

一个 O(n) 算法将简单地遍历每个列表项(您的字典将在程序中表示为一个列表),将每个项与您的查询字符串进行比较。

通过一些预先计算,您可以通过为每个字母构建一个额外的单词列表来改进大量查询,因此您的字典可能如下所示:

wordsbyletter = { 'a' : ['aardvark', 'abacus', ... ],
                  'b' : ['bat', 'bar', ...],
                  .... }

但是,这将是有限的用途,特别是如果您的查询字符串以未知字符开头。因此,我们可以通过注意给定单词中特定字母所在的位置来做得更好,生成:

wordsmap = { 'a':{ 0:['aardvark', 'abacus'],
                   1:['bat','bar'] 
                   2:['abacus']},
             'b':{ 0:['bat','bar'],
                   1:['abacus']},
             ....
           }

如您所见,如果不使用索引,您最终会大大增加所需的存储空间 - 特别是包含 n 个单词且平均长度为 m 的字典将需要 nm2 的存储空间。但是,您现在可以非常快速地进行查找以从每个集合中找到所有可以匹配的单词。

最后的优化(您可以在幼稚的方法中立即使用)是将所有相同长度的单词分成单独的存储区,因为您总是知道单词有多长。

这个版本将是 O(kx),其中 k 是查询词中 已知 个字母的数量,而 x =x(n) 是在您的实现中查找长度为 n 的字典中的单个项目的时间(通常日志(n)。

所以最终字典如下:

allmap = { 
           3 : { 
                  'a' : {
                          1 : ['ant','all'],
                          2 : ['bar','pat']
                         }
                  'b' : {
                          1 : ['bar','boy'],
                      ...
                }
           4 : {
                  'a' : {
                          1 : ['ante'],
                      ....

那么我们的算法就是:

possiblewords = set()
firsttime = True
wordlen = len(query)
for idx,letter in enumerate(query):
    if(letter is not '?'):
        matchesthisletter = set(allmap[wordlen][letter][idx])
        if firsttime:
             possiblewords = matchesthisletter
        else:
             possiblewords &= matchesthisletter

最后,possiblewords 集合将包含所有匹配的字母。

【讨论】:

  • 你真的觉得这样实用吗?如果你需要找到“土豚”这个词,你现在需要找到集合 {word |字[0] == 'a'},{字|字[1] == 'a'}, {字 | word[2] == 'r'}, ... 等等。您可以通过从最小的子集开始计算来进行一些优化,但如果您的子集变得非常大......?
  • 该算法最终在计算工作方面非常有效,但在存储需求方面却没有。这真的取决于原始“字典”有多大。
  • 这与我的想法基本相同 - 它肯定会比正则表达式匹配器的 O(N) 更有效,并且如果字典来自像英语这样的自然语言,它会做得很好.
  • 这听起来不错,但是如果单词以???xxx开头怎么办..我也不明白运行时与内存空间的关系。它需要的巨大内存空间会减慢程序的速度吗?
  • 时间与内存是算法设计的经典权衡。这里有一些很好的讨论:stackoverflow.com/questions/1898161/memory-vs-performance
【解决方案4】:

基于正则表达式的解决方案会考虑字典中的所有可能值。如果性能是您最大的限制因素,则可以构建一个索引来显着提高速度。

您可以从每个单词长度的索引开始,其中包含每个索引=字符匹配单词集的索引。对于长度为 5 的单词,例如 2=r : {write, wrote, drate, arete, arite}, 3=o : {wrote, float, group} 等。要获得原始查询的可能匹配项,例如“?ro??”,单词集将相交,在这种情况下会产生 {wrote, group}

这是假设唯一的通配符是单个字符并且单词长度是预先知道的。如果这些不是有效的假设,我可以推荐基于 n-gram 的文本匹配,例如在 this paper 中讨论的。

【讨论】:

    【解决方案5】:

    对我来说,这个问题听起来很适合Trie 数据结构。将整个字典输入到您的 trie 中,然后查找该单词。对于丢失的字母,您必须尝试所有子尝试,使用递归方法应该相对容易。

    编辑:我刚才用 Ruby 写了一个简单的实现:http://gist.github.com/262667

    【讨论】:

    • 如果你连续有一堆问号,那个算法会很快退化。
    • 如果你有很多?那么无论如何你都会有很多答案(除非你的字典非常稀疏,这意味着你无论如何都不会有很多子尝试),我不清楚这对多个 ?我错过了什么吗?
    • 从我在这里看到的情况来看,Trie 非常适合查找,但这真的会比 HashMap 快吗?
    • 感谢 trie 实现!当你不计算树的构建阶段时,它真的非常快。我想知道如何最好地处理最坏情况的查询模式,例如????????????????e
    • 如果你使用一个阈值(大约 30ish),在这个阈值之后,而不是构建 trie,你只需维护一个要扫描的单词列表,你可以在不降低速度的情况下降低内存使用量。
    【解决方案6】:

    假设您有足够的内存,您可以构建一个巨大的哈希图来在恒定时间内提供答案。这是python中的一个简单示例:

    from array import array
    all_words = open("english-words").read().split()
    big_map = {}
    
    def populate_map(word):
      for i in range(pow(2, len(word))):
        bin = _bin(i, len(word))
        candidate = array('c', word)
        for j in range(len(word)):
          if bin[j] == "1":
            candidate[j] = "?"
        if candidate.tostring() in big_map:
          big_map[candidate.tostring()].add(word)
        else:
          big_map[candidate.tostring()] = set([word])
    
    def _bin(x, width):
        return ''.join(str((x>>i)&1) for i in xrange(width-1,-1,-1))
    
    def run():
      for word in all_words:
        populate_map(word)
    
    run()
    
    >>> big_map["y??r"]
    set(['your', 'year'])
    >>> big_map["yo?r"]
    set(['your'])
    >>> big_map["?o?r"]
    set(['four', 'poor', 'door', 'your', 'hour'])
    

    【讨论】:

    • 这真的会比其他方法在恒定时间内更快吗?
    • 而且这张地图真的很大,哈哈
    • 所以我计算这大约是 10 亿个键,我可以去掉不可能的话,但这仍然会给我留下很多。这会占用多少空间?
    • 肯定比正则搜索快很多,在实验中试试。如果没有发生冲突,它是恒定的,这取决于您的哈希函数。您可以阅读“哈希表”和“完美哈希”。
    • 在内存空间方面,这样实用吗?
    【解决方案7】:

    如果您只有? 通配符,没有* 通配符可以匹配可变数量的字符,您可以试试这个:对于每个字符索引,构建一个从字符到单词集的字典。即,如果单词是 write、written、drate、arete、arite,那么您的字典结构将如下所示:

    Character Index 0:
      'a' -> {"arete", "arite"}
      'd' -> {"drate"}
      'w' -> {"write", "wrote"}
    Character Index 1:
      'r' -> {"write", "wrote", "drate", "arete", "arite"}
    Character Index 2:
      'a' -> {"drate"}
      'e' -> {"arete"}
      'i' -> {"write", "arite"}
      'o' -> {"wrote"}
    ...
    

    如果您想查找a?i??,您将获取对应于字符索引 0 => 'a' {"arete", "arite"} 的集合和对应于字符索引 2 = 'i' 的集合=> {"write", "arite"} 并取设置的交点。

    【讨论】:

      【解决方案8】:

      您想要的数据结构称为 trie - 请参阅 wikipedia article 以获得简短摘要。

      trie 是一种树形结构,其中通过树的路径形成了您希望编码的所有单词的集合 - 每个节点最多可以有 26 个子节点,每个可能的字母位于下一个字符位置。请参阅维基百科文章中的图表以了解我的意思。

      【讨论】:

        【解决方案9】:

        我相信在这种情况下,最好只使用一个平面文件,其中每个单词都位于一行中。有了这个,您可以方便地使用正则表达式搜索的强大功能,它经过高度优化,可能会击败您可以为这个问题设计的任何数据结构。

        解决方案 #1:使用正则表达式

        这是解决这个问题的 Ruby 代码:

        def query(str, data)    
          r = Regexp.new("^#{str.gsub("?", ".")}$")
          idx = 0
          begin
            idx = data.index(r, idx)
            if idx
              yield data[idx, str.size]
              idx += str.size + 1
            end
          end while idx
        end
        
        start_time = Time.now
        query("?r?te", File.read("wordlist.txt")) do |w|
          puts w
        end
        puts Time.now - start_time
        

        文件wordlist.txt 包含45425 个字(可下载here)。查询?r?te 的程序输出是:

        brute
        crate
        Crete
        grate
        irate
        prate
        write
        wrote
        0.013689
        

        因此读取整个文件并在其中找到所有匹配项只需 37 毫秒。它可以很好地扩展各种查询模式,即使 Trie 非常慢:

        查询????????????????e

        counterproductive
        indistinguishable
        microarchitecture
        microprogrammable
        0.018681
        

        查询?h?a?r?c?l?

        theatricals
        0.013608
        

        这看起来对我来说已经足够快了。

        解决方案 #2:使用准备好的数据进行正则表达式

        如果您想走得更快,您可以将单词列表拆分为包含相同长度单词的字符串,然后根据您的查询长度搜索正确的单词。用以下代码替换最后 5 行:

        def query_split(str, data)
          query(str, data[str.length]) do |w|
            yield w
          end
        end
        
        # prepare data    
        data = Hash.new("")
        File.read("wordlist.txt").each_line do |w|
          data[w.length-1] += w
        end
        
        # use prepared data for query
        start_time = Time.now
        query_split("?r?te", data) do |w|
          puts w
        end
        puts Time.now - start_time
        

        现在构建数据结构大约需要 0.4 秒,但所有查询都快了大约 10 倍(取决于具有该长度的单词数):

        • ?r?te 0.001112 秒
        • ?h?a?r?c?l? 0.000852 秒
        • ????????????????e 0.000169 秒

        解决方案 #3:一个大哈希表(更新要求)

        由于您已经更改了您的要求,您可以轻松地扩展您的想法,只使用一个包含所有预先计算结果的大哈希表。但是,您可以依靠正确实现的哈希表的性能,而不是自己解决冲突。

        在这里,我创建了一个大哈希表,其中每个可能的查询都映射到其结果列表:

        def create_big_hash(data)
          h = Hash.new do |h,k|
            h[k] = Array.new
          end    
          data.each_line do |l|
            w = l.strip
            # add all words with one ?
            w.length.times do |i|
              q = String.new(w)
              q[i] = "?"
              h[q].push w
            end
            # add all words with two ??
            (w.length-1).times do |i|
              q = String.new(w)      
              q[i, 2] = "??"
              h[q].push w
            end
          end
          h
        end
        
        # prepare data    
        t = Time.new
        h = create_big_hash(File.read("wordlist.txt"))
        puts "#{Time.new - t} sec preparing data\n#{h.size} entries in big hash"
        
        # use prepared data for query
        t = Time.new
        h["?ood"].each do |w|
          puts w
        end
        puts (Time.new - t)
        

        输出是

        4.960255 sec preparing data
        616745 entries in big hash
        food
        good
        hood
        mood
        wood
        2.0e-05
        

        查询性能是 O(1),它只是在哈希表中查找。时间 2.0e-05 可能低于计时器的精度。运行它 1000 次时,每个查询平均得到 1.958e-6 秒。为了更快地获得它,我会切换到 C++ 并使用 Google Sparse Hash,它非常节省内存,而且速度很快。

        解决方案 #4:认真对待

        以上所有解决方案都有效,并且对于许多用例来说应该足够好。如果您真的想认真起来并且手上有大量空闲时间,请阅读一些好论文:

        【讨论】:

        • 我们可以让它以 1/1000 的速度运行吗?
        • 例如你可以缓存最近的结果,所以如果相同的查询被使用了两次,只需在 O(1) 中查找。
        • 为了速度,请使用 C 和极快的正则表达式引擎 (re2c.org ?)。
        • 您可以将整个文件存储在内存中,这样您就不必一直重新读取它
        • @SuperString 让它以 1/1000 的速度运行意味着,它需要 1000 倍的时间......
        【解决方案10】:

        Directed Acyclic Word Graph 是解决这个问题的完美数据结构。它结合了 trie 的效率(trie 可以看作是 DAWG 的一个特例),但空间效率更高。典型的 DAWG 将占用包含单词的纯文本文件大小的一小部分。

        枚举满足特定条件的单词很简单,和在 trie 中一样——你必须以深度优先的方式遍历图。

        【讨论】:

        • 这会比 Trie 更快吗?
        • @Danny:trie 已经相当快了,但是 DAWG 将使用更少的内存(因此在内存中更加本地化),因此对其进行搜索可能具有更好的缓存性能。 DAWG 是从 trie 构建的,因此您必须先构建它。
        • 请注意,您也可以将其与区间树的方法结合使用。您可以存储每个顶点的最长可能字符串的长度,因为您预先知道结果字符串的长度。例如,单词:“abc”和 abfg”可以存储在这样的图中: a: 4 b: 4 c: 3 f: 4 g: 4 带有边:a -> bb -> cb -> ff - > g 当搜索 a??g 时,你知道你不必搜索“abc”之外的任何东西,只需要在“abfg”的方向上搜索。这个例子没有很好地说明它,但我希望你明白这一点。
        【解决方案11】:

        我会这样做:

        1. 将字典中的单词连接成一个由非单词字符分隔的长字符串。
        2. 将所有单词放入一个TreeMap中,其中key是单词,value是单词在大String中的起始偏移量。
        3. 查找搜索字符串的基;即不包含 '?' 的最大前导子字符串。
        4. 使用TreeMap.higherKey(base)TreeMap.lowerKey(next(base)) 在字符串中查找匹配项的范围。 (next 方法需要计算下一个更大的词到具有相同数量或更少字符的基本字符串;例如,next("aa")"ab"next("az")"b"。)
        5. 为搜索字符串创建一个正则表达式,并使用Matcher.find()搜索范围对应的子字符串。

        第 1 步和第 2 步是预先完成的,使用O(NlogN) 空格提供数据结构,其中N 是字数。

        '?' 出现在第一个位置时,这种方法会退化为对整个字典的强力正则表达式搜索,但它越靠右,需要进行的匹配就越少。

        编辑

        为了提高'?' 是第一个字符的情况下的性能,创建一个辅助查找表,记录第二个字符为“a”、“b”等的单词运行的开始/结束偏移量.这可以用在第一个非'?'的情况下。是第二个字符。对于第一个非'?'的情况,您可以采用类似的方法。是第三个字符,第四个字符等等,但你最终会得到越来越多的越来越小的运行,最终这种“优化”变得无效。

        另一种需要更多空间但在大多数情况下速度更快的替代方法是为字典中单词的所有旋转准备上述字典数据结构。例如,第一次轮换将包含 2 个或更多字符的所有单词,并且单词的第一个字符移动到单词的末尾。第二次轮换将是 3 个或更多字符的单词,前两个字符移到末尾,依此类推。然后进行搜索,寻找非'?'的最长序列搜索字符串中的字符。如果这个子串的第一个字符的索引是N,则使用Nth旋转查找范围,并在第N个旋转单词列表中搜索。

        【讨论】:

          【解决方案12】:

          我的第一篇文章有​​ Jason 发现的错误,它在什么时候不能正常工作??是在开始。我现在已经从 Anna 那里借用了循环移位。

          我的解决方案: 引入一个词尾字符 (@) 并将所有循环移位的词存储在排序数组中!!对每个字长使用一个排序数组。在查找“th??e@”时,移动字符串以将 ? 标记移动到末尾(获取 e@th??)并选择包含长度为 5 的单词的数组并对第一个出现的单词进行二进制搜索在字符串“e@th”之后。数组中所有剩余的单词都匹配,即我们会找到“e@thoo(thoose)、e@thes(这些)等。

          解决方案有时间复杂度Log(N),其中N是字典的大小,它把数据的大小扩大了6倍左右(平均词长)

          【讨论】:

          • 并非所有的单词都匹配。例如,“thing”与“th??e”不匹配,但在“these”和“those”之间。
          • 是的,你是对的,需要一个额外的过滤器。我会投票赞成你的评论。
          【解决方案13】:

          您是否考虑过使用TernarySearchTree? 查找速度与 trie 相当,但更节省空间。

          我已经多次实现了这种数据结构,在大多数语言中这是一项非常简单的任务。

          【讨论】:

            【解决方案14】:

            鉴于目前的限制:

            • 最多有 2 个问号
            • 当有 2 个问号时,它们一起出现
            • 字典中有大约 100,000 个单词,平均单词长度为 6。

            我有两个可行的解决方案:

            快速解决方案:HASH

            您可以使用哈希,其中键是您的单词,最多两个“?”,值是合适单词的列表。这个哈希将有大约 100,000 + 100,000*6 + 100,000*5 = 1,200,000 个条目(如果你有 2 个问号,你只需要找到第一个的位置......)。每个条目都可以保存一个单词列表,或者一个指向现有单词的指针列表。如果保存一个指针列表,并且我们假设平均每个单词与两个“?”匹配的单词少于 20 个,那么额外的内存小于 20 * 1,200,000 = 24,000,000。

            如果每个指针大小为 4 字节,那么这里的内存需求为 (24,000,000+1,200,000)*4 字节 = 100,800,000 字节 ~= 96 兆字节。

            总结一下这个解决方案:

            • 内存消耗:~96 MB
            • 每次搜索的时间:计算一个哈希函数,并跟随一个指针。 O(1)

            注意:如果你想使用更小的散列,你可以,但是最好在每个条目中保存平衡的搜索树而不是链表,以获得更好的性能。

            精通空间,但仍然非常快速的解决方案:TRIE 变体

            此解决方案使用以下观察:

            如果'?'单词末尾有符号,trie 将是一个很好的解决方案。

            trie 中的搜索将搜索单词的长度,对于最后几个字母,DFS 遍历将带来所有的结尾。 非常快速且非常节省内存的解决方案。

            所以让我们利用这个观察,来构建一个完全像这样工作的东西。

            您可以将字典中的每个单词视为以 @ 结尾的单词(或任何其他字典中不存在的符号)。 所以“空间”这个词将是“空间@”。 现在,如果您使用“@”符号旋转每个单词,您会得到以下结果:

            space@, pace@s, ace@sp, *ce@spa*, e@spac
            

            (第一个字母没有@)。

            如果您将所有这些变体插入到 TRIE 中,您可以通过“旋转”您的单词轻松地在单词的长度上找到您正在寻找的单词。

            示例: 您想找到所有适合 '??ce' 的单词(其中一个是空格,另一个是切片)。 你构建这个词:s??ce@,然后旋转它,这样 ?标志在最后。即'ce@s??'

            所有的旋转变化都存在于 trie 内部,特别是“ce@spa”(上面标有 *)。找到开头后 - 您需要以适当的长度遍历所有延续,并保存它们。然后,您需要再次旋转它们,使 @ 成为最后一个字母,并且 walla - 您找到了所有要查找的单词!

            总结一下这个解决方案:

            • 内存消耗: 对于每个单词,它的所有旋转都出现在 trie 中。平均而言,*6 的内存大小被保存在 trie 中。特里树大小约为其中节省的空间的 *3(只是猜测......)。所以这个 trie 所需的总空间是 6*3*100,000 = 1,800,000 字 ~= 6.8 兆字节。

            • 每次搜索的时间:

              • 旋转单词:O(word length)
              • 在 trie 中寻找开头:O(字长)
              • 遍历所有结尾:O(匹配数)
              • 旋转结尾:O(答案总长度)

              总结起来非常非常快,而且取决于字长*小常数。

            总结一下……

            第二个选择的时间/空间复杂度很高,是您使用的最佳选择。第二种解决方案存在一些问题(在这种情况下,您可能需要使用第一种解决方案):

            • 实施起来更复杂。我不确定是否有开箱即用的内置尝试的编程语言。如果没有 - 这意味着您需要自己实现它......
            • 不能很好地扩展。如果明天您决定需要将问号分布在整个单词中,而不一定要连接在一起,那么您需要认真考虑如何适应第二种解决方案。对于第一个解决方案 - 很容易概括。

            【讨论】:

            【解决方案15】:

            安娜的第二个解决方案是这个解决方案的灵感来源。

            首先,将所有单词加载到内存中,并根据单词长度将字典分成多个部分。

            对于每个长度,制作一个指向单词的指针数组的 n 个副本。对每个数组进行排序,以便字符串在旋转一定数量的字母时按顺序显示。例如,假设 5 字母单词的原始列表是 [plane, apple, space, train, happy, stack, hacks]。那么你的五个指针数组将是:

            rotated by 0 letters: [apple, hacks, happy, plane, space, stack, train]
            rotated by 1 letter:  [hacks, happy, plane, space, apple, train, stack]
            rotated by 2 letters: [space, stack, train, plane, hacks, apple, happy]
            rotated by 3 letters: [space, stack, train, hacks, apple, plane, happy]
            rotated by 4 letters: [apple, plane, space, stack, train, hacks, happy]
            

            (如果可以节省平台空间,您可以使用整数来标识单词,而不是指针。)

            要搜索,只需询问您需要将 模式 旋转多少,以便问号出现在末尾。然后就可以在相应的列表中进行二分查找了。

            如果您需要查找 ??ppy 的匹配项,则必须将其旋转 2 以生成 ppy??。因此,在旋转 2 个字母时查看按顺序排列的数组。快速二分搜索发现“happy”是唯一匹配项。

            如果您需要找到 th??g 的匹配项,则必须将其旋转 4 以生成 gth??。因此,请查看数组 4,其中二进制搜索发现没有匹配项。

            无论有多少问号,只要它们一起出现,这都有效。

            需要空间除了字典本身:对于长度为 N 的单词,这需要空间用于(N 倍于长度为 N 的单词的数量)指针或整数。

            每次查找的时间:O(log n),其中 n 是适当长度的单词数。

            在 Python 中的实现:

            import bisect
            
            class Matcher:
                def __init__(self, words):
                    # Sort the words into bins by length.
                    bins = []
                    for w in words:
                        while len(bins) <= len(w):
                            bins.append([])
                        bins[len(w)].append(w)
            
                    # Make n copies of each list, sorted by rotations.
                    for n in range(len(bins)):
                        bins[n] = [sorted(bins[n], key=lambda w: w[i:]+w[:i]) for i in range(n)]
                    self.bins = bins
            
                def find(self, pattern):
                    bins = self.bins
                    if len(pattern) >= len(bins):
                        return []
            
                    # Figure out which array to search.
                    r = (pattern.rindex('?') + 1) % len(pattern)
                    rpat = (pattern[r:] + pattern[:r]).rstrip('?')
                    if '?' in rpat:
                        raise ValueError("non-adjacent wildcards in pattern: " + repr(pattern))
                    a = bins[len(pattern)][r]
            
                    # Binary-search the array.
                    class RotatedArray:
                        def __len__(self):
                            return len(a)
                        def __getitem__(self, i):
                            word = a[i]
                            return word[r:] + word[:r]
                    ra = RotatedArray()
                    start = bisect.bisect(ra, rpat)
                    stop = bisect.bisect(ra, rpat[:-1] + chr(ord(rpat[-1]) + 1))
            
                    # Return the matches.
                    return a[start:stop]
            
            words = open('/usr/share/dict/words', 'r').read().split()
            print "Building matcher..."
            m = Matcher(words)  # takes 1-2 seconds, for me
            print "Done."
            
            print m.find("st??k")
            print m.find("ov???low")
            

            在我的电脑上,系统字典有 909KB 大,这个程序除了存储单词(指针是 4 个字节)外,还使用了大约 3.2MB 的内存。对于这本词典,您可以使用 2 字节整数而不是指针将其减半,因为每个长度的单词少于 216 个。

            测量结果:在我的机器上,m.find("st??k") 在 0.000032 秒内运行,m.find("ov???low") 在 0.000034 秒内运行,m.find("????????????????e") 在 0.000023 秒内运行。

            通过写出二进制搜索而不是使用 class RotatedArraybisect 库,我将前两个数字缩短到 0.000016 秒:速度是原来的两倍。在 C++ 中实现它会更快。

            【讨论】:

            • log(n) 会不会太慢?很酷,您看到我们可以使用索引而不是单词来节省空间。
            • 不,O(log n) 非常快。当前票数最高的答案是 O(n)。我看到的所有声称比 O(log n) 更快的答案都涉及提前计算所有可能查询的答案。
            • 请注意,对于本词典,log2(n) 为 14 或更少。
            • 好主意!非常快速和高效的内存。我能看到的唯一缺点是像?h?a?r?c?l? 这样的查询。
            【解决方案16】:

            构建所有单词的哈希集。要查找匹配项,请将模式中的问号替换为每种可能的字母组合。如果有两个问号,则一个查询由 262 = 676 个快速、预期时间恒定的哈希表查找组成。

            import itertools
            
            words = set(open("/usr/share/dict/words").read().split())
            
            def query(pattern):
                i = pattern.index('?')
                j = pattern.rindex('?') + 1
                for combo in itertools.product('abcdefghijklmnopqrstuvwxyz', repeat=j-i):
                    attempt = pattern[:i] + ''.join(combo) + pattern[j:]
                    if attempt in words:
                        print attempt
            

            这比我的其他答案使用的内存更少,但随着您添加更多问号,它会成倍地变慢。

            【讨论】:

              【解决方案17】:

              总结:使用两个紧凑的二进制搜索索引,一个是单词,一个是反转单词。空间成本是索引的 2N 个指针;几乎所有的查找都非常快;最坏的情况“??e”仍然不错。如果您为每个字长制作单独的表格,那么即使是最坏的情况也会非常快。

              详细信息: Stephen C. 发布了good idea:搜索有序字典以查找模式可以出现的范围。但是,当模式以通配符开头时,这无济于事。您也可以按字长索引,但这里有另一个想法:在 reversed 字典单词上添加有序索引;那么一个模式总是在正向索引或反向词索引中产生一个小范围(因为我们被告知没有像?ABCD?这样的模式)。单词本身只需要存储一次,两个结构的条目都指向同一个单词,查找过程可以正向或反向查看它们;但是为了使用 Python 的内置二进制搜索功能,我创建了两个单独的字符串数组,浪费了一些空间。 (我使用排序数组而不是其他人建议的树,因为它节省空间并且速度至少一样快。)

              代码

              import bisect, re
              
              def forward(string): return string
              def reverse(string): return string[::-1]
              
              index_forward = sorted(line.rstrip('\n')
                                     for line in open('/usr/share/dict/words'))
              index_reverse = sorted(map(reverse, index_forward))
              
              def lookup(pattern):
                  "Return a list of the dictionary words that match pattern."
                  if reverse(pattern).find('?') <= pattern.find('?'):
                      key, index, fixup = pattern, index_forward, forward
                  else:
                      key, index, fixup = reverse(pattern), index_reverse, reverse
                  assert all(c.isalpha() or c == '?' for c in pattern)
                  lo = bisect.bisect_left(index, key.replace('?', 'A'))
                  hi = bisect.bisect_right(index, key.replace('?', 'z'))
                  r = re.compile(pattern.replace('?', '.') + '$')
                  return filter(r.match, (fixup(index[i]) for i in range(lo, hi)))
              

              测试:(该代码也适用于 ?AB?D? 等模式,但没有速度保证。)

              >>> lookup('hello')
              ['hello']
              >>> lookup('??llo')
              ['callo', 'cello', 'hello', 'uhllo', 'Rollo', 'hollo', 'nullo']
              >>> lookup('hel??')
              ['helio', 'helix', 'hello', 'helly', 'heloe', 'helve']
              >>> lookup('he?l')
              ['heal', 'heel', 'hell', 'heml', 'herl']
              >>> lookup('hx?l')
              []
              

              效率:这需要 2N 个指针加上存储字典单词文本所需的空间(在调整后的版本中)。最坏的情况出现在模式 '??e' 上,它在我的 235k 字 /usr/share/dict/words 中查看 44062 个候选;但是几乎所有的查询都快得多,比如 'h??lo' 查看 190,如果我们需要的话,首先按字长索引会减少 '??e' 几乎为零。每个候选检查都比其他人建议的哈希表查找更快。

              这类似于rotations-index 解决方案,它以需要大约 10N 个指针而不是 2N 个指针为代价避免所有错误匹配候选(假设平均字长约为 10,如我的 /usr/share/dict/words )。

              您可以在每次查找时进行一次二分搜索,而不是两次,使用自定义搜索功能同时搜索下限和上限(因此不会重复搜索的共享部分)。

              【讨论】:

                【解决方案18】:

                如果 80-90% 的准确度是可以接受的,您可以使用 Peter Norvig 的 spell checker。实现小而优雅。

                【讨论】:

                • 当我看到这个问题时,我想到了(我想了很多次)
                【解决方案19】:

                如果你真的想要每秒 10 亿次搜索量级的东西(尽管我无法想象为什么除了制作下一个大师级拼字游戏 AI 或用于大型网络服务的东西之外的任何人会想要那么快),我建议使用线程来生成 [机器上的核心数量] 线程 + 一个将工作委托给所有这些线程的主线程。然后应用您迄今为止找到的最佳解决方案,并希望您不会耗尽内存。

                我的一个想法是,您可以通过按字母准备切片字典来加快某些案例的速度,然后如果您知道所选内容的第一个字母,则可以求助于寻找一个小得多的大海捞针。

                我的另一个想法是,您正在尝试暴力破解某些东西——也许构建一个数据库或列表或用于拼字游戏的东西?

                【讨论】:

                  【解决方案20】:

                  一个懒惰的解决方案是让 SQLite 或其他 DBMS 为您完成这项工作。

                  只需创建一个内存数据库,加载您的单词并使用 LIKE 运算符运行选择。

                  【讨论】:

                    猜你喜欢
                    • 2011-10-14
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-01-05
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-10-18
                    相关资源
                    最近更新 更多