【问题标题】:Implementing a fixed-size hash map实现固定大小的哈希映射
【发布时间】:2016-01-25 03:17:19
【问题描述】:

我需要实现一个针对内存和速度优化的固定大小的哈希映射,但不清楚这意味着什么:这是否意味着我可以在哈希映射中拥有的存储桶数量是固定的?或者我无法动态分配内存并扩展我的哈希映射的大小以通过链表解决冲突?如果后一个问题的答案是肯定的,那么想到的第一个冲突解决方法是线性探测——任何人都可以评论其他更节省内存和速度的方法,或者指向我开始使用的任何资源吗?谢谢!

【问题讨论】:

  • 这可能意味着桶的大小是固定的。从每个桶开始的每个 LinkedList(或类似的数据结构)可以是任意大小。线性探测和定期重新散列不是坏主意
  • 它可以用许多不同的方式来解释(其中一些你已经合理地描述了)。但这并不是 SO 社区能够真正回答的问题。最好回过头来澄清需求的来源。

标签: java c hash hashmap hashtable


【解决方案1】:

在没有看到具体要求的情况下,很难解释“针对内存和速度优化的固定大小哈希映射”的含义,因此我将重点关注“针对内存和速度优化”方面。

内存

如果哈希映射实际上以“固定”大小存在,则很难给出内存效率的建议。一般来说,open-addressing 可以提高内存效率,因为可以单独存储键和值,而不需要指向下一个和/或上一个链表节点的指针。如果您的哈希映射允许调整大小,您需要选择一种冲突解决策略,在调整大小之前允许更大的负载(元素/容量)。 1/2 是许多哈希映射实现使用的常见负载,但这意味着至少 2x 始终使用必要的内存。冲突解决策略通常需要在速度和内存效率之间进行平衡,特别是针对您的实际需求/用例进行调整。

速度

从现实世界的角度来看,特别是对于较小的哈希映射或那些具有微不足道大小的键的哈希映射,优化速度的最重要方面可能是减少 cache-misses。这意味着将执行操作所需的尽可能多的信息放在连续的内存空间中。

我的建议是使用open-addressing 而不是chaining 来解决冲突。这将允许更多的内存是连续的,并且每个键比较应该至少少 1 次缓存未命中。开放寻址将需要某种探测,但与从内存中获取链表的每个链接的成本相比,在多个数组元素上循环检查键比较应该更快。有关 c++ std::vector 与 std::list 的基准,请参阅 here,尽管算法复杂,但由于空间局部性,对于大多数操作来说,正常的连续数组更快。

就探测类型而言,线性探测存在聚类问题。随着冲突的发生,相邻元素被消耗,这导致数组的同一部分中的冲突越来越多,当表快满时,这变得异常重要。这可以通过重新散列来解决,@ 987654326@ (当您正在探测插入时,如果您到达的元素比被插入的元素更接近它的理想插槽,则交换两者并尝试插入该元素,一个更好描述可以看here)等。二次探测没有线性探测的聚类问题,但是它有自己的局限性,不是每个数组位置都可以从其他数组位置到达,所以取决于大小,通常只有一半的数组可以在需要调整大小之前填充。

数组的大小也会影响性能。最常见的大小是 2 大小的幂和素数大小。 Java: A "prime" number or a "power of two" as HashMap size? 两者都存在参数,但大多数性能将取决于使用情况,特别是两种大小的功率通常非常对于顺序散列很糟糕,但散列到数组索引的转换可以用单个 @ 完成987654332@ 操作与相对昂贵的mod 操作。

值得注意的是,google_dense_hash 是一个非常好的哈希映射,在几乎所有用例中都轻松胜过 c++ 标准库变体,并且使用开放寻址、2 的幂次调整大小约定和二次探测。 Malte Skarupke 编写了一个出色的哈希表,在许多情况下都击败了 google_dense_hash,包括查找。他的实现使用 robin hood 散列和线性探测,具有探测长度限制。在 blog post 中对其进行了很好的描述,并针对其他哈希表进行了出色的基准测试以及对性能提升的描述。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-22
    • 1970-01-01
    • 2018-08-08
    • 1970-01-01
    • 2023-01-18
    • 2016-12-28
    • 2012-04-01
    • 1970-01-01
    相关资源
    最近更新 更多