Java中的哈希 - 结构与...访问时间

时间:2013-08-01 20:38:49

标签: java hash time-complexity

我正在寻找关于两个不同但相关的论点的验证 - 上面(A)以及(B)下面的第一行 - 在Q中的评论。

(A) HashMap 的结构方式是:

HashMap 是一个普通表。那是直接内存访问(DMA)。

首先在 HashMap (或一般哈希)背后的整个想法

是为了使用这种恒定时间内存访问

a。)通过他们自己的数据内容(< K,V>)访问记录, 而不是它们在DMA中的位置(表索引)

b。)管理可变数量的记录 - 一些 不具有给定大小的记录,可以/不保持不变 在整个使用这种结构的大小。

因此,Java Hash中的整体结构是:

表格: 表格 //我使用 HashMap

中使用的标识符

此表格的每个单元格都是 存储桶

每个 存储桶 条目类型的链接列表 - 即,该链接列表的每个节点(不是Java / API的链接列表,但是数据结构)是条目类型,其又是< K,V>对。

当一对新对进入哈希时, 为此计算唯一的 hashCode K,V>对。 此 hashCode 是此<的索引的关键。 K,V>在 - 它告诉我 这个< K,V>将进入哈希。 注意: hashCode 通过函数 hash()进行“规范化”(在 HashMap 中为一个) 更好地适应 的当前长度。 indexFor()也在使用中 确定哪个桶,即表格的单元格< K,V>将进去。

当确定桶时,< K,V>被添加到此存储桶中链接列表的开头 - 因此,它是第一个< K,V>此存储桶中的条目以及已存在的链接列表的第一个条目现在是 这个新添加的条目指向的“下一个”条目。

// ============================================= ==================

(B) 从我在 HashMap 中看到的, 的大小调整 - 哈希仅在基于 散列大小和容量,即当前和最大值。整个哈希中的#个条目。

没有对单个铲斗尺寸进行重新构造或调整大小 - 例如当存储桶中的最大#个条目超过这样的“大小”时,“resize()”。

这是不可能的,但可能是大量的条目可能在一个桶中堆积,而其余的哈希几乎是空的。

如果是这种情况,即每个桶的大小没有上限,则散列不是常数而是线性访问 - 理论上是一件事。获取哈希条目需要$ O(n)$时间,其中$ n $是条目总数。但那不应该。

// ============================================= ==================

我认为我在上面的(A)部分中没有遗漏任何内容。

我不完全确定(B)部分。这是一个重要问题,我正在寻找这个论点的准确性。

我正在寻找两个部分的验证。

提前致谢。

// ============================================= ==================

编辑:

最大桶大小是固定的,即每当重构时都会重构哈希 存储桶中的#entries达到最大值将解决它 - 访问时间很明显 在理论和使用上都是不变的。

这不是一个结构良好但快速修复,并且为了持续访问而工作得很好。

hashCodes很可能在整个存储桶中均匀分布,并且不太可能 任何桶都会在达到哈希总体大小的阈值之前达到桶最大值。 这是HashMap的当前设置也在使用的假设。

同样基于Peter Lawrey的讨论。

2 个答案:

答案 0 :(得分:3)

HashMap中的冲突只是拒绝服务攻击等病态案件中的一个问题。

在Java 7中,您可以更改散列策略,以便外部方无法预测您的散列算法。

AFAIK,在Java 8中,字符串键的HashMap将使用树映射而不是链表来进行冲突。这意味着O(ln N)最坏情况而不是O(n)访问时间。

答案 1 :(得分:1)

  

我希望在所有内容都在同一个哈希值时增加表大小。当表的大小确实时,hash-to-bucket映射会发生变化。

你的想法听起来不错。并且它完全正确,基本上当表格大小小于期望/每桶的平均元素数量太大时HashMap会做什么。 它不是通过查看每个桶并检查是否存在太多因为它很容易计算它。

根据this在OpenJDK中实现HashMap.get()

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

这表明HashMap如何找到非常好的元素,但它以非常混乱的方式编写。经过一些重命名,评论和重写后,它看起来大致如下:

public V get(Object key) {
    if (key == null)
        return getForNullKey();

    // get key's hash & try to fix the distribution.
    // -> this can modify every 42 that goes in into a 9
    // but can't change it once to a 9 once to 8
    int hash = hash(key.hashCode());

    // calculate bucket index, same hash must result in same index as well
    // since table length is fixed at this point.
    int bucketIndex = indexFor(hash, table.length);
    // we have just found the right bucket. O(1) so far.
    // and this is the whole point of hash based lookup:
    // instantly knowing the nearly exact position where to find the element.


    // next see if key is found in the bucket > get the list in the bucket
    LinkedList<Entry> bucketContentList = table[bucketIndex];

    // check each element, in worst case O(n) time if everything is in this bucket.
    for (Entry entry : bucketContentList) {
        if (entry.key.equals(key))
            return entry.value;
    }
    return null;
}

我们在这里看到的是,存储桶确实取决于每个密钥对象返回的.hashCode()和当前表大小。它通常会改变。但仅限于.hashCode()不同的情况。

如果你有一个包含2 ^ 32个元素的巨大表格,你可以简单地说bucketIndex = key.hashCode(),它会尽可能完美。遗憾的是没有足够的内存来执行此操作,因此您必须使用更少的存储桶并将2 ^ 32哈希映射到几个存储桶中。这就是indexFor本质上的作用。将大量空间映射为小空间。

在典型的情况下(几乎)没有任何对象具有相同的.hashCode(),这是完全正常的。但是你不能用HashMaps做的一件事就是只添加具有完全相同散列的元素。

如果每个散列都相同,那么基于散列的查找会产生相同的存储桶,并且您的所有HashMap都变成了LinkedList(或者任何数据结构都包含存储桶的元素)。现在你有O(N)访问时间的最坏情况,因为你必须迭代所有N个元素。