_156_Java_解决哈希冲突的三种方法

本文深入探讨了散列表中的两种冲突解决策略:拉链法与开放地址法。拉链法通过链表存储冲突元素,适用于不确定表长的情况,提供高效的删除操作。开放地址法则采用再寻址策略,如线性探查、二次探查和伪随机探测,避免了额外的指针开销。对比两者优劣,为实际应用选择合适策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 转自http://www.cnblogs.com/kaleidoscope/p/9588151.html,感谢作者的无私分享。

 转自https://blog.youkuaiyun.com/abm1993/article/details/80886058,感谢作者的无私分享。

 

一、拉链法

一 介绍

    拉链法又叫链地址法,Java中的HashMap在存储数据的时候就是用的拉链法来实现的,拉链发就是把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有m个散列地址就有m个链表,同时用指针数组T[0..m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值应为空指针(来自百度知道)。

二 具体实现(依据HashMap分析)

//当我们向HashMap中进行存储数据对象时,我们先根据key中,生成一个hash值
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0) /*判断当前的HashMap是否已经初始化,
这里的table,是Node<K,V>[] table数组,而Node就是一个链表实现*/
            n = (tab = resize()).length;//进行初始化
        if ((p = tab[i = (n - 1) & hash]) == null)//位运算获取key对应的数组的索引,并检查对应的数据是否存在
            tab[i] = newNode(hash, key, value, null);//当table数组对应没有结点数据时,创建新的结点,此时结点的next为null(构造函数最后一个参数),插入的数据便是链表头部。
        else {//这里说明对应节点已经至少存在一个结点数据
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//判断当前的结点是否与插入的结点为同一个节点
                e = p;//如果是看后续分析
            else if (p instanceof TreeNode)//进入红黑树逻辑,暂不重点介绍
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {//循环遍历链表数据
                    if ((e = p.next) == null) {//链表尾部,往链表插入
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) /* -1 for 1st//jdk1.8之后为了弥补之前数组+链表的实现,
                                                             在链表有n个元素时,遍历的时间复杂度O(n),
                                                           引入了红黑树(复杂度为O(logn),这里暂不细分,只着重分析拉链法的具体实现*/
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//循环寻找链表中是否存在同key的情况,存在则进入后续分析
                    p = e;//既没有达到链表尾部,又没有相同key,继续遍历链表,直到链表尾部
                }
            }
            if (e != null) { // existing mapping for key//后续分析
                V oldValue = e.value;//如果进入此判断,说明当前的key值存在值,则更新value值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
后续分析
            else if (p instanceof TreeNode)//进入红黑树逻辑,暂不重点介绍
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {//循环遍历链表数据
                    if ((e = p.next) == null) {//链表尾部,往链表插入
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) /* -1 for 1st//jdk1.8之后为了弥补之前数组+链表的实现,
                                                             在链表有n个元素时,遍历的时间复杂度O(n),
                                                           引入了红黑树(复杂度为O(logn),这里暂不细分,只着重分析拉链法的具体实现*/
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//循环寻找链表中是否存在同key的情况,存在则进入后续分析
                    p = e;//既没有达到链表尾部,又没有相同key,继续遍历链表,直到链表尾部
                }
            }
            if (e != null) { // existing mapping for key//后续分析
                V oldValue = e.value;//如果进入此判断,说明当前的key值存在值,则更新value值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

上诉HashMap的添加数据的代码逻辑,淋漓尽致的体现了拉链法的代码实现,简单来说拉链发就是数组加链表

    //通过指定的key向HashMap中获取对应的value
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 
    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {//
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode) 
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;//满足条件:HashMap还未初始化,已初始化但length为0,未在HashMap中找到指定key对应的value时返回null
    }
后续分析
            else if (p instanceof TreeNode)//进入红黑树逻辑,暂不重点介绍
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {//循环遍历链表数据
                    if ((e = p.next) == null) {//链表尾部,往链表插入
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) /* -1 for 1st//jdk1.8之后为了弥补之前数组+链表的实现,在链表有n个元素时,遍历的时间复杂度O(n),引入了红黑树(复杂度为O(logn),这里暂不细分,只着重分析拉链法的具体实现*/
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//循环寻找链表中是否存在同key的情况,存在则进入后续分析
                    p = e;//既没有达到链表尾部,又没有相同key,继续遍历链表,直到链表尾部
                }
            }
            if (e != null) { // existing mapping for key//后续分析
                V oldValue = e.value;//如果进入此判断,说明当前的key值存在值,则更新value值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

三 总结
从HashMap的实现来看,我们总结拉链发的实现步骤如下:
1. 计算 key 的 hashValue
2. 根据 hashValue 值定位到 table[hashIndex] 。( table[hashIndex] 是一条链表Node)
3. 若 table[hashValue] 为空则直接插入,不然则添加到链表末尾

 

 

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

 

拉链法的缺点

指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

使用例子:HashMap

 

二、开发地址法

开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。

有几种常用的探查序列的方法:

①线性探查

dii=1,2,3,…,m-1;这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

(使用例子:ThreadLocal里面的ThreadLocalMap)

②二次探查

di=12,-12,22,-22,…,k2,-k2    ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

③ 伪随机探测

di=伪随机数序列;具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),生成一个位随机序列,并给定一个随机数做起点,每次去加上这个伪随机数++就可以了。

 

三、再散列法

再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置

缺点:每次冲突都要重新散列,计算时间增加。

<think>好的,我现在需要回答用户关于Java HashMap如何解决哈希冲突的问题。首先,我得回忆一下HashMap的基本结构和工作原理。记得HashMap是使用数组和链表(或红黑树)实现的哈希表结构。哈希冲突指的是不同的键通过哈希函数计算后得到相同的索引,这时候就需要处理这些冲突的情况。 接下来,我得确定HashMap具体用了哪些方法解决冲突。首先想到的是链地址法(Separate Chaining),也就是每个数组元素是一个链表,当发生冲突时,将新的键值对添加到链表中。这一点是正确的,但需要确认是否还有其他的处理方式。 然后,可能还有开放定址法,但根据我的记忆,Java HashMap并没有使用开放定址法,而是采用链表法,并且在Java 8之后,当链表长度超过阈值时,会将链表转换为红黑树,以提高查找效率。这点很重要,需要详细说明。 另外,需要提到哈希函数的设计,HashMap的hash()方法如何减少冲突。例如,通过高位运算和异或操作,使得哈希值分布更均匀,从而减少冲突的可能性。同时,扩容机制(resize)也是一个关键点,当负载因子达到阈值时,数组会扩容,重新计算每个元素的位置,减少冲突的概率。 现在需要把这些点组织起来,按照用户的要求结构清晰,分步骤解释。可能需要分成几个部分,比如哈希函数优化、链地址法、红黑树转换、扩容机制等。同时要确保使用正确的中文术语,避免技术术语错误。 需要检查是否有遗漏的部分,比如处理冲突的具体过程,比如put方法中如何找到索引,如何处理链表或红黑树的插入。另外,要提到当链表长度超过8且数组长度大于等于64时,链表转红黑树,否则只是扩容数组。而当红黑树节点数小于6时,会转换回链表。这些细节需要准确描述。 还需要注意LaTeX格式的问题,用户要求行内数学表达式用$...$,比如哈希函数的索引计算可能涉及数学公式,这时候需要用正确的格式。例如,索引计算为$ (n - 1) \& hash $,这里可能需要用LaTeX表达。 最后,总结整个解决冲突的方法,确保回答覆盖了所有关键点,并且结构清晰,符合用户的要求。需要验证每个步骤的正确性,比如红黑树的转换条件,扩容的触发条件(负载因子默认0.75),以及具体的处理流程。</think>Java中的HashMap通过以下几种方法解决哈希冲突,确保高效的查找和插入性能: --- ### 一、哈希函数优化 HashMap的`hash()`方法通过两次扰动减少冲突概率: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` - **高位异或**:将哈希码的高16位与低16位异或,使低位信息更均匀。 - **索引计算**:通过$ (n - 1) \& hash $确定桶位置(`n`为数组长度)。 --- ### 二、链地址法(Separate Chaining) 当不同键的哈希值映射到同一数组位置(桶)时: 1. **链表存储**:每个桶初始化为链表,冲突的键值对以链表形式存储。 2. **插入方式**:JDK1.7使用头插法,JDK1.8改为尾插法(避免并发扩容时死循环)。 --- ### 三、红黑树优化(JDK8+) 当链表过长时,查询效率下降,HashMap自动转换数据结构: - **转换条件**: - 链表长度 ≥ 8 **且** 数组长度 ≥ 64 → 链表转为红黑树。 - 红黑树节点数 ≤ 6 → 退化为链表。 - **优势**:红黑树将查询时间复杂度从$O(n)$降低到$O(\log n)$。 --- ### 四、动态扩容(Resize) 当元素数量超过阈值(容量×负载因子,默认0.75)时触发扩容: 1. **新容量**:数组扩大为原来的2倍。 2. **重新哈希**:所有元素重新计算索引,分配到新数组的桶中。 3. **效果**:减少哈希冲突概率,平衡时间与空间开销。 --- ### 处理冲突的完整流程(以`put()`为例) 1. 计算键的哈希值,确定桶位置。 2. 若桶为空,直接插入新节点。 3. 若桶为链表/红黑树: - **链表**:遍历查找是否存在相同键,若存在则覆盖值;否则添加到链表尾部。 - **红黑树**:调用树节点的插入方法。 4. 插入后检查是否需要扩容或转换数据结构。 --- ### 总结 | 方法 | 描述 | 目的 | |-----------------|-------------------------------------|-------------------------| | 哈希函数优化 | 高位异或运算 | 使哈希分布更均匀 | | 链地址法 | 链表存储冲突元素 | 直接解决冲突 | | 红黑树转换 | 链表长度≥8时转红黑树 | 提高长链表的查询效率 | | 动态扩容 | 数组翻倍并重新哈希 | 降低后续操作冲突概率 | 这些机制共同保证了HashMap在$O(1)$时间复杂度下高效处理哈希冲突
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值