【问题标题】:Find the existence of a word in a large dictionary在大字典中查找单词的存在
【发布时间】:2009-08-26 02:46:22
【问题描述】:

假设我有一个包含 2 亿字的平面文件的大型字典,而我的函数需要检查字典中是否存在任何给定的字,那么最快的方法是什么?您无法将字典存储在内存中,因为您只有 1GB 的内存。您可以将它存储在数据库中,但是如果没有任何优化,查询它仍然会非常非常慢。您无法索引完整的单词,因为您没有足够的资源。

编辑:除了下面提到的文件优化方法外,还有没有数据库优化?我正在考虑创建部分索引,例如对于单词中的每 2 个字母,我创建一个索引。这会加快数据库查询吗?

【问题讨论】:

  • 平面文件词典中的单词是按字母顺序排列的吗?
  • 如果索引太大而无法放入 RAM,数据库会不允许您定义索引吗?
  • 这似乎是一个非常人为的问题,因为 a) 我所知道的任何语言都没有接近 200M 字; b) 为什么要限制次优数据结构?
  • 听起来更像是一张彩虹表,而不是一种自然语言。
  • 不,这是一个现实世界的问题。硬件问题会更明确。

标签: dictionary


【解决方案1】:

这是Bloom filter 的经典用例。布隆过滤器是一种概率数据结构,它针对成员资格测试进行了优化(“X 是这个集合的成员吗?”),并提供 O(1) 查找。作为交换,你引入了一个任意小的误报概率——也就是说,过滤器会说一个特定的词存在,但实际上并不存在。您使用的内存越多,您可以使这个概率越小。但是,漏报的概率为零:如果一个词确实存在,过滤器永远不会说它不存在。

在您的特定情况下,使用 80 亿位 (1 GB) 时,每 1,000,000,000 次试验中的误报率可能略高于 1。这是一个极低的误报率。如果您查找 2 亿个随机字符串,您永远不会遇到一个误报的概率约为 82%。

这不需要对字典进行排序,空间效率很高,并且不需要数据库或其他辅助存储结构。总的来说,它可能是满足您需求的不错选择。

【讨论】:

  • 我真的很喜欢这个建议,但这个问题看起来好像没有误报的余地。
  • 我没有看到任何东西倾向于这种或另一种方式。但我会澄清我的回应,补充说它只适用于你认为误报概率很小的情况。使用 80 亿比特,您可以获得比每十亿次尝试中的误报率略高的误报率。这是一个极低的误报率。如果您查找全部 2 亿个单词,您从未遇到过一个误报的概率约为 82%。
【解决方案2】:

使用Trie 可以有效地解决经典的单词查找问题。不幸的是,正如您所提到的,您无法将所有您需要的数据存储在内存中,但这不应该阻止您使用 Trie 来减少搜索空间。假设您只存储初始段,而不是将整个单词集存储在 Trie 中,并且您的末端节点指向可以在数据库中轻松(且快速)搜索的小型数据集合。

【讨论】:

  • +1。比我的回答好。 Trie 将开始和结束位置存储在文件中,binsearch 可以在文件块中找到单词。
  • 假设我的样本空间全是字母和数字,那么 36^4 =1,679,616,我最多可以构建 4 到 5 个级别的尝试,但我想它仍然有助于大大减少搜索空间.
  • 当然...但是由于您编码的内容相当于所有 4 个字符前缀的集合,因此您仍然大大减少了您的集合。这取决于单词长度的分布,这实际上有多有效,但在最坏的情况下,您可以在 O(4) 时间内搜索前 4 个字符,并使用 outis 的解决方案搜索该叶子上的单词子集,如他在评论中建议。
  • @wo_shi_ni_ba_ba:你的数学没用,但它仍然可以使用 1/2 GiB 达到 5 个级别。每个节点占用n=37*sizeof(void*) 字节(36 个子节点指针和一个分支/叶子标记;在叶子中,36 个指针被解释为文件范围 [开始/长度对]。通过对齐,一个节点占用与 37 个指针相同的空间)。总共有t=2 ** 29 / n 个节点(1/2 GiB 除以节点大小),完整树的深度大约为log(t)/log(36)+1。一个节点不知道它以哪个字符开头;该信息隐式存储在子指针数组中的节点索引中。
  • 这个问题是在“算法设计手册”一书中提出的,AFAIK 作者使用压缩特里树将大量的字典式数据(在他的情况下是人类基因组)保存在内存中/内存。
【解决方案3】:

二分查找

假设字典中的单词按字母顺序排列,我会尝试修改binary search。通过跳转到文件的中点位置并查看其中的单词来划分和征服文件。如果猜到高,则将较低的分成两半,然后重试,直到没有文件位置可供尝试或找到该单词。

(作为outis mentioned in a comment,在跳转到文件位置后,您需要前后扫描以找到您跳转到的单词的边界。)

您可以通过根据单词的第一个字母立即猜测位置块来优化这一点。例如,如果单词以“c”开头,则从文件的第 3/26 部分开始搜索。但实际上,我认为这个早期的猜测总体上只会产生微不足道的影响。

其他优化可能包括保留索引的一小部分。例如,保留以字母表中每个字母开头的第一个单词的索引,或保留以每个可能的两个字母组合开头的每个单词的索引。这将允许您立即缩小搜索范围。

O(log n)

【讨论】:

  • +1,但缺少一件事:任意文件位置很可能位于单词的中间。通过向前(或向后)扫描直到找到单词边界即可轻松修复。
  • @wo_shi_ni_ba_ba:这取决于文件系统 API,但应该是单个函数或方法调用。对于 PHP,有 fseek。在 Java 中,你有 SeekableByteChannel.position(long)
  • @Ryan:您可以扩展位置猜测优化。让dict 成为字典,word 成为正在搜索的单词。在给定的迭代中,假设l 下界和u 上界的单词之间的第一个差异位于i 位置。那么m = abs(word[i]-dict[l][i])/abs(dict[u][i]-dict[l][i]) + l 就是猜测的中点。基本上,您假设分布均匀。仍然是 O(log(N)),但时间应该包含一个更小的常数因子,从而提供真实世界的加速。
  • 顺便说一句,将数据存储在闪存驱动器上可能比将数据存储在速度相当快的硬盘上要快。这就是 ReadyBoost 背后的基本理念……闪存驱动器上的实际 IO 传输速度较慢,但​​“搜索”时间要快得多。如果与您的数据集相比,您的硬盘有大量缓存,那么使用硬盘可能会更好。
  • @wo_shi_ni_ba_ba 让我们假设一个绝对可怕的硬盘寻道时间为 50 毫秒(为了简单起见,也涵盖了任何其他计算时间)。最坏情况的二分搜索将是 200,000,000 的日志基数 2,即 27.5。 27.5 x 50 毫秒 = 1.375 秒。 (大多数硬盘寻道时间约为 9 毫秒。)
【解决方案4】:

如果单词有很多前缀和后缀,您可以使用 Directed Acyclic Word Graph 将它们全部加载到内存中(怎么样,DAWG!)

这就像一个 trie,但压缩了共享的后缀。这是否有用取决于您的字典中的内容,但将 200M 装入 1GB 内存可能是可行的。

【讨论】:

  • +1 for DAWG over Trie 在内存受限的环境中实现更高效的存储。
【解决方案5】:

使用 Boyer–Moore 字符串搜索算法?

http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm

【讨论】:

  • 是否可以在不加载到主存的情况下使用 Boyer-Moore 算法?
【解决方案6】:

如果您没有索引,请使用流。

有时最简单的解决方案是最好的。

   public Int32 IndexOf(FileStream file, Byte[] ascii_bytes, Int32 start_index)
   {
       Int32 index = -1;
       {
           Int32 current = 0;
           Int64 original_index = 0;
           Boolean found = true;

           file.Position = start_index;
           current = file.ReadByte();
           while (current >= 0)
           {
               if ((Byte)current == ascii_bytes[0])
               {
                   found = true;
                   original_index = file.Position - 1;
                   for (Int32 i = 1; (i < ascii_bytes.Length && current > 0); i++)
                   {
                       current = file.ReadByte();
                       if ((Byte)current != ascii_bytes[i])
                       {
                           file.Position--;
                           found = false;
                           break;
                       }
                   }

                   if (found)
                   {
                       file.Position = original_index;
                       index = (Int32)original_index;
                       break;
                   }
               }
               current = file.ReadByte();
           }
       }
       return index;
   }

【讨论】:

  • 几乎不是“最快”的方法
  • +1 表示流。 -1 用于蛮力搜索。净结果 0 票。
  • @Chaos 他在问题中要求快
【解决方案7】:

假设:

  1. 您将在进程的生命周期中多次搜索一个单词(而不是每次查找一个单词时都启动一个进程)。
  2. 文件已排序。

您可以部分索引数据,占用大部分可用内存:使用 B 树或排序数组将单词及其起始位置存储在文件中(后者更节省空间,但需要单个连续块; 另外,b-tree 要求您存储块的结束位置,而数组不需要)。留出足够的内存空间以从文件中加载单个单词块。在索引(树遍历或二进制搜索)中搜索将包含该单词的块。从部分索引中找到特定块后,将文件中的相应块加载到内存中并对其执行二进制搜索。

如果你需要额外的内存,你可以从索引中折腾一些元素。使用数组,您可以使用以下伪 C 伪代码将索引减少到 n 个元素:

struct chunk {
    string word;
    int start;
};
chunk index[];
d = index.length / n;
for (i=0;i<n; ++i) {
    index[i] = index[i*d];
}
realloc(index, sizeof(chunk) * n);

由于块 i 的结尾是 index[i+1].start,因此该算法对于数组实现来说非常简单。对于基于 B 树的索引,您可以轻松地将叶子与其父级合并。

【讨论】:

    【解决方案8】:

    如果某些词的查询频率比其他词高得多,那么内存中的 LRU 缓存和其背后的数据库可能是有意义的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-10-19
      • 1970-01-01
      • 1970-01-01
      • 2011-02-07
      • 2021-06-03
      • 2012-12-05
      • 2013-11-09
      • 1970-01-01
      相关资源
      最近更新 更多