【问题标题】:Data structure for efficiently returning the top-K entries of a hash table (map, dictionary)用于有效返回哈希表(映射、字典)的前 K 个条目的数据结构
【发布时间】:2011-01-07 11:44:12
【问题描述】:

这是一个描述:

它的操作类似于带有getputremove 方法的常规映射,但有一个getTopKEntries(int k) 方法来获取按key 排序的top-K 元素:

对于我的特定用例,我在结构中添加、删除和调整许多值,但任何时候大约有 500-1000 个元素;我想有效地返回前 10 个键的条目。

  • 我调用了putremove 方法很多次。
  • 我调用getTopKEntries方法。
  • 我多次调用putremove 方法。
  • 我调用getTopKEntries方法。
  • ...

我希望 O(1) getputremove 操作,并且 getTopKEntries 仅依赖于 K,而不依赖于地图的大小。

那么有效返回地图的前 K 个元素的数据结构是什么?

My other question 类似,但适用于返回地图的所有元素的情况,按键排序。

如果有帮助,键和值都是 4 字节整数。

【问题讨论】:

  • 关键是什么? (直接还是指针?)
  • @Stephan Eggermont: 键和值都是 4 字节整数。

标签: data-structures hash map hashmap sorted


【解决方案1】:

二叉搜索树(即 C++ 中的 std::map)听起来像是完美的结构:它已经按字典顺序排序,即简单的中序遍历将按升序生成元素。因此,迭代前 k 个元素将直接产生顶部 k 个元素。

此外,由于您预见到大量“删除”操作,因此哈希表无论如何都不会非常适合:删除操作会破坏哈希表的负载因子特性,从而导致运行时迅速恶化。

【讨论】:

  • 一棵二叉树在平均情况下需要 O(log n) 时间,但在最坏情况下需要 O(n) 时间。不是真的 O(1).. 我认为在这种情况下,如果不使用 TreeMap(二叉树 + 哈希图),O(1) 是不可能的。
  • 然而,维护二叉搜索树的问题在于操作变成了O(log n)... 我很高兴你接受了remove 操作;我有一个专门的哈希映射实现,具有良好的实时性能,可以平滑地增加容量和压缩。
  • @Chris Kannon: 我当然希望可以在不对整个内容进行排序的情况下获得前 K 个条目。查看我的另一个问题的答案,我可以维护索引,然后只获取前 K 个元素,而不是对整个事物进行排序。但我觉得有更好的方法......
  • 虽然我同意 Chris 对 TreeMap 的选择,但添加到二叉树中不是仍然 O(log n) 吗?我不确定您是否可以实现 o(1) 来添加或删除任何已排序的内容。
  • 如果不对整个内容进行排序,就不可能获得前 K 个条目。为什么不呢,你问?因为如果您碰巧删除了前 K 个条目之一,您需要已经知道哪个条目是第 K+1 个,依此类推直到最后。另一种方法是对剩余条目进行选择排序,以确定哪个现在在前 K 中,这绝对会影响您的删除性能。
【解决方案2】:

我不确定我是否完全接受 Konrad 的观点,即大量删除操作会破坏哈希表的结构。

如果不进行删除操作,您可以将所有对象保存在哈希表中,并将前 K 个对象保存在优先级堆中,以便增量更新。这将使插入 O(1 + log K),即 N 中的恒定时间,假设 K 是恒定的并且不依赖于 N(N = 表中的对象数)。但是,当您有 remove 操作可用时,这不起作用。提议的斐波那契堆具有 O(log N) 摊销删除操作,因此它也没有提供好的解决方案,因为所有对象都需要保留在堆中,如果您最终删除插入的每个对象,您会得到每个插入+删除对的 O(log N) 行为。

我可能会尝试以下方法:

将对象存储在哈希表中,假设您需要整个表用于返回顶部对象之外的其他目的。维护一个优先级堆(标准堆),其中包含 C 的 K * C 个对象,您需要通过实验搜索其值。每当您添加一个新对象时,请尝试将其插入堆中;如果它适合 KC 空间(堆尚未达到容量或将另一个对象推开),则将其插入并在哈希表中设置一个位以表示该对象在堆中;当您将对象推出堆时,请清除该位。当您移除一个对象时,检查该位;如果 bit=1 即对象在堆中,则从那里删除它(除非您从哈希表中有指向它的指针,否则您需要搜索它;最好维护指针)。现在发生的是堆缩小。他们的关键是只要堆仍然至少有 K 个对象,它就保证包含所有前 K 个对象。这就是因素 C 的用武之地,因为它为堆提供了“回旋余地”。当堆大小低于 K 时,您对整个哈希表运行线性扫描并将堆填充回 KC 容量。

设置 C 是经验性的,因为它取决于您的对象如何来去去去;但是调整它应该很容易,因为您可以根据运行时分析对其进行调整。

复杂性:插入是 O(1 + log (KC))。移除是 O(1 + p log (KC) + q N),其中 p 是被移除对象在堆中的概率,q 是需要重建堆的概率。 p 取决于对象如何来去去去的特性。对于简单的分析,我们可以设置 p=(KC/N),即假设均匀概率。 q 对物体的“流动”更加敏感。例如,如果新对象的价值通常随着时间的推移而增加,而您总是删除旧对象,则 q 趋于零。

请注意,有趣的是,p 与 N 成反比,所以实际上这部分在 N 增长时会加速:)

【讨论】:

  • 删除操作会破坏哈希表的性能特征不是我的“观点”——这是解决冲突的开放寻址的固有事实,这是许多高级通用哈希表实现所使用的。即使是复杂的开放寻址方案也会受到这种限制。 (当然,单独的链接没有有这个问题。)
  • @Konrad 是的,通过开放式寻址,您会遇到这个问题。但这不是哈希表构造的一般属性,而是开放寻址方案的副产品。 “观点”方面是您的主张因此被概括了......但是,嘿,感谢您提供有价值的 cmets!
【解决方案3】:

另一种方法是对项目进行排序。

在您的使用场景中,只有 1000 个项目 - 对它们进行排序非常快(请记住 log2 1000 ≈ 10 = 几乎 1),而且似乎不会发生 太频繁了。

您甚至可以调整 selection algorithm 以返回 K 最小的项目。不幸的是,这将仍然依赖于n,而不仅仅是你希望的kO( n + k 记录 k).

(我已将此作为新答案添加,因为它实际上与我的第一个条目完全无关。)

【讨论】:

  • 但是插入和删除会移动大量内存。也许是一个小的 BTree?
  • @Stephan:我想我应该更清楚一点。我指的是哈希表解决方案(具有较大的负载因子,因此遍历是有效的),然后将元素复制到数组中——甚至不复制它们并就地执行选择。这只是一个疯狂的猜测,但我肯定会对一些建议方法的性能比较感兴趣。
【解决方案4】:

我会推荐fibonacci heap

【讨论】:

  • 有趣,但我的remove 操作与put 操作一样多(渐近);在这种情况下,斐波那契堆可能不是那么好......而且我可能需要重新评估摊销数据结构的使用,因为一些需要很长时间才能完成的操作可能会扼杀实时性能......
【解决方案5】:

您可能需要heap(尽管删除可能是个问题)。

【讨论】:

    【解决方案6】:

    除非我今天严重缺乏创造力,否则你无法在 O(1) 上完成所有工作。

    如果您要维护排序顺序,则添加和删除可能会在 O(log n) 处。如果你不是,那么你的搜索将必须是 O(n)。

    哈希表只是不进行排序。我建议您使用 O(log n) 进行插入和删除,并使用建议的数据结构之一(堆可能是最好的)。如果您需要 O(1) 查找,您可以组合一个散列,但是您需要并行维护两个数据结构,还不如使用 TreeMap。

    【讨论】:

      【解决方案7】:

      如果排序键是一个简单的整数或十进制数,trie 会很快。它将耗尽内存,从技术上讲,在 trie 中找到一个元素是 O(log n)。但实际上它会类似于 log256 n,因此常数因子非常小(log256 of 20 亿 = 4) .

      【讨论】:

        【解决方案8】:

        我觉得,堆是解决这个问题的最佳数据结构。因为,put、remove 和 return K 个 top 元素可以在 O(klog(N)) 时间内返回。如果您想要最大元素,请使用最大堆。

        在这里,我假设 k 个顶部元素意味着,您需要具有最大值的 k 个元素。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-05-15
          • 1970-01-01
          • 2015-08-17
          • 2014-04-12
          • 1970-01-01
          • 2015-06-29
          • 2020-07-27
          • 1970-01-01
          相关资源
          最近更新 更多