基于JDK8 HashMap源码——肝翻红黑树(1)

1、 红黑树定义

红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或者是BLACK。通过对任何一条从根到叶子的简单路径上各个结点颜色的约束,红黑树确保没有一条路径会比其他路径长2倍,因此是近似于平衡的。

树中每个结点包含5个属性:color、key、left、right、parent。如果一个节点没有子节点或父节点,则该节点相应指针属性设置为NIL(NULL)。红黑树的性质有:

  • 每个节点要么是红色,要么是黑色;

  • 根节点是黑色的;

  • 每个叶节点(NIL)是黑色的;

  • 对每个结点,从该节点到其所有后代叶结点到简单路径上,均包含相同数目的黑色结点;

基于红黑树的定义,可以定义红黑树的成员变量:

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left; //左孩子
        TreeNode<K,V> right; //右孩子
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red; //是红节点
    }

从定义中可以看出,定义的红黑树的链表结构为双向链表。

2、左旋与右旋

为了维护红黑树的性质,需要对树的结构和颜色进行改变。改变树的结构的常见操作为:左旋和右旋,这也是平衡二叉树的常见操作。当某个节点x做左旋时,假设它的右孩子为y(不为NIL),左旋以x到y的链为“支轴”进行,x成为y的左子树,y原来的左子树成为x的右子树。具体左旋和右旋的示意图如下:

 

下面看一下HashMap中如何实现红黑树的左旋和右旋。

2.1 左旋

左旋的源代码如下:

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                      TreeNode<K,V> p) {
    /**
     * pp:p的父节点
     * r:p的右结点
     * rl:r的左结点
     */
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) {    
        // 场景1
        if ((rl = p.right = r.left) != null)    
            rl.parent = p;
        // 场景2
        if ((pp = r.parent = p.parent) == null)  
            (root = r).red = false;
        else if (pp.left == p)                   
            pp.left = r;
        else
            pp.right = r;                        
        r.left = p;
        p.parent = r;
    }
    return root;
}

左旋子树的初始结构为,其中假设图中结点都不为NIL:

1)第1个条件时判断p和r是不是为NIL,如果为空则不需要旋转;否则可以进行旋转。

2)如果进行旋转,首先将p的右孩子指向rl;再判断rl是否为NIL,因为空节点没有父亲结点,如果不为NIL,则将rl的父节点设置为p。

3)将r的父节点指向p的父节点pp,然后针对pp进行判断:

pp如果为空,则p为根节点,就必须将p的颜色设置为黑色;

pp如果不为空,并且p是pp的左孩子,则将pp的左孩子设置为r;

pp如果不为空,并且p是pp的右孩子,则将pp的右孩子设置为r;

最后将r的左孩子设置为p,p的父节点设置为r。以p为pp的左孩子为例,具体示意图如下:

整体步骤总结来说,先将r的左子树关联到p,再建立pp与r的关系,最后建立p和r的关系。

2.2 右旋

右旋的源码如下:

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                       TreeNode<K,V> p) {
    /**
     * pp:p的父节点
     * l:p的左结点
     * lr:l的右结点
     */
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) {
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        l.right = p;
        p.parent = l;
    }
    return root;
}

右旋的步骤与左旋很相似,大体旋转步骤如下:

3、红黑树的插入

红黑树的节点插入的时间复杂度为O(lgn)。插入的主要步骤为:首先遍历树找到插入的位置,然后将其着色为红色,最后为了保持红黑树的性质,需要调用辅助函数对树进行着色和旋转。

在阅读源码前,先通过构造一个值为1~8的红黑树,构造步骤如下:

HashMap中插入的源码如下:

        final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;    //记录是否查询到结点
            // 判断父亲结点是否为null,如果parent不为null,则调用root()方法查找root结点,否则当前结点设置为root
            TreeNode<K,V> root = (parent != null) ? root() : this;
            // 遍历红黑树
            for (TreeNode<K,V> p = root;;) {
                /**
                 * dir:记录选择左子树还是右子树
                 * ph:p结点的hash值
                 * pk:p结点的key值
                 */
                int dir, ph; K pk;
                // x结点的hash值小于当前树结点p的hash值:dir=-1,则放在左子树;
                if ((ph = p.hash) > h)
                    dir = -1;
                // x结点的hash值大于当前树结点p的hash值:dir=1,则放在右子树
                else if (ph < h)
                    dir = 1;
                // x结点的hash值相等,pk等于插入结点的key或者key不为null且k等于pk,则相当于插入的结点已经存在红黑树中,直接返回当前结点
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                // HashMap相关,略去
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }
​
​
                TreeNode<K,V> xp = p;
                /**
                 * 当dir<=0并且p=p.left,当dir>0时,p=p.right
                 * 当p==null时,则插入,当p!=null,继续查找结点
                 */
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                    // 生成新的TreeNode
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    // 新增结点,设置相关属性
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    xp.next = x;
                    x.parent = x.prev = xp;
​
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    // 先调用辅助函数balanceInsertion维护红黑树的性质,moveRootToFront是维护HashMap桶的性质,暂不考虑
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

从源码中可以看出insert函数的主要流程为:

  • 首先获取根节点。如果当前结点的parent不为空,则调用root()函数获取根节点,否则将当前对象设置为根节点;

  • 遍历红黑树,分别将插入结点的值与遍历的当前结点的值进行比较,如果小于当前结点的值,则遍历左子树,否则遍历右子树。当前结点的左子树或右子树为空时则插入该结点;

  • 插入结点后调用balanceInsertion()函数维护红黑树的性质。

balanceInsertion()函数的源码为:

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    x.red = true;
    /**
     * xp为x的父节点
     * xpp为x的祖父结点
     * xppl为祖父结点的左结点,即为x的叔叔结点
     * xppr为祖父结点的右结点,即为x的叔叔结点
     */
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        // 新结点X的的父节点为null,则将颜色变为黑色即可;
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // x结点的父节点是黑色或者祖父结点为空,直接返回
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // X父结点为祖父结点的左子树
        if (xp == (xppl = xpp.left)) {
            // X的叔叔结点为红色,叔叔结点和父亲结点设置为黑色,x的祖父结点设置为红色。并将x设置为xpp进行迭代遍历
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            // x的叔叔结点为空或者叔叔结点为黑色,需要进行旋转和变色
           else {
                // x是父节点的右子树
                if (x == xp.right) {
                    // 将xp、x子树左旋
                    root = rotateLeft(root, x = xp);
                    // 如果x的父亲结点是null,则x的祖父结点也为null
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 变色+右旋
                if (xp != null) {
                    // x的父亲结点设置为黑色,
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        //将xpp子树右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // X父节点为祖父结点的右子树
        else {
            // 叔叔结点不为空并且为红色,只进行变色
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                // x是xp的左子树,则要先进行右旋
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 进行左旋变色
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

源码中分为几种场景:

1)场景1:x父节点空,只需要变色:红变黑

2)场景2:x的父节点是黑色或x的祖父结点是NULL

3)场景3:X父结点为祖父结点的左子树

  • 场景3.1:叔叔结点是红色:父节点和叔叔结点变黑色,祖父结点变红色;

  • 场景3.2:叔叔结点是黑色或者为空,并且x为右子树:左旋将其转换为场景3.3

  • 场景3.3:叔叔结点是黑色或者为空,并且x为左子树:先右旋,再将x的父亲结点变为黑色,x的祖父结点变为红色,

4)场景4:X父结点为祖父结点的右子树

场景4.1:叔叔结点是红色:父节点和叔叔结点变黑色,祖父结点变红色;

场景4.2:叔叔结点是黑色或者为空,并且x为左子树:右旋将其转换为场景4.3;

场景4.3:叔叔结点是黑色或者为空,并且x为右子树:先左旋,再将x的父亲结点变为黑色,祖父结点变为红色。

具体示意图如下:

场景4与场景3类似,所以在示意图中未展示出来。因为一棵有n个结点的红黑树的高度为O(lgn),因此结点遍历的要花费O(lgn)时间。当x的叔叔结点是红色时将x的祖父结点变为红色,所以会触发循环,循环的次数为O(lgn)。而旋转操作最多执行2次并且不会出发循环,所以插入的时间复杂度为O(lgn)。

因为红黑树的删除比较复杂,所以另开一篇。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值