HashTables如何处理冲突?


98

我在我的学位课程中听说,HashTable如果新的Key条目与另一个碰撞,则a 将在“下一个可用”存储桶中放置一个新条目。

HashTable如果使用碰撞键向后调用时发生碰撞,仍然会如何返回正确的值?

我假设Keysare String类型,并且hashCode()返回说Java生成的默认值。

如果我实现自己的哈希函数并将其用作查找表的一部分(即a HashMapDictionary),那么存在哪些处理冲突的策略?

我什至看到与质数有关的注释!Google搜索中的信息不太清楚。

Answers:


93

哈希表以两种方式之一处理冲突。

选项1:让每个存储桶包含散列到该存储桶的元素的链表。这就是为什么无效的哈希函数会使哈希表中的查询变得非常慢的原因。

选项2:如果哈希表条目都已满,则哈希表可以增加其拥有的存储桶数,然后重新分配表中的所有元素。哈希函数返回一个整数,哈希表必须采用哈希函数的结果,并针对表的大小对其进行修改,以确保可以将其存储到存储桶中。因此,通过增加大小,它将重新哈希并运行取模运算,如果幸运的话,可能会将对象发送到不同的存储桶。

Java在其哈希表实现中同时使用了选项1和2。


1
对于第一种选择,是否有任何理由使用链表而不是数组甚至是二叉搜索树?

1
上面的解释是高层次的,我认为对于链表和数组并没有太大的区别。我认为二叉搜索树会显得过分杀伤力。我还认为,如果您深入研究诸如ConcurrentHashMap之类的内容,那么任何底层的实现细节都可能会导致性能差异,而上面的高层解释并没有考虑到这些细节。
AMS

2
如果使用链接,当给定密钥时,我们如何知道要取回哪个项目?
ChaoSXDemon

1
@ChaoSXDemon可以按键遍历链中的列表,不存在重复键的问题,因为两个不同的键具有相同的哈希码。
2015年

1
@ams:哪个是首选?哈希冲突是否有任何限制,此后第二点由JAVA执行?
Shashank Vivek

78

当您谈论“如果新的Key条目与另一个哈希条目碰撞,哈希表将把一个新条目放入'下一个可用的”存储桶中。”,您正在谈论的是哈希表冲突解决的开放寻址策略


哈希表有几种解决冲突的策略。

第一种大方法要求将键(或指向它们的指针)及其关联值存储在表中,该值还包括:

  • 单独链接

在此处输入图片说明

  • 开放式寻址

在此处输入图片说明

  • 合并哈希
  • 布谷鸟哈希
  • 罗宾汉哈希
  • 2选择散列
  • 跳房子哈希

处理碰撞的另一种重要方法是动态调整大小,它还有几种方法:

  • 通过复制所有条目来调整大小
  • 增量调整大小
  • 单调键

编辑:以上内容是从wiki_hash_table借来的,您应该去看看那里以获取更多信息。


3
“ [[...]要求键(或指向它们的指针)以及相关的值一起存储在表中”。谢谢,这一点在阅读有关存储值的机制时并不总是立即很清楚。
mtone

27

有多种技术可用于处理碰撞。我会解释一些

链接: 在链接中,我们使用数组索引存储值。如果第二个值的哈希码也指向相同的索引,则我们用链接列表替换该索引值,并且所有指向该索引的值都存储在链接列表中,而实际的数组索引指向链接列表的开头。但是,如果只有一个哈希码指向数组的索引,则该值将直接存储在该索引中。检索值时应用相同的逻辑。在Java HashMap / Hashtable中使用它来避免冲突。

线性探测:当表中的索引多于要存储的值时,将使用此技术。线性探测技术基于不断递增的概念,直到您发现一个空插槽为止。伪代码如下所示:

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

双重哈希技术:在这项技术中,我们使用两个哈希函数h1(k)和h2(k)。如果h1(k)处的时隙被占用,则第二个哈希函数h2(k)用于增加索引。伪代码如下所示:

index = h1(k)

while( val(index) is occupied)

index = (index + h2(k)) mod n

线性探测和双重哈希技术是开放寻址技术的一部分,并且仅在可用插槽大于要添加的项数的情况下才可以使用它。与链接相比,它占用的内存更少,因为这里没有使用额外的结构,但是由于移动过多而导致速度变慢,直到找到一个空插槽为止。同样在开放式寻址技术中,当将项目从插槽中移出时,我们放置一个墓碑以指示该项目从此处移出,这就是其为空的原因。

有关更多信息,请访问此站点


18

我强烈建议您阅读最近发表在HackerNews上的这篇博客文章: HashMap如何在Java中工作

简而言之,答案是

如果两个不同的HashMap键对象具有相同的哈希码,将会发生什么?

它们将存储在同一存储桶中,但不会存储在链表的下一个节点中。键equals()方法将用于在HashMap中标识正确的键值对。


3
HashMaps非常有趣,而且深入人心!:)
Alex

1
我认为问题是有关HashTables而不是HashMap的
Prashant Shubham

10

我在学位课程中听说,如果新的Key条目与另一个哈希条目碰撞,则HashTable会将新条目放入“下一个可用”存储桶中。

实际上,至少对于Oracle JDK,这是不正确的(这一个实现细节,在API的不同实现之间可能会有所不同)。相反,每个存储桶都包含Java 8之前的条目的链接列表,以及Java 8或更高版本中的平衡树。

那么如果使用冲突键向后调用时发生此冲突,HashTable将如何仍返回正确的值?

它使用equals()来查找实际匹配的条目。

如果我实现自己的哈希函数并将其用作查找表(即HashMap或Dictionary)的一部分,那么存在哪些处理冲突的策略?

有各种冲突处理策略,它们具有不同的优缺点。 Wikipedia在哈希表上的条目提供了很好的概述。


这是真的两者HashtableHashMap在由Sun / Oracle的JDK 1.6.0_22。
Nikita Rybak

@Nikita:不确定Hashtable,并且我现在无法访问源,但是我100%确信HashMap在我在调试器中见过的每个版本中都使用链接而不是线性探测。
Michael Borgwardt

@Michael Well,我现在正在查看HashMap的来源public V get(Object key)(与上述版本相同)。如果您确实找到了出现这些链接列表的精确版本,我很想知道。
Nikita Rybak

@Niki:我现在正在寻找相同的方法,并且我看到它使用for循环遍历Entry对象的链接列表:localEntry = localEntry.next
Michael Borgwardt

@Michael抱歉,这是我的错误。我用错误的方式解释了代码。自然,e = e.next不是++index。+1
Nikita Rybak

7

自Java 8起更新: Java 8使用自平衡树进行冲突处理,将最坏的情况从O(n)改进为O(log n)进行查找。自平衡树的使用是Java 8中引入的,它是对链接(直到Java 7为止使用)的改进,后者使用链表,并且在查询时需要遍历O(n)(因为它需要遍历)名单)

为了回答问题的第二部分,插入是通过将给定元素映射到哈希图的基础数组中的给定索引来完成的,但是,当发生冲突时,仍然必须保留所有元素(存储在辅助数据结构中) ,而不仅仅是在基础数组中被替换)。通常,这是通过将每个数组组件(插槽)作为辅助数据结构(又称为存储桶)来完成的,然后将元素添加到位于给定数组索引上的存储桶中(如果存储桶中不存在键,在这种情况下将其替换)。

在查找过程中,键被散列到其对应的数组索引,并在给定存储桶中搜索与(精确)键匹配的元素。由于存储桶不需要处理冲突(直接比较键),因此可以解决冲突问题,但是这样做的代价是必须在辅助数据结构上执行插入和查找。关键点在于,在哈希图中,密钥和值都被存储,因此即使哈希冲突,也可以直接比较密钥的相等性(在存储桶中),因此可以在存储桶中唯一标识。

在没有冲突处理的情况下,Collission-handling从O(1)到O(n)进行链接(链接列表用作辅助数据结构)和O(log n),从O(1)的插入和查找性能最差。自平衡树。

参考文献:

在发生高冲突的情况下,Java 8对HashMap对象进行了以下改进/更改。

  • Java 7中添加的备用String哈希函数已被删除。

  • 达到一定阈值后,包含大量冲突键的存储桶会将其条目存储在平衡树中,而不是链表中。

以上更改可确保在最坏的情况下O(log(n))的性能(https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8


您能解释一下在最坏情况下,链表HashMap的插入只有O(1)而不是O(N)吗?在我看来,如果非重复键的冲突率为100%,您最终将不得不遍历HashMap中的每个对象以找到链接列表的末尾,对吗?我想念什么?
mbm29414 '18

在散列图实现的特定情况下,您实际上是正确的,但不是因为您需要找到列表的末尾。在一般情况下的链表实现中,指针存储在头部和尾部,因此可以通过将下一个节点直接附加到尾部来在O(1)中进行插入,但是对于哈希映射,则使用insert方法需要确保没有重复项,因此必须搜索列表以检查元素是否已存在,因此最终得到O(n)。因此,强加给链表的set属性会导致O(N)。我会更正我的回答:)
丹尼尔·瓦兰德


4

由于对Java的HashMap使用哪种算法(在Sun / Oracle / OpenJDK实现中)有些困惑,因此这里是相关的源代码片段(来自Ubuntu上的OpenJDK 1.6.0_20):

/**
 * Returns the entry associated with the specified key in the
 * HashMap.  Returns null if the HashMap contains no mapping
 * for the key.
 */
final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : 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 != null && key.equals(k))))
            return e;
    }
    return null;
}

这种方法(举是从线355至371),仰视时,在表中的条目,例如被称为get()containsKey()和其他一些人。这里的for循环遍历由入口对象形成的链表。

此处是输入对象的代码(691-705 + 759行):

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

  // (methods left away, they are straight-forward implementations of Map.Entry)

}

在此之后的addEntry()方法:

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

这会将新的Entry添加到存储桶的前面,并带有指向旧的第一个条目的链接(如果没有,则为null)。类似地,该removeEntryForKey()方法遍历列表,并负责仅删除一个条目,而保留列表的其余部分不变。

因此,这是每个存储桶的链接条目列表,我非常怀疑它从_20变为_22,因为从1.2开始就是这样。

(此代码是(c)1997-2007 Sun Microsystems,在GPL下可用,但是为了更好地复制,请使用Sun / Oracle的每个JDK中的src.zip以及OpenJDK中包含的原始文件。)


1
我将其标记为社区Wiki,因为它实际上并不是答案,更多地讨论了其他答案。在注释中,根本没有足够的空间来引用这些代码。
圣保罗Ebermann

3

这是Java中非常简单的哈希表实现。仅在实现put()和中get(),但是您可以轻松添加所需的任何内容。它依赖于hashCode()所有对象都实现的Java 方法。您可以轻松创建自己的界面,

interface Hashable {
  int getHash();
}

并根据需要强制通过按键来实现。

public class Hashtable<K, V> {
    private static class Entry<K,V> {
        private final K key;
        private final V val;

        Entry(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }

    private static int BUCKET_COUNT = 13;

    @SuppressWarnings("unchecked")
    private List<Entry>[] buckets = new List[BUCKET_COUNT];

    public Hashtable() {
        for (int i = 0, l = buckets.length; i < l; i++) {
            buckets[i] = new ArrayList<Entry<K,V>>();
        }
    }

    public V get(K key) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        for (Entry e: entries) {
            if (e.key.equals(key)) {
                return e.val;
            }
        }
        return null;
    }

    public void put(K key, V val) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        entries.add(new Entry<K,V>(key, val));
    }
}

2

解决冲突的方法有很多种,其中包括单独链接,开放式寻址,罗宾汉哈希,布谷鸟哈希等。

Java使用独立链接解决哈希表中的冲突。这里有一个很好的链接,说明了如何发生冲突:http ://javapapers.com/core-java/java-hashtable/

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.