HashMap底层红黑树原理(超详细图解)+手写红黑树代码

Posted 温文艾尔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap底层红黑树原理(超详细图解)+手写红黑树代码相关的知识,希望对你有一定的参考价值。

在看完了小刘老师和黑马的源码视频之后,我整理了一篇HashMap的底层源码文章,学海无涯,这几天看了对红黑树的讲解,故将其整理出来

HashMap底层源码解析上
HashMap底层源码解析下

视频链接
小刘老师讲源码
传智播客-黑马程序员


树结构回顾

什么是树

树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合,它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为他看起来像一颗倒挂的树,也就是说它是根朝上,而叶向下的。

树有很多种,向上面的一个节点有多余两个的子节点的树,称为多路树,而每个节点最多只能有两个子节点的一种形式称为二叉树

  • 节点:上图的圆圈,比如A,B,C等都是表示节点,节点一般代表一些实体,在java面向对象编程中,节点一般代表对象
  • 边:连接节点的线称为边,边表示节点的关联关系,一般从一个节点到另一个节点的唯一方法就是沿着一条顺着有边的道路前进,在java当中通常表示引用

树结构的常用术语

  • 路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为‘路径’

  • 根:树顶端的节点称为根,一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径,A是根节点。

  • 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点

  • 子节点:一个节点含有的子树的节点称为该节点的子节点,F,G是C的子节点

  • 兄弟节点:具有相同父节点的节点互称为兄弟节点,F,G互为兄弟节点

  • 叶节点:没有子节点的节点称为叶节点,也叫叶子结点,比如上图的H,E,F,G都是叶子结点

  • 子树:每个节点都可以作为子树的根,它和它所有的子节点,子节点的子节点等都包含在子树中

  • 节点的层次:从根开始定义,根为第一层,根的子节点为第二层,以此类推。

  • 深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0(从上往下看)

  • 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;(从下往上看)

二叉树

树的每个节点最多只能有两个子节点

上图的第一幅图B节点有DEF三个子节点,就不是二叉树,称为多路树

而第二幅图每个节点最多只有两个节点,是二叉树,并且二叉树的子节点称为“左子节点”和“右子节点”

二叉搜索树

如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树的特殊二叉树

二叉搜索树要求:

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值

  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值

  • 它的左、右子树也分别为二叉排序树

二叉搜索树-查找节点

查找某个节点,我们必须从根节点开始查找

  • 查找值比当前节点值大,则搜索右子树
  • 查找值等于当前节点值,停止搜索(终止条件)
  • 查找值小于当前节点值,则搜索左子树

二叉搜索树-插入节点

  • 要插入节点,必须先找到插入的位置,与查找操作相似,由于二叉搜索树的特殊性

  • 待插入节点也需要从根节点开始进行比较,小于根节点则与根节点左子树比较

  • 反之则与右子树比较,直到左子树为空或右子树为空,则插入到相应为空的位置

二叉搜索树-遍历节点

遍历树是根据一种特定的顺序访问树的每一个节点,比较常用的有前序遍历,中序遍历和后序遍历。而二叉搜索树最常用的是中序遍历

  • 中序遍历:左子树->根节点->右子树
  • 前序遍历:根节点->左子树->右子树
  • 后序遍历:左子树->右子树->根节点

二叉搜索树-查找最大值和最小值

要找最小值,先找根的左节点,然后一直找这个左节点的左节点,直到没有左节点,那么这个节点就是最小值,同理要找最大值,一直找根节点的右节点,直到没有右节点,则就是最大值

二叉搜索树-删除节点

删除节点是二叉搜索树中最复杂的操作,删除的节点有三种情况,前两种比较简单,但是第三种却很复杂

  • 该节点是叶节点(没有子节点)
  • 该节点有一个子节点
  • 该节点有两个子节点

①删除没有子节点的节点

​ 要删除叶节点,只需要改变该节点的父节点引用该节点的值,即将其引用改为null即可


②删除有一个子节点的节点

删除有一个子节点的节点,我们只需要将其父节点原本指向该节点的引用,改为指向该节点的子节点即可


③删除有两个子节点的节点

当删除的节点存在两个子节点,那么删除之后,两个子节点的位置我们就没办法处理了,既然处理不了,我们就想到一种办法,用另一个节点来代替被删除的节点,那么用哪一个节点来代替呢?

我们直到二叉搜索树中的节点是按照关键字来进行排列的,某个节点的关键字次高节点是它的中序遍历后继节点。
用后继节点来代替删除的节点,显然该二叉树还是有序的

那么如何找到删除节点的中序后继节点呢?
其实我们稍微分析,这实际上就是要找比删除节点关键值大的节点集合中最小的节点,只有这样代替删除节点后才能满足二叉搜索树的特性

④删除有必要吗?

通过上面的删除分类讨论,我们发现删除其实是挺复杂的,那么其实我们可以不用真正的删除该节点,只需要在Node类中增加一个标识字段isDelete,当该字段为true,标识该节点已经删除,反之则没有删除,这样删除节点就不会改变树的结构了。

影响就是查询时需要判断一下节点是否删除

二叉搜索树-时间复杂度分析:

二分查找算法时间复杂度推算过程

从上表可以看出,N(2*k)肯定是大于等于1,也就是N/(2^K)>=1,我们计算时间复杂度是按照最坏的情况进行计算,也就是查到剩余最后一个数才查到我们想要的数据,也就是

N/(2^k)=1 => 2^K = N => K = log2(N) =>二分查找算法时间复杂度:O(log2(N))=>O(logN)

普通二叉搜索树的致命缺陷


这颗二叉搜索树的时间复杂度为O(N)

可以看到这颗二叉搜索树的左子树实际上已经成为了链表

怎么解决二叉搜索树退化成线性链表的问题?
如果插入元素时,树可以自动调整两边平衡,会保持不错的查找性能

AVL树简介

AVL树有什么特点?

  1. 具有二叉搜索树的全部特性
  2. 每个节点的左子树和右子树的高度差至多等于1

平衡树基于这种特点就可以保证不会出现大量节点偏向于一边的情况了(插入或删除时,会发生左旋,右旋操作,使这棵树再次左右保持一定的平衡)

为什么有了二叉平衡树还需要红黑树

虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在O(logn),不过却不是最佳的

因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋右旋来进行调整,使之再次称为一颗符合要求的平衡树

显然,如果在那种插入删除很频繁的场景中,平衡树需要频繁的进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,便有了红黑树

红黑树原理

红黑树的性质


红黑树属于二叉搜索树的一个特殊分支

红黑树并不是一个完美平衡二叉查找树,从图上可以看到,根节点P的左子树显然比右子树高

但左子树和右子树的黑结点的层数是相等的,也即任意一个节点到每个叶子结点的路径都包含数量相同的黑结点

所以我们叫红黑树这种平衡为黑色完美平衡

前面讲到红黑树能自平衡,它靠的是什么?

三种操作:左旋右旋变色

  • 变色:结点的颜色由红变黑或由黑变红
  • 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子树变为旋转结点的右子树,右子结点的右子树保持不变
  • 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子树变为旋转结点的左子树,左子结点的左子树保持不变

左旋图示:


右旋图示:

红黑树查找

因为红黑树是一颗二叉平衡树,而且查找不会破坏树的平衡,所以查找跟二叉平衡树无异

例如在此红黑树中查找指定元素:

查找50

50<80,找到左子结点40

50>40,找到40的右子结点60

50<60,找到60的左子结点50

红黑树插入

插入操作包括两部分工作

  1. 查找插入的位置
  2. 插入后自平衡

注意:插入结点,必须为红色,理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作,但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,破坏了红黑树的黑高性质,必须做自平衡。

在开始每个情景的讲解前,我们还是先来约定下

红黑树插入节点情景分析

情景1:红黑树为空树

最简单的一种情景,直接把插入结点作为根节点就行

注意,根据红黑树性质2,:根节点是黑色。还需要把插入结点设为黑色

情景2:插入结点的Key已存在

处理:更新当前节点的值,为插入结点的值

情景3:插入结点的父结点为黑结点

由于插入的结点是红色的,当插入结点的父结点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡

情景4:插入结点的父结点为红色

再次回想下红黑树的性质2:根节点是黑色,如果插入结点的父结点为红结点,那么该父结点不可能为根节点,所以插入结点总是存在祖父结点,这一点很关键,因为后续的旋转操作肯定需要祖父结点的参与

插入场景4.1:叔叔结点存在并且为红结点

依据红黑树性质4可知。红色结点不能相连=>祖父结点肯定为黑结点

因为不可以同时存在两个相连的红结点,那么此时该插入子树的红黑树层数的情况是:黑红红,显然最简单的处理方式是把其改为红黑红处理:

  1. 将P和U结点改为黑色
  2. 将PP改为红色
  3. 将PP设置为当前结点,继续后续处理

    可以看到,我们把PP节点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;
    但如果PP的父结点是红色,则违反红黑树性质了,所以需要将PP设置为当前结点,继续做插入操作自平衡处理,直到平衡为止
插入场景4.2:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的左子节点

注意:单纯从插入前来看,叔叔结点非红即空(NIL结点),否则的话破坏了红黑树性质5,此路径会比其他路径多一个黑色结点。

插入场景4.2.1:新插入结点,为其父结点的左子结点(LL红色情况)

处理:

  1. 变颜色:将P设置为黑色,将PP设置为红色
  2. 对PP节点进行右旋

情景4.2.2:新插入结点,为其父结点的右子结点(LR红色情况)


处理:

  1. 对P进行左旋
  2. 将P设置为当前结点,得到LL红色情况
  3. 按照LL红色情况处理(1.变颜色2.右旋PP)

情景4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点

该情景对应情景4.2,只是方向反转,直接看图

插入场景4.3.1:新插入结点,为其父结点的右子结点(RR红色情况)


处理:

  1. 变颜色:将P设置为黑色,将PP设置为红色
  2. 对PP结点进行左旋
插入场景4.3.2:新插入结点,为其父结点的左子结点(RL红色情况)


处理:

  1. 对P进行右旋
  2. 将P设置为当前结点,得到RR红色情况
  3. 按照RR红色情况处理(1.变颜色2.左旋PP)


注:上图第一步图片内容错误,应该为右旋

下面我们具体分析一个案例

  1. 要求,在红黑树中插入结点7
  2. 找到插入位置,7<19,7>5,7<15,7<8,故7应该放在8的左子结点
  3. 7的父结点和叔叔结点都为红色,且78为双红,违反了性质4,属于上面提到的插入情景4.1
  4. 将8,18结点变黑,将15结点变红,将当前结点设置为15
  5. 此时5,15为红色,出现双红,违反了性质4,,但和我们提到的情景4.2.2类似,以5结点进行左旋
  6. 左旋之后,5与15结点为双红,符合情景4.2.1,故父结点变为黑色,祖父结点变为红色,变色完毕以根结点19为基准,进行右旋

手写红黑树

  • 创建RBTree,定义颜色
  • 创建RBNode
  • 辅助方法定义:parentOf(node),isRed(node),isBlack(node),setRed(node),setBlack(node),InOrderPrint()
  • 左旋方法定义leftRotate(node)
  • 右旋方法定义rightRotate(node)
  • 公开插入接口方法定义:insert(K key,V value)
  • 内部插入接口方法定义:insert(RBNode node)
  • 修正插入导致红黑树失衡的方法定义:insertFlxUp(RBNode node)
  • 测试红黑树正确性

手写红黑树代码

package HashMapProject;

import com.sun.org.apache.regexp.internal.RE;
import org.omg.CORBA.NO_IMPLEMENT;

/**
 * Description
 * User:
 * Date:
 * Time:
 * - 创建RBTree,定义颜色
 * - 创建RBNode
 * - 辅助方法定义:parentOf(node),isRed(node),isBlack(node),setRed(node),setBlack(node),InOrderPrint()
 * - 左旋方法定义leftRotate(node)
 * - 右旋方法定义rightRotate(node)
 * - 公开插入接口方法定义:insert(K key,V value)
 * - 内部插入接口方法定义:insert(RBNode node)
 * - 修正插入导致红黑树失衡的方法定义:insertFlxUp(RBNode node)
 * - 测试红黑树正确性
 */
//RBTree中node节点里面存的就是key和value
public class RBTree<K extends Comparable<K>, V> 
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    /** 树根的引用 */
    private RBNode root;

    public RBNode getRoot() 
        return root;
    

    /**
     * 获取当前结点的父结点
     * @param node
     * @return
     */
    private RBNode parentOf(RBNode node)
        if (node!=null)
            return node.parent;
        
        return null;
    

    /**
     * 结点是否为红色
     * @param node
     * @return
     */
    private boolean isRed(RBNode node)
        return node!=null&&node.color == RED;
    

    /**
     *结点是否为黑色
     * @param node
     * @return
     */
    private boolean isBlack(RBNode node)
        return node!=null&&node.color==BLACK;
    

    /**
     * 将当前结点颜色设置为红色
     * @param node
     */
    private void setRed(RBNode node)
        if (node!=null)
            node.color=RED;
        
    

    /**
     * 将当前结点颜色设置为黑色
     * @param node
     */
    private void setBlack(RBNode node)
        if (node!=null)
            node.color=BLACK;
        
    

    public void InOrderPrint()
        inOrderPrint(this.root);
    

    /**
     * 中序打印,左中右
     * @param node
     */
    private void inOrderPrint(RBNode node)
        if (node!=null)
            inOrderPrint(node.left);
            System.out.println("key:"+node.key+",value:"+node.value);
            inOrderPrint(node.right);
        
    

    /**
     * 公开的插入方法
     * @param key
     * @param value
     */
    public void insert (K key,V value)
        RBNode node = new RBNode();
        //新节点一定是红色,为了不违反黑高性质
        node.setColor(RED);
        node.setKey(key);
        node.setValue(value);
        insert(node);
    

    private void insert(RBNode node)
        //第一步:查找当前node的父节点
        RBNode parent = null;
        RBNode x = this.root;
        while (x!=null)
            parent = x;
            //cmp>0 说明node.key大于x.key,需要到x的右子树寻找
            //cmp=0 说明node.key等于x.key,需要进行替换操作
            //cmp<0 说明node.key小于x.key,需要到x的左子树寻找
            int cmp = node.key.compareTo(x.key);
            if (cmp>0)
                x=x.right;
            else if (cmp<0)
                x=x.left;
            else 
                x.setValue(node.getValue());
                return;
            
        
        node.parent=parent;
        //判断node与parent的key谁大,以此来判断是在parent的左子树还是右子树
        //防止红黑树时空的,这样parent就为空,所以这里if判断一下
        if (parent!=null)
            int cmp = node.key.compareTo(parent.key);
            if (cmp>0)//当前node的key比parent的key大,需要把node放入parent的右子节点
                parent.right=node;
            else 
                //没有等于0的情况,等于0的情况在while中已经做了替换
                //当前node的key比parent的key小,需要把node放入parent的左子节点
                parent.left=node;
            
        else 
            //红黑树为空,则node直接为root节点
            this.root=node;
        

        //需要调用修复红黑树平衡的方法
        insertFlxUp(node);
    
    /**插入后修复红黑树平衡的方法
     * |---情景1:红黑树为空树
     * |---情景2:插入结点的key已经存在,不需要处理,已经在插入时做了替换
     * |---情景3:插入结点的父节点为黑色,因为你所插入的路径,黑色结点没有变化,所以红黑树依然平衡,所以不需要处理
     *
     * 真正需要处理的是情景4的所属情况
     * |---情景4:插入结点的父节点为红色
     *     |---情景4.1:叔叔结点存在,并且为红色(父-叔 双红)
     *          将父亲结点和叔叔结点染成黑色,将祖父结点染成红色,并设置祖父结点为当前结点
     *     |---情景4.2:叔叔结点不存在,或者为黑色,父结点为爷爷结点的左子树
     *          |---情景4.2.1:插入结点为其父结点的左子结点(LL情况)
     *              将父亲结点染成黑色,将祖父结点染成红色进行右旋
     *          |---情景4.2.2:插入结点为其父结点的右子结点(LR情况)
     *              以父亲结点进行左旋,按照4.2.1情景处理
     *     |---情景4.3:叔叔结点不存在,或者为黑色,父结点为爷爷结点的右子树
     *          |---情景4.3.1:插入结点为其父结点的左子结点(RL情况)
     *              以父亲结点进行右旋,按4.3.2情景处理
     *          |---情景4.3.2:插入结点为其父结点的右子结点(RR情况)
     *              将父亲结点染成黑色,将祖父结点染成红色,进行左旋
     */
    private void insertFlxUp(RBNode node)
        this.root.setColor(BLACK);

        //拿到父亲结点
        RBNode parent = parentOf(node);
        RBNode gparent = parentOf(parent);
        //父节点是红色
        if (parent!=null&&isRed(parent))
            //进入情景4
            RBNode uncle = null;
            if (parent==gparent.left)
                uncle=gparent.right;
                if (uncle!=null&&isRed(uncle))
                    //情景4.1
//                    将父亲结点和叔叔结点染成黑色,将祖父结点染成红色,并设置祖父结点为当前结点
                    setBlack(parent);
                    setBlack(uncle);
                    setRed(gparent);
                    insertFlxUp(gparent);
                    return;
                
                if (uncle==null || isBlack(uncle))
                    //情景4.2
                    if (node==parent.left)
                        //情景4.2.1(LL)
//                        将父亲结点染成黑色,将祖父结点染成红色进行右旋
                        setBlack(parent);
                        setRed(gparent);
                        rightRotate(gparent);
                        return;
                    
                    if (node==parent.right)
                        //情景4.2.2(LR)
//                        以父亲结点进行左旋,按照4.2.1情景处理
                        leftRotate(parent);
                        insertFlxUp(parent);
                        return;
                    
                
            else 
                uncle=gparent.left;

                if (uncle!=null&&isRed(uncle))
                    //情景4.1
//                    将父亲结点和叔叔结点染成黑色,将祖父结点染成红色,并设置祖父结点为当前结点
                    setBlack(parent);
                    setBlack(uncle);
                    setRed(gparent);
                    insertFlxUp(gparent);
                    return;
                
                //情景4.3
                if (node==parent.left)
                    //情景4.3.1(RL)
//                    以父亲结点进行右旋,按4.3.2情景处理
                    rightRotate(parent);
                    insertFlxUp(parent);
                    return;
                
                if (node==parent.right)
                    //情景4.3.2(RR)
//                    将父亲结点染成黑色,将祖父结点染成红色,进行左旋
                    setBlack(parent);
                    setRed(gparent);
                    leftRotate(gparent);
                    return;
                
            

        
    

    /**
     * 左旋都做了哪些事
     *       p                   p
     *      |                   |
     *      x                   y
     *     / \\      ---->      / \\
     *    lx  y               x   ry
     *       / \\             / \\
     *      ly  ry          lx  ly
     *   1.将y的左子结点的父结点更新为x,将x的右子节点更新为y的左子结点
     *   2.当x的父节点不为空时,更新y的父节点为x的父节点,并将x的父节点的指定子树(当前x的子树位置指定为y
     *   3.将x的父节点更新为y,将y的左子结点更新为x
     */
    public void leftRotate(RBNode x)
        RBNode y = x.right;
//     1.将y的左子结点的父结点更新为x,将x的右子节点更新为y的左子结点
        x.right=y.left;
        if (y.left!=null)
            y.left.parent=x;
        
//     2.当x的父节点不为空时,更新y的父节点为x的父节点,并将x的父节点的指定子树(当前x的子树位置指定为y
        if (x.parent!=null)
            y.parent=x.parent;
            if (x == x.parent.left)
                x.parent.left=y;
            else 
                x.parent.

以上是关于HashMap底层红黑树原理(超详细图解)+手写红黑树代码的主要内容,如果未能解决你的问题,请参考以下文章

图解集合 7 : 红黑树概念红黑树的插入及旋转操作详细解读

图解集合红黑树概念红黑树的插入及旋转操作详细解读

图解集合7:红黑树概念红黑树的插入及旋转操作详细解读

硬核图解红黑树并手写实现

面试让我手写红黑树?!

面试让我手写红黑树?!