【问题标题】:What is the fastest way to count the unique elements in a list of billion elements?计算十亿元素列表中唯一元素的最快方法是什么?
【发布时间】:2019-11-25 13:33:33
【问题描述】:

我的问题并不常见。让我们想象几十亿个字符串。字符串通常少于 15 个字符。在这个列表中,我需要找出唯一元素的数量。

首先,我应该使用什么对象?你不应该忘记如果我添加一个新元素,我必须检查它是否已经存在于列表中。一开始这不是问题,但是在几百万字之后它确实会减慢这个过程。

这就是为什么我认为 Hashtable 将是这项任务的理想选择,因为理想情况下检查列表只需要 log(1)。不幸的是,.net 中的单个对象只能是 2GB。

下一步将实现一个自定义哈希表,其中包含一个 2GB 哈希表列表。

我想知道也许你们中的一些人知道更好的解决方案。 (电脑的规格非常高。)

【问题讨论】:

  • “独特元素”是指字符还是字符串?字符串是一个单词吗?
  • 您认为会有很多独特的元素,还是大多数字符串都可能重复?
  • 最快的编码方式:将所有内容添加到 SQL Server 表并发出查询。
  • 字符串中的字符限制为一个字节或更少(例如 ANSI、ASCII)或 Unicode 或...?
  • “我需要找出唯一元素的数量”——你是在计算同一个字符串的多次出现,找出该字符串是否在集合中,还是在做其他事情?

标签: c# algorithm memory collections


【解决方案1】:

我会跳过数据结构练习,只使用 SQL 数据库。为什么要编写另一个您必须分析和调试的自定义数据结构,只需使用数据库。他们真的很擅长回答这样的问题。

【讨论】:

  • 这真的取决于他的应用程序限制,并且做出了一个可能不成立的假设。
  • 这是一道编程题,不是查询题。 (是的,查询是程序,但让我们避开它。)加上 OP 将问题标记为 C#。
  • SQL Server 等数据库引擎针对大量数据进行了优化。任何内存中的算法都有花费太多时间并导致过多分页和/或线程争用的风险。在这种情况下,我认为您不应该排除数据库可能是最快的。
  • 这是一个非常糟糕的主意,与使用 trie 跟踪您看到的字符串对数据进行单次迭代相比,这将花费很长时间。我唯一的遗憾是我只能投反对票一次。
  • (1) 数据库针对基于集合的函数进行了优化 - 存在、交集、计数等。 (2) C# 在我上次检查时具有数据库访问权限 (3) 如果数据集更大比可用/有效的内存大小,那么自定义数据结构变得非常困难 - 考虑如何将部分 trie 分页到磁盘并仍然使其高效 (4) 不排除如果您需要多次执行加载数据的成本 (5) 尝试编写一个多线程可以遍历并允许修改的 trie
【解决方案2】:

我会考虑使用TrieDirected acyclic word graph,它们应该比哈希表更节省空间。对字符串成员资格的测试将是 O(len),其中 len 是输入字符串的长度,这可能与字符串散列函数相同。

【讨论】:

  • 我没有硬数据,但我认为 trie 会比数据库快。
  • 附加好处:尝试真的很容易实现。
  • 我们不要混淆我们的 Ns。测试 DAWG 中的成员身份将是 O(n),但 n 是字符串中的字符数,而不是集合中的字符串数。巨大的差异。
  • 我使用了有向无环词图。非常有效。
  • 一个包含这么多单词的 trie 可能适合也可能不适合 2G,具体取决于数据和 trie 节点的实现(即使是索引也需要 4 个字节......)。
【解决方案3】:

这可以在最坏情况 O(n) 时间内解决,使用 radix sort 将计数排序作为每个字符位置的稳定排序。这在理论上比使用哈希表(O(n) 预期但不能保证)或合并排序(O(n log n))更好.使用 trie 还会导致最坏情况的 O(n) 时间解决方案(对 n 键的恒定时间查找,因为所有字符串都有一个有界长度,即小常数),所以这是可比的。我不确定他们在实践中如何比较。基数排序也很容易实现,并且有很多现有的实现。

如果所有字符串都是d个或更短的字符,并且不同字符的个数是k,那么基数排序需要O(d ( n + k)) 对 n 个键进行排序的时间。排序后,您可以在 O(n) 时间内遍历排序列表,并在每次到达新字符串时递增一个计数器。这将是不同字符串的数量。由于 d 约为 15 并且 kn (十亿)相比相对较小,因此运行时间还不错。

这使用 O(dn) 空间(保存每个字符串),因此它的空间效率低于尝试。

【讨论】:

  • 比建议数据库更好,但仍然 - 对数据进行排序是多余的,并且问题空间不需要。任何这样做的解决方案都不是最优的。然而,特里树旨在解决几乎这个确切的问题。
  • @Terry Mahaffey:比较排序(例如合并排序)不是最优的。然而,问题的约束允许基数排序,这是最优的(渐近的)。被排序的标记是有界长度的字符串,每个位置都有一定数量的可能字符。我同意尝试更好(出于空间原因),但不是因为基数排序不是最优的。
【解决方案4】:

如果项目是可比较的字符串......那么我建议放弃哈希表的想法并使用更像二叉搜索树的东西。 C# 中有几个实现(没有一个内置到框架中)。确保获得平衡的,例如红黑树或 AVL 树。

优点是树中的每个对象都相对较小(仅包含它的对象,以及到其父对象的链接和两个叶子),因此您可以拥有大量的对象。

另外,因为是排序的,所以检索和插入时间都是O log(n)。

【讨论】:

    【解决方案5】:

    由于您指定单个对象不能包含所有字符串,因此我假设您在磁盘或其他一些外部存储器上拥有字符串。在那种情况下,我可能会进行排序。从排序列表中提取唯一元素很简单。合并排序在外部排序中很流行,并且只需要与您拥有的空间相等的额外空间。首先将输入分成适合内存的部分,对它们进行排序,然后开始合并。

    【讨论】:

      【解决方案6】:

      对于几十亿个字符串,即使只有百分之几是唯一的,散列冲突的可能性也相当高(.NET 散列码是 32 位 int,产生大约 40 亿个唯一散列值。如果你有这么少作为 1 亿个唯一字符串,哈希冲突的风险可能高得无法接受)。统计数据不是我的强项,但一些谷歌研究表明,完美分布的 32 位散列的冲突概率是 (N - 1) / 2^32,其中 N 是散列的唯一事物的数量.

      使用使用更多位的算法such as SHA-1,您运行哈希冲突的概率要低得多。

      假设一个足够的散列算法,一个接近你已经尝试过的简单方法是创建一个散列表数组。将可能的哈希值划分为足够多的数字范围,这样任何给定的块都不会超过每个对象 2GB 的限制。根据散列值选择正确的散列表,然后在该散列表中搜索。例如,您可以创建 256 个哈希表并使用 (HashValue)%256 从 0..255 获取哈希表编号。在将字符串分配给存储桶以及检查/检索它时使用相同的算法。

      【讨论】:

        【解决方案7】:

        分而治之 - 按前 2 个字母划分数据(比如)

        xx字典=>字符串字典=>计数

        【讨论】:

        • 我的倾向,但我会让第一个分区更有效。不要键入两个字符,键入字符串哈希的前 16 位。
        • 要获取字符串的哈希值,您需要扫描整个字符串。检查前几个字符可能会更快(尽管它可能会受到总线速度的限制,并且由于缓存一次加载一行,可能不会)
        【解决方案8】:

        我会使用数据库,任何数据库都可以。

        可能是最快的,因为现代数据库针对速度和内存使用进行了优化。

        你只需要一列有索引,然后你就可以统计记录数了。

        【讨论】:

        • 我怀疑通用数据库在这种情况下能否胜过专门的优化算法。通用数据库平衡了许多相互竞争的需求(插入速度、更新速度、查询速度、内存与 CPU)。可以根据 OP 的需要调整专门的算法。
        • 但是什么是最快的?将数据转储到数据库中,或者选择或发明并调整专门的算法。如果您可以将所有内容保存在内存中,并且不满足 Array、List 或 Dictionary 的内部限制,那么实现将大致相同,代码性能可能会更快。但是,如果您达到这些限制....
        【解决方案9】:

        +1 用于 SQL/Db 解决方案,让事情变得简单——让您专注于手头的实际任务。

        但仅出于学术目的,我想加上我的 2 美分。

        -1 用于哈希表。 (我还不能投反对票)。因为它们是使用桶实现的,所以在许多实际实现中存储成本可能很大。另外,我同意 Eric J 的观点,碰撞的机会会破坏时间效率优势。

        Lee,构建 trie 或 DAWG 会占用空间以及一些额外的时间(初始化延迟)。如果这不是问题(当您将来可能需要对字符串集执行类似搜索的操作并且您有足够的可用内存时),尝试可能是一个不错的选择。

        空间将是基数排序或类似实现(如 KirarinSnow 所述)的问题,因为数据集很大。

        以下是我对一次性重复计数的解决方案,其中限制了可以使用的空间。

        如果我们的内存可以存储 10 亿个元素,我们可以在 Θ(n log n) 时间内通过 heap-sort 对它们进行排序,然后在 O(n ) 时间和这样做:

        if (a[i] == a[i+1])
            dupCount++;
        

        如果我们没有那么多可用内存,我们可以将磁盘上的输入文件分成更小的文件(直到大小变得足够小以将集合保存在内存中);然后使用上述技术对每个这样的小文件进行排序;然后将它们合并在一起。这需要对主输入文件进行多次传递。

        我想远离quick-sort,因为数据集很大。如果我可以为第二种情况挤入一些内存,我会更好地使用它来减少通过次数,而不是将其浪费在合并排序/快速排序中(实际上,这在很大程度上取决于我们手头的输入类型)。

        编辑:SQl/DB 解决方案仅在您需要长时间存储此数据时才适用。

        【讨论】:

          【解决方案10】:

          您是否尝试过哈希映射(.Net 中的字典)? Dictionary<String, byte> 在 x86 上每个条目只占用 5 个字节(4 个用于指向字符串池的指针,1 个用于字节),大约是 400M 个元素。如果有很多重复项,它们应该能够适应。在实现方面,它可能非常慢(或不起作用),因为您还需要将所有这些字符串存储在内存中。

          如果字符串非常相似,您也可以编写自己的 Trie 实现。

          否则,最好的办法是在磁盘上对数据进行就地排序(之后计算唯一元素就变得微不足道了),或者使用更低级别、更占用内存的语言,如 C++。

          【讨论】:

          • 为什么字符串必须“非常相似”才能使用 Trie?
          • 因为如果每个节点都是 26 个额外的指针,你必须有内存。字符串越不相似,您拥有的节点就越多。
          【解决方案11】:

          字典 在内部组织为列表列表。您不会接近 64 位机器上的 (2GB/8)^2 限制。

          【讨论】:

          • 在 32 位和 64 位操作系统上 CLR 对象的最大大小是否存在差异?
          • 好的,那么限制是进程的最大内存大小?
          • 不,64 位进程有 TB,具体取决于分页文件的大小。 2GB 限制是 x64 指令集问题,结合 Int32 数组索引限制。
          【解决方案12】:

          我同意其他关于数据库解决方案的海报,但除此之外,对触发器的合理智能使用和可能可爱的索引方案(即字符串的数字表示)将是最快的方法,恕我直言.

          【讨论】:

            【解决方案13】:

            如果您需要的是唯一计数的近似值,请查找 HyperLogLog 算法。它用于对您所指的大型数据集的基数进行密切估计。 Google BigQuery、Reddit 将其用于类似目的。许多现代数据库已经实现了这一点。它非常快,并且可以使用最少的内存。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2016-12-20
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-12-17
              • 2013-07-02
              • 1970-01-01
              • 2021-11-10
              相关资源
              最近更新 更多