基于234树手撕TreeMap红黑树源码(3万字长文带你走进红黑深处的细节)

Posted 「 25' h 」

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于234树手撕TreeMap红黑树源码(3万字长文带你走进红黑深处的细节)相关的知识,希望对你有一定的参考价值。

红黑树测试连接
234树测试链接

红黑树优势到底在哪

红黑树的查询性能略微逊色于AVL树,因为红黑树比AVL树会稍微不平衡,造成多出一层来,也就是说红黑树的查询性能只比相同内容的avl树最多多出一次的比较。但是:红黑树在插入和删除上基本可以完虐AVL树,AVL树每次插入删除会进行大量的平衡度计算,而对于红黑树依照维持红黑性质所做的红黑变换和旋转操作所需要的开销相较于AVL树要小得多。红黑树最多使用三次旋转能完成所有的添加和删除操作。

红黑树和234树的映射关系

什么是234B

234树特点:

  • 严格的平衡树
  • 在每个节点上没有子节点或者有且仅有与之对应的子节点数量
  • 每一个叶子节点对应的树的层数是一定的
  • 遵循向上生长的原则

234节点的对应

每一个蓝色底盘就是一个234树的一个节点,每个节点中的每个橙色就是一个节点中的元素。在填充234树时要么没有子节点要么将节点中的每个黑色线均填充子节点,否则就不满足B树平衡.

234树的生长

  1. 添加元素时会进行查找到合适的位置,放在适合的节点
  2. 若加上该元素后该节点中有了四个元素(5节点式),就要将原来的节点中三个元素中间的元素作为父节点上提,并将对于原节点左右元素分别作为父节点左右子节点
  3. 对于上提的父节点与原节点父节点元素结合,若结合后是四个元素,重复步骤 2

234树的删除

  1. 若在节点中删除指定元素并不改变树的结构会直接删除,后不做其他操作(下图第一步)

  2. 若删除指定节点后会影响节点平衡就会向与之最近的多余节点的节点借,但并非直接借,而是通过父节点间接借

  3. 若在整个树中没有可以借的节点就会通过降低层数的方式维持输的平衡

映射关系

  • 一个红黑树只对应一个234树,一个234树可能对应多个红黑树。
  • 红黑树的红色节点不可能连续
  • 一个234树中的节点对应到红黑树上只有一个黑色节点,因此在红黑树的任意节点到该节点的叶子节点路径上黑色节点个数一定是一样的

转化Test

这是一个带颜色的234树:

转化为红黑为:(标号为该路径上有几个黑色节点)

红黑树性质

  1. 节点为红色和黑色
  2. 根节点为黑色

这两条可理解为红黑树定义

  1. 所有叶子均为黑色

对这一条可能有些疑问,实际上在处理红黑树的的节点时,若该节点为叶子节点,就会在后边填补俩个黑色空节点,在上述举例时只不过被省略了。

  1. 左右红色节点下均有两个黑色节点

第三条的介入时只成立

  1. 从任意节点到该节点所有的叶子节点的简单路径上的黑的节点个数一定(黑色平衡)

根据234树的平衡,每个节点对应在红黑树的节点时有且只有一个黑色节点

节点类和基本方法

public class RBTree<K extends Comparable<K>,V>{
    private RBNode root;//先定义根节点
    private static final boolean BLACK=true;//黑色为真
    private static final boolean RED=false;//红色为假

	//获取节点颜色
	private static boolean getColor(RBNode rbNode){
        //空节点时为黑,因为最后层为黑
        //这样可以避免在后续复杂处理时出现空指针
        return rbNode==null?BLACK:rbNode.color;
    }
    //设置节点颜色
    private static void setColor(RBNode rbNode, boolean b){
        if(rbNode!=null){
            rbNode.color=b;
        }
    }
    //获取父节点
    private static RBNode parentOf(RBNode rbNode){
        return rbNode==null?null:rbNode.parent;
    }
    //获取左子节点
    private static RBNode leftOf(RBNode rbNode){
        return rbNode==null?null:rbNode.left;
    }
    //获取右子节点
    private static RBNode rightOf(RBNode rbNode){
        return rbNode==null?null:rbNode.right;
    }
    
	//节点类放在了RBTree类中当作了内部类
	//该类的泛型K键值(TreeMap中的键)继承Comparable接口,用于后续比较
	static class RBNode<K extends Comparable<K>,V>{
	    K key;
	    V value;
	    private boolean color;
	    RBTree.RBNode parent;//父亲节点
	    RBTree.RBNode left;//左节点
	    RBTree.RBNode right;//右节点
	
		//创建构造器
	    public RBNode(K key, V value, RBTree.RBNode parent) {
	        this.key = key;
	        this.value = value;
	        this.color = BLACK;
	        this.parent = parent;
	    }
	
		//可自己调用前序遍历时显示键值对的值
	    @Override
	    public String toString() {
	        return "RBNode{" +
	                "key=" + key +
	                ", value=" + value +
	                ", color=" + (color?"BLACK":"RED") +
	                '}';
	    }
	
		//一些 get  set 方法
	    public RBTree.RBNode getLeft() {
	        return left;
	    }
	    public RBTree.RBNode getRight() {
	        return right;
	    }
	    public boolean isColor() {
	        return color;
	    }
	    public V getValue() {
	        return value;
	    }
	}
}

关于为什么有这些私有方法,是因为在后续过程中不管是左右旋,还是节点颜色判断将不会担心空指针异常。

红黑树的左右旋

左旋图1

左旋图2

  • p节点为以p节点进行左旋转
  • 蓝色箭头为该子树的父节点
  • 绿色节点为颜色位置的节点,可能为空
  1. l为用来代替p的节点

  2. 将p颜色给l,并将p置为红色

  3. 改变父节点指向,将l左子树给p的右树

  4. 将l的左指向p

结果:

注:每一次左右节点的交换都要改变父亲父节点的指向,同时要注意父亲节点的左还是右指向该孩子节点,上图省略了

于是有了下面代码:

左旋代码

//以p节点为旋转点
private  void leftRotate(RBNode p)  {
   if(p!=null){
       RBNode l=p.right;
       setColor(l, p.color);
       setColor(p, RED);
       p.right = l.left;
       //上句交换了指向,所以要修改父节点指向,方向已知
       if (l.left != null) {
           l.left.parent = p;
        }
        l.parent = p.parent;
        //上句交换了指向,所以要修改父节点指向,方向未知,所以要判断方向
        if (p.parent == null) {//若无说明p节点为根节点,让根节点指向l即可
            root = l;
        } else if (p.parent.left == p) {//若是父节点左
            p.parent.left = l;
        } else {//若是父节点右
            p.parent.right = l;
        }
        l.left = p;
        //处理父节点,且不可能为空
        p.parent = l;
    }
}

右旋图

右旋代码

同理左旋,不在赘述

private void rightRotate(RBNode p)  {
    RBNode r = p.left;
    setColor(r, getColor(p));
    setColor(p, RED);
    p.left = r.right;
    if (r.right != null) {
        r.right.parent = p;
    }
    r.parent = p.parent;
    if (p.parent == null) {
        root = r;
    } else if (p.parent.left == p) {
       p.parent.left = r;
    } else {
        p.parent.right = r;
    }
    r.right = p;
    p.parent = r;
}

新增节点

AVL树方法搜索位置

在RBTree类中创建put方法,先进行查找:

class RBTree<K extends Comparable<K>,V>{
    public void put(K key,V value){
        if(key==null){
            throw new RuntimeException("空指针异常");
        }
        //若root为空,说明第一个元素,那么直接对root修改
        if(null == root){
        	//颜色默认黑
            root=new RBNode<>(key,value==null?key:value,null);
            return;
        }
        //com记录比较值,用于记录插入节点的位置是父节点的方向左还是右
        int com;
        //记录插入节点的父节点,因为插入节点不可能立即成为根结点(后续调整可能成为父节点),所以父节点必存在
        RBNode parentTail;
        //从根节点开始搜索,parentTail紧跟其后
        RBNode tail=root;
        do{
            //记录父节点
            parentTail=tail;
            //将插入值和tail搜索的值及进行比较,用com记录
            com=key.compareTo((K) tail.key);
            if(com<0){
                //向左搜索
                tail=leftOf(tail);
            }else if (com>0){
                //向右搜索
                tail=rightOf(tail);
            }else {
                //若相同就直接覆盖当前值,因为并未改变树的平衡,并直接结束程序
                tail.value=value==null?key:value;
                return;
            }
        }while (tail!=null);//终止搜索条件
        //创建一个新节用于存储进来的数据
        RBNode newRBNode=new RBNode<>(key,value==null?key:value,parentTail);
        //根据com大小进行父节点的左右赋值操作
        if (com<0){
            parentTail.left=newRBNode;
        }else {
            parentTail.right=newRBNode;
        }
    }
   fixAfterPut(newRBNode);
}

是不是以为这样就完了?(怎么可能有一句代码我还没见过!!!)


对,没错,还有fixAfterPut函数!!!

那我们就看看用AVL搜索后的结果可能出现的所有情况吧!

红黑插入的情况图解(以左为例)

根据插入节点对应在234树上的情况有:

  • 添加的颜色要优先置为红,防止直接对红黑树平衡造成影响
  • 在父节点为黑色的时候,无需操作。
  • 父亲节点为红时,就要判断叔叔节点是否存在
    • 叔叔节点不存在(第三种),判断该节点是父亲什么节点做一次或两次旋转
    • 叔叔节点存在就转换颜色(第四种),将爷爷节点作为新节点判断进入循环
  • 根置黑

情况三的第二个:

情况三的第三个:


情况四的第一个:

情况四的第二个:

红黑插入的情况图流程图

代码详解

关于root空的这一种情况在查找前已经排除。

    private void fixAfterPut(RBNode newRBNode) {
    	//将新增节点置为红色
        setColor(newRBNode,RED);
        //若新增节点的父节点为黑,直接退出
        //若没有这个判断也可,因为不会进入循环,不过这样好理解
        if(getColor(parentOf(newRBNode))==BLACK){
            return;
        }
        //新增节点不为根节点,且父节点为红色
        //(父节点为红色是作为出循环条件,毫无影响第一次进循环
        while (newRBNode != root&&getColor(parentOf(newRBNode))==RED){
        	//添加指向爷爷和父亲的指针,便于理解
            RBNode grandFather=parentOf(parentOf(newRBNode));
            RBNode father=parentOf(newRBNode);
            
            //判断父亲是爷爷的左节点还是右节点,依次判断叔叔在爷爷节点的左右
            if(leftOf(grandFather)==father){
            	//确定叔叔节点,可能为null
                RBNode uncle=rightOf(grandFather);
                
                //叔叔节点空还是红
                //(不可能为黑,若黑那么之前的树不平衡)
                if(getColor(uncle)==RED){
                	//撑死式换颜色
                    setColor(grandFather,RED);
                    setColor(uncle,BLACK);
                    setColor(father,BLACK);
                    //将爷爷换成新增节点,重新循环
                    newRBNode=grandFather;
                }else {//叔叔节点空:
                	//是否需要进行左旋调整
                    if(rightOf(father)==newRBNode){
                        leftRotate(father);
                        //指向新节点位置
                        newRBNode=father;
                    }
                    
                    //根据爷爷节点右旋即可
                    //右旋后newRBNode节点的父亲就是黑色了
                    //这里不需要手动变颜色,因为在旋转时已经变过了,不过要小心验证
                    rightRotate(grandFather);
                }
            }else {//跟上一段代码雷同
                RBNode uncle=leftOf(grandFather);
                if(getColor(uncle)==RED){
                    setColor(grandFather,RED);
                    setColor(uncle,BLACK);
                    setColor(father,BLACK);
                    newRBNode=grandFather;
                }else {
                    if(leftOf(father)==newRBNode){
                        rightRotate(father);
                        newRBNode=father;
                    }
                    leftRotate(grandFather);
                }
            }
        }
        //将根节点置为黑,防止其他操作变成了红色
        setColor(root,BLACK);
    }

新增代码整合(put)

    public void put(K key,V value){
        if(null == root){
            root=new RBNode<>(key,value==null?key:value,null);
            return;
        }
        int com;
        RBNode tail=root;
        RBNode parentTail;
        if(key==null){
            throw new RuntimeException("空指针异常");
        }
        do{
            parentTail=tail;
            com=key.compareTo((K) tail.key);
            if(com<0){
                tail=leftOf(tail);
            }else if (com>0){
                tail=rightOf(tail);
            }else {
                tail.value=value==null?key:value;
                return;
            }
        }while (tail!=null);
        RBNode newRBNode=new RBNode<>(key,value==null?key:value,parentTail);
        if (com<0){
            parentTail.left=newRBNode;
        }else {
            parentTail.right=newRBNode;
        }

        fixAfterPut(newRBNode);
    }

    private void fixAfterPut(RBNode newRBNode) {
        setColor(newRBNode,RED);
        while (newRBNode != root&&getColor(parentOf(newRBNode))==RED){
            RBNode grandFather=parentOf(parentOf(newRBNode));
            RBNode father=parentOf(newRBNode);
            if(leftOf(grandFather)==father){
                RBNode uncle=rightOf(grandFather);
                //空或者黑色不进入
                if(getColor(uncle)==RED){
                    setColor(grandFather,RED);
                    setColor(uncle,BLACK);
                    setColor(father,BLACK);
                    newRBNode=grandFather;
                }TreeMap源码剖析

Java - HashTree源码解析 + 红黑树

TreeMap红黑树源码详解

结合java.util.TreeMap源码理解红黑树

转:Java集合源码剖析TreeMap源码剖析

TreeMap关键源码解析-红黑树操作