不可变集合 SetN 实现详细信息

2022-09-03 14:43:58

我很难理解java-9的实现细节;具体来说,为什么需要将内部数组增加两次。ImmutableCollections.SetN

假设您执行以下操作:

Set.of(1,2,3,4) // 4 elements, but internal array is 8

更确切地说,我完全理解为什么在a的情况下这样做(双重扩展) - 你永远不会(几乎)想要成为一个。例如,当条目更好地分散到存储桶中时,值为 可缩短搜索时间。HashMapload_factor!=1

但是在不可变的集合的情况下 - 我真的不能说。特别是因为内部数组的索引选择方式。

让我提供一些细节。首先如何搜索索引:

 int idx = Math.floorMod(pe.hashCode() ^ SALT, elements.length);

pe是我们放入集合中的实际值。 在启动时仅生成32位,每一次(如果需要,这是实际的随机化)。 对于我们的例子是(4个元素,但这里8个 - 大小加倍)。SALTJVMelements.length8

此表达式类似于负安全模运算。请注意,例如,在选择存储桶时,会在 () 中完成相同的逻辑操作。HashMap(n - 1) & hash

因此,对于我们的情况,则此表达式将返回任何小于 8 的正值。elements.length is 8(0, 1, 2, 3, 4, 5, 6, 7)

现在剩下的方法:

 while (true) {
        E ee = elements[idx];
        if (ee == null) {
            return -idx - 1;
        } else if (pe.equals(ee)) {
            return idx;
        } else if (++idx == elements.length) {
            idx = 0;
        }
    }

让我们分解一下:

if (ee == null) {
    return -idx - 1;

这很好,这意味着数组中的当前插槽是空的 - 我们可以将我们的值放在那里。

} else if (pe.equals(ee)) {
    return idx;

这很糟糕 - 插槽被占用,并且已经到位的条目等于我们想要放置的条目。s 不能有重复的元素 - 因此稍后会引发异常。Set

 else if (++idx == elements.length) {
      idx = 0;
 }

这意味着此槽被占用(哈希冲突),但元素不相等。在此条目中,将放入与 or 相同的存储桶中 - 但此处不是这种情况。HashMapLinkedNodeTreeNode

因此递增并尝试下一个位置(小警告是,当它到达最后一个位置时,它会以圆形方式移动)。index

问题是:如果在搜索索引时没有做任何太花哨的事情(除非我错过了什么),为什么需要一个两倍大的数组?或者为什么函数不是这样写的:

int idx = Math.floorMod(pe.hashCode() ^ SALT, input.length);

// notice the diff elements.length (8) and not input.length (4)

答案 1

的当前实现是一个相当简单的封闭哈希方案,而不是 使用单独的链接方法。(“闭合散列”也被称为“开放寻址”。在封闭哈希方案中,元素存储在表本身中,而不是存储在从每个表槽链接的元素的列表或树中,这是单独的链接。SetNHashMap

这意味着,如果两个不同的元素散列到同一个表槽,则需要通过为其中一个元素找到另一个槽来解决此冲突。当前的实现使用线性探测解决了这个问题,其中按顺序检查表槽(在最后环绕),直到找到空插槽。SetN

如果要存储 N 个元素,它们肯定会适合大小为 N 的表。您始终可以找到集合中的任何元素,尽管您可能必须探测多个(或多个)连续的表槽才能找到它,因为会有很多冲突。但是,如果探测集合的对象不是成员,则线性探测必须检查每个表槽,然后才能确定该对象不是成员。对于完整的表,大多数探测操作将降级为 O(N) 时间,而大多数基于哈希的方法的目标是操作为 O(1) 时间。

因此,我们有一个类时空权衡。如果我们把桌子弄大,整个桌子上就会有空的插槽。存储物品时,碰撞应该更少,线性探测会更快地找到空槽。彼此相邻的满槽群集将更小。对非成员的探测将更快地进行,因为它们更有可能在线性探测时更快地遇到空槽 - 可能是在根本不需要重新探测之后。

在提出实现时,我们使用不同的扩展因子运行了一堆基准测试。(我在代码中使用了术语EXPAND_FACTOR,而大多数文献都使用负载因子。原因是膨胀因子是负载因子的倒数,如 中所用,并且对这两种含义使用“负载因子”会令人困惑。当膨胀系数接近1.0时,探头性能相当慢,正如预期的那样。随着膨胀系数的增加,它得到了显着改善。当它达到3.0或4.0时,改进确实已经趋于平缓。我们选择了 2.0,因为它获得了大部分性能改进(接近 O(1) 时间),同时与 .(抱歉,我们没有在任何地方发布这些基准测试数据。HashMapHashSet

当然,所有这些都是实现细节,并且可能会从一个版本更改为下一个版本,因为我们找到了更好的方法来优化系统。我确信有办法改进当前的实现。(幸运的是,当我们这样做时,我们不必担心保留迭代顺序

有关开放寻址和性能权衡与负载因子的良好讨论,请参见

塞奇威克,罗伯特和凯文韦恩。算法第四版。艾迪生-卫斯理, 2011.

在线图书网站在这里,但请注意,印刷版有更多细节。


答案 2