HashTables 如何处理冲突?

Posted

技术标签:

【中文标题】HashTables 如何处理冲突?【英文标题】:How do HashTables deal with collisions? 【发布时间】:2011-06-26 05:31:09 【问题描述】:

我在学位课程中听说HashTable 将在新的 Key 条目与另一个条目发生冲突时将一个新条目放入“下一个可用”存储桶中。

如果在使用冲突键调用返回时发生这种冲突,HashTable 如何仍然返回正确的值?

我假设KeysString 类型,hashCode() 返回由Java 生成的默认值。

如果我实现自己的散列函数并将其用作查找表的一部分(即HashMapDictionary),有哪些应对冲突的策略?

我什至见过与素数有关的笔记!谷歌搜索的信息不太清楚。

【问题讨论】:

【参考方案1】:

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

选项 1: 通过让每个存储桶包含一个元素的链接列表,这些元素会散列到该存储桶。这就是为什么一个糟糕的哈希函数会使哈希表中的查找变得非常缓慢的原因。

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

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

【讨论】:

在第一个选项的情况下,是否有任何理由使用链表而不是数组甚至二叉搜索树? 上面的解释是高级别的,我认为链表和数组没有太大区别。我认为二叉搜索树将是矫枉过正。另外我认为,如果您深入研究 ConcurrentHashMap 和其他东西,那么有许多低级实现细节会产生性能差异,而上面的高级解释并没有解释。 如果使用链接,当给定一个键时,我们如何知道要取回哪个项目? @ChaoSXDemon 您可以按键遍历链中的列表,重复键不是问题,问题是两个不同的键具有相同的哈希码。 @ams:哪一个更受欢迎? Hash冲突是否有限制,之后JAVA执行第二点?【参考方案2】:

当您谈到“如果新的 Key 条目与另一个条目发生冲突时,Hash Table 将在 'next available' bucket 中放置一个新条目。”,您是在谈论 Collision 的 开放寻址策略哈希表的解析。


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

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

单独链接

开放寻址

合并散列 Cuckoo 哈希 罗宾汉哈希 二选散列 跳房子散列法

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

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

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

【讨论】:

“[...] 要求将键(或指向它们的指针)与关联的值一起存储在表中”。谢谢,在阅读有关存储值的机制时,这一点并不总是很清楚。【参考方案3】:

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

链接: 在链接中,我们使用数组索引来存储值。如果第二个值的哈希码也指向同一个索引,那么我们用链表替换该索引值,所有指向该索引的值都存储在链表中,实际数组索引指向链表的头部。 但是,如果只有一个哈希码指向数组的索引,则该值直接存储在该索引中。检索值时应用相同的逻辑。这用于 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

线性探测和双散列技术是开放寻址技术的一部分,只有在可用槽数大于要添加的项目数时才能使用。它比链式占用更少的内存,因为这里没有使用额外的结构,但它很慢,因为在我们找到一个空槽之前会发生很多移动。同样在开放寻址技术中,当一个项目从一个插槽中移除时,我们放置一个墓碑来表明该项目是从这里移除的,这就是它为空的原因。

有关更多信息,请参阅this site。

【讨论】:

【参考方案4】:

我强烈建议您阅读最近出现在 HackerNews 上的这篇博文: How HashMap works in Java

简而言之,答案是

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

它们将存储在同一个存储桶中,但 没有链表的下一个节点。和钥匙 equals() 方法将用于 识别正确的键值对 哈希映射。

【讨论】:

HashMap 非常有趣而且深入! :) 我认为问题是关于 HashTables 而不是 HashMap【参考方案5】:

我在学位课程中听说过 HashTable 将在其中放置一个新条目 “下一个可用”存储桶(如果是新存储桶) 键项与另一个项发生冲突。

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

那么 HashTable 还怎么 如果这个返回正确的值 呼叫时发生碰撞 用碰撞钥匙回来?

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

如果我实现自己的哈希函数 并将其用作查找表的一部分 (即 HashMap 或字典),什么 存在应对策略 碰撞?

有各种不同的碰撞处理策略,各有优缺点。 Wikipedia's entry on hash tables 提供了很好的概述。

【讨论】:

Sun/Oracle 的 jdk 1.6.0_22 中的 HashtableHashMap 都是如此。 @Nikita:不确定 Hashtable,我现在无法访问源代码,但我 100% 确定 HashMap 在我曾经使用过的每个版本中都使用链接而不是线性探测在我的调试器中看到。 @Michael 好吧,我现在正在查看 HashMap 的 public V get(Object key) 的来源(与上面的版本相同)。如果您确实找到了这些链接列表出现的精确版本,我很想知道。 @Niki:我现在正在研究相同的方法,我看到它使用 for 循环遍历 Entry 对象的链表:localEntry = localEntry.next @Michael 对不起,这是我的错误。我以错误的方式解释代码。当然,e = e.next 不是 ++index。 +1【参考方案6】:

自 Java 8 以来的更新:Java 8 使用自平衡树进行碰撞处理,将最坏情况从 O(n) 改进为 O(log n) 以进行查找。 Java 8 中引入了自平衡树的使用,作为对链接(直到 java 7 使用)的改进,它使用链表,并且查找的最坏情况为 O(n)(因为它需要遍历列表)

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

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

冲突处理将插入和查找的最坏情况性能从 O(1) 在没有冲突处理的情况下带到 O(n) 用于链接(链表用作辅助数据结构)和 O( log n) 用于自平衡树。

参考资料:

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 中的每个对象才能找到链表的末尾,对吧?我错过了什么? 在 hashmap 实现的特定情况下,您实际上是对的,但不是因为您需要找到列表的末尾。在一般情况下链表实现中,指针存储在头和尾,因此可以通过直接将下一个节点附加到尾来在 O(1) 中完成插入,但在哈希映射的情况下,插入方法需要确保没有重复,因此必须搜索列表以检查元素是否已经存在,因此我们最终得到 O(n)。所以它是强加在链表上的集合属性导致 O(N)。我会更正我的答案:)【参考方案7】:

它会使用equals方法来查看key是否存在,尤其是在同一个bucket中有多个元素的时候。

【讨论】:

【参考方案8】:

由于对 Java 的 HashMap 使用哪种算法存在一些混淆(在 Sun/Oracle/OpenJDK 实现中),这里是相关的源代码 sn-ps(来自 OpenJDK,1.6.0_20,在 Ubuntu 上):

/**
 * 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);

这会在存储桶的前面添加新条目,并带有指向旧 first 条目的链接(如果没有,则为 null)。类似地,removeEntryForKey() 方法遍历列表并只删除一个条目,让列表的其余部分保持不变。

所以,这里是每个存储桶的链接条目列表,我非常怀疑这是否从 _20 更改为 _22,因为从 1.2 开始就是这样。

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

【讨论】:

我将其标记为 community wiki,因为它并不是真正的答案,更多的是对其他答案的讨论。在 cmets 中根本没有足够的空间来引用此类代码。【参考方案9】:

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

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));
    

【讨论】:

【参考方案10】:

解决冲突的方法有很多种。其中一些是独立链接、开放寻址、罗宾汉哈希、布谷鸟哈希等。

Java 使用分离链来解决哈希表中的冲突。这里有一个很好的链接来了解它是如何发生的: http://javapapers.com/core-java/java-hashtable/

【讨论】:

以上是关于HashTables 如何处理冲突?的主要内容,如果未能解决你的问题,请参考以下文章

如何处理列名('hash')与 Rails 冲突的旧数据库表?

Django slugify url - 如何处理冲突?

咨询师如何处理冲突?

如何处理名称冲突 collections.Counter 和 typing.Counter?

在 VC++ 中获取读取访问冲突异常如何处理此异常?

如何处理第三方组件依赖与 react native 或其他组件冲突?