红黑树

本文深入探讨JDK1.8中HashMap使用的红黑树数据结构,讲解红黑树的意义、规则及平衡操作,通过实例展示插入元素时的树变化,包括左旋和右旋操作。

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

JDK1.8HashMap之红黑树学习

红黑树出现的意义

一般的二叉查找树理想情况下时间复杂度是O(lgN),但是有退化成链表的时候,这时的二叉查找树的时间复杂度可变为O(N).所以出现了B-树,B+树,AVL树,红黑树等这类型需要维护树的平衡甚至是树的高度的数据结构,我们本篇文章主要是讲红黑树。

红黑树的定义:

1.红黑树中的节点非黑即红。
2.根节点是黑色的。
3.从根节点出发到叶子节点的路径中,不能出现两个连续的红点,反之不一定。
4.从根节点出发到叶子节点的所有路径中,黑色节点的个数相等。
5.新插入的节点颜色是红色的。

JDK 1.8中HashMap的红黑色树平衡操作

本文将以往红黑树插入元素的一个真实过程展示红黑树的变化,并结合这个过程一步步分析源码的流程。

插入100,是根节点,涂黑返回。图一:
在这里插入图片描述

插入41,200,因为父节点100是黑色的,无需任何平衡操作,都是返回。图二:
在这里插入图片描述
插入40,父亲节点41和叔叔节点200都是红色的,只需将父亲节点和叔叔节点涂黑,将爷爷节点100涂红,将爷爷设置为当前节点x,进入下一轮循环,因为爷爷节点是根节点,所以下一循环中又被涂黑了,结束。图三:
在这里插入图片描述
插入60,父节点41是黑色,无需平衡操作,返回。图四:
在这里插入图片描述
插入50,父节点60和叔叔节点40都是红色,只需将父节点和叔叔节点涂黑,爷爷节点41涂红设置为当前节点x,进入下一轮循环,因41的父节点是黑色的,返回结束。图五:

在这里插入图片描述
插入70,父节点60是黑色,无需平衡操作,返回。图六:
在这里插入图片描述
插入79,父节点70和叔叔节点50都是红色,将父节点和叔叔节点涂黑,爷爷节点60涂红,并设置为60当前节点x,进入下一轮循环,未结束。图七:
在这里插入图片描述
当前节点x=60,它的父节点xp=41不是根节点,也不是黑色的(违背了规则3不能连续出现两个红点),且xp=41是它父亲的左孩子,当前节点x=60是它父节点41的右孩子并且叔叔节点200不是红色的,这时候要以父节点41为支点进行左旋操作,未结束。图八:
在这里插入图片描述
经过左旋之后,若爷爷节点存在,都要紧接着以当前节点的爷爷节点为支点做一次右旋操作,当前节点x=41,父节点xp=60,爷爷节点xpp=100,右旋前需要将xp=60涂黑,xpp=100涂红,以xpp为支点,进行右旋,结束。图九:
在这里插入图片描述

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    //默认插入的节点颜色是红色的                                        	
    x.red = true;
    //无限循环直到return根节点
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
    	//如果是根节点的插入,直接涂黑返回(图一)
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        //如果插入节点的父节点是黑色的,无需做任何操作,直接返回根节点;(图二)
        //或者父节点是红色的,给xpp这个引用赋值为爷爷节点,这里写得有点迷惑性
        //父节点为红色的了,怎么可能还会是根节点?难道只是为了为xpp赋值?
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
         //如果父节点是爷爷的左孩子(图三)
        if (xp == (xppl = xpp.left)) {
            //如果叔叔节点不为空且是红色的,只需将父节点和叔叔节点涂黑,爷爷节点涂红,将当前节点的引用指向爷爷节点,继续下一循环的平衡操作
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                //走到这里说明叔叔节点为空或者不为红色。
                //且当前节点是父节点的右孩子
                if (x == xp.right) {
                    //需要进行左旋(图八左旋)
                    root = rotateLeft(root, x = xp);
                    //左旋完之后给爷爷节点xpp,父节点xp重新赋值
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                //如果父节点不为空(图九)
                if (xp != null) {
                    //涂黑
                    xp.red = false;
                    if (xpp != null) {
                        //涂红
                        xpp.red = true;
                        //右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        else {
            //走到这里说明父节点是爷爷节点的右孩子
            if (xppl != null && xppl.red) {
                //同理,如果左叔叔不为空并且是红色的,将父节点和叔叔节点涂黑,爷爷节点涂红,并将当前节点指向爷爷节点
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
            	//走到这里说明左叔叔为空或者节点颜色不为红色。且如果当前节点是父节点的左孩子
                if (x == xp.left) {
                    //右旋
                    root = rotateRight(root, x = xp);
                    //左旋完之后给爷爷节点xpp,父节点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);
                    }
                }
            }
        }
    }
}

左旋和右旋

左旋和右旋是一个相对称的操作,我们使用左旋来进行分析:

/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                      TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    //p是旋转的支点,也就是r的父节点,当旋转的时候p变成r的左孩子,r变成p的父亲,左旋前,如果r的孩子存在,旋转后因为p变成了r的左孩子,所以要将r之前的左孩子作为p的右孩子。
    if (p != null && (r = p.right) != null) {
    	//将rl,p.right指向r的左孩子
        if ((rl = p.right = r.left) != null)
            //如果r的左孩子不为空,将它的父亲指向p
            rl.parent = p;
        //将r的父亲指向p的父亲,同时赋值给pp
        if ((pp = r.parent = p.parent) == null)
            //如果p刚好是根节点,涂黑,r变成根节点
            (root = r).red = false;
        //走到这里说明p不是根节点,如果p旋转前是父亲的左孩子,那么r将取代p变成左孩子
        else if (pp.left == p)
            pp.left = r;
        else//否则r取代p变成右孩子
            pp.right = r;
        //将p作为r的左孩子
        r.left = p;
        //将r作为p的父亲
        p.parent = r;
    }
    return root;
}

总结:

1.插入的节点是第一个节点,即是根节点,涂黑返回。
2.当前插入节点的父节点是黑色的,无需做任何改变。
3.当前插入节点的父节点是红色的,且是爷爷节点的左孩子,(1)如果存在叔叔节点且是红色的,将父节点和叔叔节点涂黑,将爷爷节点涂红,并将爷爷节点设置为当前节点。(2)如果不存在叔叔节点或者叔叔节点是黑色的,且当前节点是父节点的右孩子,先进行左旋,再进行右旋。否则是父节点的左孩子直接右旋(上面的图示包含了这个过程,第4点虽然没有包含,但也是同样的道理)
4.当插入节点的父节点是红色的,且是爷爷节点的右孩子,(1)如果存在叔叔节点且是红色的,将父节点和叔叔节点涂黑,将爷爷节点涂红,并将爷爷节点设置为当前节点。(2)如果不存在叔叔节点或者叔叔节点是黑色的,且当前节点是父节点的左孩子,先进行右旋,再进行左旋。否则是父节点的右孩子直接左旋。和3是相对称的操作。

附上一个数据结构可视化的网站链接。
链接: link.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值