红黑树在HashMap中的应用

本文介绍了红黑树在HashMap中的应用,详细分析了HashMap的查找、插入和删除操作,特别是在解决哈希冲突时如何利用红黑树优化性能,将时间复杂度从O(N)降低到O(logN)。通过源码解析,阐述了红黑树在HashMap中的插入、查找和删除节点的过程,以及树化和非树化的条件。

在上一篇文章:红黑树(Red-Black Tree)解析 中我们了解了二叉查找树以及红黑树的概念和特性,并且对查找、插入和删除操作的实现源码进行了详细的剖析。其复杂的操作流程保证了红黑树的五条特性始终能够被满足,从而使得红黑树操作的时间复杂度为O(logN)。也正因为如此,Java的很多集合框架都引入了红黑树结构以提高性能。在JDK1.8中,我们常用的HashMap也成功傍身红黑树策马奔腾,下面就让我们一起看看HashMap是如何入手红黑树的。

这里我们依然从查找,插入和删除三个常用操作来进行分析。除去这三个操作之外还有一个地方与红黑树结构密切相关–resize扩容操作,关于HashMap的扩容我们在另一篇文章中有详述,这里就不再重复,有兴趣的童鞋请戳这里(Java中集合的扩容策略及实现)

  • 相关成员变量

    首先,先介绍一下相关的成员变量

        //哈希表中的数组,JDK 1.8之前存放各个链表的表头。1.8中由于引入了红黑树,则也有可能存的是树的根
        transient Node<K,V>[] table;
    
        //树化阈值。JDK 1.8后HashMap对冲突处理做了优化,引入了红黑树。
        //当桶中元素个数大于TREEIFY_THRESHOLD时,就需要用红黑树代替链表,以提高操作效率。此值必须大于2,并建议大于8
        static final int TREEIFY_THRESHOLD = 8;
    
        //非树化阈值。在进行扩容操作时,桶中的元素可能会减少,这很好理解,因为在JDK1.7中,
        //每一个元素的位置需要通过key.hash和新的数组长度取模来重新计算,而1.8中则会直接将其分为两部分。
        //并且在1.8中,对于已经是树形的桶,会做一个split操作(具体实现下面会说),在此过程中,
        //若剩下的树元素个数少于UNTREEIFY_THRESHOLD,则需要将其非树化,重新变回链表结构。
        //此值应小于TREEIFY_THRESHOLD,且规定最大值为6
        static final int UNTREEIFY_THRESHOLD = 6;
    
        //最小树化容量。当一个桶中的元素数量大于树化阈值并请求treeifyBin操作时,
        //table的容量不得小于4 * TREEIFY_THRESHOLD,否则的话在扩容过程中易发生冲突
        static final int MIN_TREEIFY_CAPACITY = 64;
    
  • 查找

    HashMap中的查找是最常用到的API之一,调用方法为map.get(key),我们就从这个get方法看起:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    可以看到这个方法调用了两个内部方法:hash和getNode,下面依次来看这两个方法:

    //hash方法对传入的key进行了哈希值优化,具体做法为将key的哈希值h无符号右移16位之后与h本身按位异或,
    //相当于将h的高16位于低16位按位异或。这样做的原因在于一个对象的哈希值即使分布再松散,其低几位发生冲突的概率也较高,
    //而HashMap在计算index时正是用该方法的返回值与(length-1)按位与,结果就是哈希值的高位全归零,只保留低几位。
    //这样一来,此处的散列值优化就显得尤为重要,它混合了原始哈希值的高位与低位,以此来加大低位的松散性。
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key  //此处传入的就是上面hash方法的返回值,是经过优化的哈希值
     * @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;
        //上文提到的计算index的方法:(n - 1) & hash,first是这个数组table中index下标处存放的对象
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                //如果first对象匹配成功,则直接返回
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //否则就要在index指向的链表或红黑树(如果有的话)中进行查找
                if (first instanceof TreeNode)
                    //如果first节点是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;
    }

    重点来了,这里关注下TreeNode.getTreeNode(hash, key)方法,这是1.8中引入红黑树后新增的操作,它对于HashMap在哈希冲突多发,产生长链表的情况下的查找效率有着极大的提升:

    /**
    * Calls find for root node.
    */
    //定位到树的根节点,并调用其find方法
    final TreeNode<K,V> getTreeNode(int h, Object k) {
        return ((parent != null) ? root() : this).find(h, k, null);
    }
    
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
        TreeNode<K,V> p = this; //p赋值为根节点,并从根节点开始遍历
        do {
            int ph, dir; K pk;
            TreeNode<K,V> pl = p.left, pr = p.right, q;
            if ((ph = p.hash) > h) //查找的hash值h比当前节点p的hash值ph小
                p = pl; //在p的左子树中继续查找
            else if (ph < h)
                p = pr; //反之在p的右子树中继续查找
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p; //若两节点hash值相等,且节点的key也相等,则匹配成功,返回p
    
        /****---- 下面的情况是节点p的hash值和h相等,但key不匹配,需继续在p的子树中寻找 ----****/
    
            else
### JavaHashMap 使用红黑树的情况及其工作机制 #### 一、HashMap 的桶结构 在 JDK 1.8 及之后版本中,`HashMap` 的底层实现由数组和链表组成。当某个桶中的链表长度达到一定阈值时,为了提高查找效率,链表会被转换为红黑树[^1]。 具体来说,`TREEIFY_THRESHOLD` 是决定是否将链表转为红黑树的关键参数,默认值为 **8**。这意味着如果某一个桶内的节点数量超过或等于 8,则会触发 `treeifyBin` 方法,尝试将此链表转换成红黑树[^5]。 然而需要注意的是,只有当哈希表的容量大于等于 **64** 时才会执行上述操作;否则会选择扩容而不是直接构建红黑树[^3]。这是因为对于较小规模的数据集而言,扩容通常比维护复杂的树形结构更高效。 #### 二、红黑树的工作机制 ##### (1)基本概念 红黑树是一种特殊的二叉搜索树(BST),具有以下几个特性来保持动态平衡状态: - 每个结点要么是红色要么是黑色; - 根总是黑色; - 如果一个内部节点存在子节点的话那么这些孩子都不能为空白叶子(nil); - 对于任何给定路径上的所有简单路径从该顶点到后代叶之间经过相同数目黑节点的数量相等; - 不允许连续两个红色节点相邻出现即父与儿子不能同为red color. 以上性质共同作用使得整个数据结构能够在O(log n)时间内完成增删改查等各种基础运算[^2]. ##### (2)自平衡调整过程概述 每当向一棵已存在的RB Tree 插入新元素或者移除已有成员后都需要重新审视当前整体布局是否存在违反前述规则之处并采取相应措施予以修正包括但不限于旋转(spinning)以及颜色翻转(color flipping): ###### A). 插入后的修复流程可能涉及以下几种情况处理: * Case I: 新加入者作为唯一根部无需额外动作; * Case II~VII : 需要根据不同上下文环境分别应用左旋/右旋配合变色策略直至恢复合法形态为止. ###### B). 删除过程中也可能引发局部失衡现象同样需借助类似手段加以解决: 通过一系列精心设计好的算法步骤最终达成全局稳定目的同时兼顾性能表现方面的要求. #### 三、代码片段展示 以下是部分核心逻辑摘录用于辅助理解: ```java // 判断是否需要将链表转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) { treeifyBin(tab, hash); } private final void treeifyBin(Node<K,V>[] tab, int hash) { ... } ``` 此外还有关于如何初始化实例化对象等内容也可以参阅如下构造函数定义样例: ```java public HashMap(Map<? extends K, ? extends V> m){ this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m,false); } ``` 这里体现了默认装载因子设定为0.75的同时还支持批量导入外部映射关系等功能特点[^4].
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值