ImmutableCollections SetN实现细节

时间:2017-07-27 13:55:29

标签: java collections java-9

我很难理解java-9中的实现细节ImmutableCollections.SetN;特别是为什么需要增加两次内部数组。

假设你这样做:

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

更确切地说,我完全理解为什么在HashMap的情况下完成(双重扩展) - 你从不(几乎)希望load_factor成为一个。值!=1可以改善搜索时间,因为条目可以更好地分散到存储桶中。

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

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

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

pe是我们在集合中放置的实际值。 SALT在启动时仅生成32位,每JVM一次(如果需要,这是实际的随机化)。我的示例elements.length8(4个元素,但此处为8个 - 大小加倍)。

此表达式类似于负安全模运算。请注意,选择存储区时,HashMap中的相同逻辑事件(例如(n - 1) & hash)也是如此。

因此,对于我们的情况,如果elements.length is 8,那么此表达式将返回任何小于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;

这很糟糕 - 插槽被占用,已经存在的条目等于我们想要放置的条目。 <{1}}不能有重复的元素 - 因此稍后会抛出异常。

Set

这意味着此插槽已被占用(哈希冲突),但元素不相等。在 else if (++idx == elements.length) { idx = 0; } HashMap中,此条目将与LinkedNodeTreeNode放在同一个广告系列中 - 但不是这里的情况。

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

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

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

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

1 个答案:

答案 0 :(得分:15)

SetN的当前实现是一种相当简单的闭合散列方案,而不是HashMap使用的单独链接方法。 (“封闭散列”也混淆地称为“open addressing”。)在封闭散列方案中,元素存储在表本身中,而不是存储在从每个表链接的元素的列表或树中插槽,这是单独的链接。

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

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

因此,我们有一个类时空权衡。如果我们把桌子做得更大,整个桌子上都会有空的插槽。存储项目时,应该有更少的冲突,线性探测将更快地找到空槽。彼此相邻的完整时隙簇将更小。非成员的探测器将更快地进行,因为他们更可能在线性探测时更快地遇到空槽 - 可能在完全不需要重新探测之后。

在提出实施时,我们使用不同的扩展因子运行了一系列基准测试。 (我在代码中使用了术语 EXPAND_FACTOR ,而大多数文献使用加载因子。原因是扩展因子是负载因子的倒数,如{{{对于这两种含义使用“加载因子”会令人困惑。)当扩展因子接近1.0时,探测器性能非常慢,正如预期的那样。随着扩展系数的增加,它得到了显着改善。到达3.0或4.0时,这种改进确实很平坦。我们选择了2.0,因为它获得了大部分性能提升(接近O(1)时间),同时与HashMap相比节省了大量空间。 (对不起,我们没有在任何地方发布这些基准数字。)

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

有关负载因子的开放寻址和性能权衡的详细讨论可以在

的第3.4节中找到
  塞奇威克,罗伯特和凯文韦恩。 算法,第四版。 Addison-Wesley,2011年。

在线图书网站here,但请注意印刷版的详细信息。