【树】红黑树 图解 Java描述

一、前言

了解红黑树之前,最好先了解一下二叉搜索树以及平衡二叉树。毕竟红黑树属于平衡二叉树的一个变种。

1. 二叉搜索树

这里简单介绍一下二叉搜索树,二叉树搜索树有以下特性:

  • 任意节点的左子树的节点值小于当前该节点的节点值;
  • 任意节点的右子树的节点值小于当前该节点的节点值;
  • 任意节点的左右子树也必须是二叉查找树;
  • 任意节点的键值不想等;

在这里插入图片描述
二叉搜索树的缺点很明显,当部分元素的插入顺序按升序或降序插入的话,二叉搜索树的部分节点将退化成链表,使其查找、插入效率低下;

2. 平衡二叉树

平衡二叉树满足二叉搜索树的所有要求,与二叉搜索树最大的不同点在于其节点内维护了一个平衡因子(该节点左子树高度减去右子树高度的差),平衡因子被严格控制在{-1、0、1}这个集合内,从而避免了上面所述的退化成链表的情况出现。一旦平衡因子超出这个范围,二叉树会通过旋转操作使得失衡节点恢复到平衡状态。这篇文章着重讲红黑树,这里只简单介绍下。

在这里插入图片描述

3. 红黑树起步

虽然平衡树解决了二叉搜索树退化为链表的缺点,能把查找效率控制在O(log n),不过这种实现却不是最佳的。因为平衡树的要求十分严格,每个节点的左子树和右子树的高度差最多只能为1,这样导致了几乎每次插入/删除节点的时候,都会破坏平衡,从而导致左旋和右旋操作的发生。因此又引入了红黑树,作为一种折中的方案。

先来了解一下红黑树的几个性质(或者说是要求):

  • 每个节点会被标注为红色,或者黑色;
  • 根节点只能是黑色
  • 每个叶节点都只能是黑色,值得注意的是,这里的叶节点,在Java里是指null,如下图的NIL节点;
    在这里插入图片描述
  • 如果一个节点为红色,那么它的子节点必须是黑色的,这一点也可以转义为不能存在两个连续的红节点
  • 对于任意一个节点,其到树的尾端NIL的每条路径都包含必须包含相同数目的黑节点;

正是由于红黑树上述的特点,使得它能够在最坏的情况下,也能在O(log n)的时间复杂度查找到某个节点。不过与平衡树不同的是,红黑树并不是一个完美平衡二叉树搜索树,但由前面第五个性质我们可以直到红黑树的每个节点到叶子节点的路径都包含数量相同的黑节点,这种性质叫黑色完美平衡

二、旋转与变色

要保持红黑树的黑色完美平衡,我们需要到两个很关键的操作——旋转(Rotation)and 变色(Recolor);毕竟如果我们在插入或删除节点后,很容易破坏树的平衡,必须要通过一些操作来保持树的平衡,才能达到红黑树的要求;

  • 变色:重新标色,红变黑或者黑变红;

  • 旋转:旋转分为左旋,右旋;视失衡节点情况再决定如何旋转;主要情况有LL型,RR型,RL型,LR型,这里还是采用上面那张总结图来表示一下(先不考虑需要变色的情况)
    在这里插入图片描述

    扫描二维码关注公众号,回复: 11378062 查看本文章

三、操作说明

首先说明,为了演示过程的清晰,这里将新插入(删除)的节点用S(Son)表示,插入(删除)节点的父节点用F(Father)表示,插入(删除)节点的祖父节点用G(Grandfather)表示,插入(删除)节点的叔叔节点(即F的兄弟节点)用U表示

1. 插入操作

首先介绍插入操作,下面是整个插入过程的流程以及插入后遇见的几种情况所对应的操作说明:

  • 找到插入节点的位置,将节点插入并置为红色;

    我们希望插入过程尽可能不破坏上述红黑树的性质,这样我们就可以不用进行额外的调整操作。

    根据“对于任意一个节点,其到树的尾端NIL的每条路径都包含必须包含相同数目的黑节点”这个性质,显然不能将节点涂成黑色,因为这样所有经过插入节点的路径的黑色节点都多了一个,就破坏了平衡,所以我们将S的颜色首先着色为

    在这里插入图片描述

  • 判断插入是否为根节点,是的话就将插入的节点置为黑色即可,插入过程到此结束,否则继续往下;

  • 判断插入节点的父节点是否为黑色,如果为黑色则表明平衡没有陪破坏,那么插入到此完成,不需要任何修改,否则继续往下执行;

  • 现在插入节点的父节点为红色,接下来判断叔叔节点是否为红色,那么就有以下两种情况:

    • 父节点为红色,叔叔节点也为红色。那么直接将父节点和叔叔节点置为黑色,祖父节点置为红色,本节点也为红色,如下所示:

      在这里插入图片描述
      这里需要说明一下,这个操作其实是想将红色节点往根节点方向移动。将红色往上移动一层,不断将红色往上移动,当移动到根时,直接将根设置为黑色(递龟),就完全符合红黑树的性质了。

    • 父节点为红色,叔叔节点为黑色,那么就又有两种情况了:

      • 插入节点的父节点为祖父节点的左子树(F为G的左孩子),然后这里TM又有两种情况:

        • 插入节点是父节点的右孩子(S为F的右孩子),那么将以G为根节点的子树左旋,此时会变成S为F的左孩子,如下图所示:

          在这里插入图片描述

          这一步操作是为了将S为F的右孩子转变为S为F的左孩子,之后就按照S为F的左孩子节点的情况进行操作,也就是接下来讲的情况。

        • 插入节点是父节点的左孩子(S为F的左孩子),这种情况的变化稍显复杂,先看一下图示:

          在这里插入图片描述

          G与F颜色互换后左右子树的黑色节点不想等,G失衡,将G右旋,使得黑色的F变为新的G,这也操作后将就完全符合红黑树的性质,之所以说是完全符合,因为原来G的地方是个黑色节点,后来用了黑色的F去代替了G,对于G往上的层来说,并没有发生颜色变化,自然就是平衡的,所以,经过这一步的操作,红黑树恢复所有特性。

      • 插入节点的父节点为祖父节点的右子树(F为G的右孩子),不画了,按上面F为G的左孩子可以镜像推导出F为G的右孩子的情况;

这是大概的整个流程,用下图总结一下:

在这里插入图片描述

2. 删除操作

。。。

当然上面的图只是演示而已,真的实现的话还是得通过递归回溯来完成判断啦,不可能每个节点都来一遍这种判断,不想去维护多个指向父节点的指针了。接下来在二叉搜索树的基础上实现红黑树;

四、代码实现

1. 二叉搜索树

public class RBTree<K extends Comparable<K>, V> {
    
    // 用来表示红黑树节点的颜色
    private static final boolean RED = true;
    private static final boolean BLACK = false;
 
    private Node root;
    private int size;
    
    private class Node {
        public K key;
        public V value;
        public Node left, right;
        public boolean color;
 
        public Node(K key, V value){
            this.key = key;
            this.value = value;
            left = null;
            right = null;
            // 默认新创建的的节点为红色
            color = RED;
        }
    }
   
    public RBTree(){
        root = null;
        size = 0;
    }
 
    public int getSize(){
        return size;
    }
 
    public boolean isEmpty(){
        return size == 0;
    }
 
    // 判断节点node的颜色
    private boolean isRed(Node node){
        if(node == null){
            return BLACK;
        }
        return node.color;
    }
 
    // 向二分搜索树中添加新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
    }
 
    // 向以node为根的二分搜索树中插入元素(key, value),递归算法
    // 返回插入新节点后二分搜索树的根
    private Node add(Node node, K key, V value){
        if(node == null){
            size ++;
            return new Node(key, value);
        }
 
        if(key.compareTo(node.key) < 0){
            node.left = add(node.left, key, value);
        }else if(key.compareTo(node.key) > 0){
            node.right = add(node.right, key, value);
        }else{ // key.compareTo(node.key) == 0
            node.value = value;
        }
        return node;
    }
 
    // 返回以node为根节点的二分搜索树中,key所在的节点
    private Node getNode(Node node, K key){
 
        if(node == null){
            return null;
        }
        if(key.equals(node.key)){
            return node;
        }else if(key.compareTo(node.key) < 0){
            return getNode(node.left, key);
        }else{ // if(key.compareTo(node.key) > 0)
            return getNode(node.right, key);
        }
    }
 
    public boolean contains(K key){
        return getNode(root, key) != null;
    }
 
    public V get(K key){
 
        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }
 
    public void set(K key, V newValue){
        Node node = getNode(root, key);
        if(node == null){
            throw new IllegalArgumentException(key + " doesn't exist!");
        }
        node.value = newValue;
    }
 
    // 返回以node为根的二分搜索树的最小值所在的节点
    private Node minimum(Node node){
        if(node.left == null){
            return node;
        }
        return minimum(node.left);
    }
 
    // 删除掉以node为根的二分搜索树中的最小节点
    // 返回删除节点后新的二分搜索树的根
    private Node removeMin(Node node){
 
        if(node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
            return rightNode;
        }
 
        node.left = removeMin(node.left);
        return node;
    }
 
    // 从二分搜索树中删除键为key的节点
    public V remove(K key){
 
        Node node = getNode(root, key);
        if(node != null){
            root = remove(root, key);
            return node.value;
        }
        return null;
    }
 
    private Node remove(Node node, K key){
 
        if( node == null ){
            return null;
        }
        if( key.compareTo(node.key) < 0 ){
            node.left = remove(node.left , key);
            return node;
        }else if(key.compareTo(node.key) > 0 ){
            node.right = remove(node.right, key);
            return node;
        }else{   // key.compareTo(node.key) == 0
 
            // 待删除节点左子树为空的情况
            if(node.left == null){
                Node rightNode = node.right;
                node.right = null;
                size --;
                return rightNode;
            }
 
            // 待删除节点右子树为空的情况
            if(node.right == null){
                Node leftNode = node.left;
                node.left = null;
                size --;
                return leftNode;
            }
 
            // 待删除节点左右子树均不为空的情况
 
            // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
            // 用这个节点顶替待删除节点的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;
 
            node.left = node.right = null;
 
            return successor;
        }
    }
}

2. 改进插入操作

// 向红黑树中添加新的元素(key, value)
public void add(K key, V value){
    root = add(root, key, value);
    root.color = BLACK; // 最终根节点为黑色节点
}

// 向以node为根的红黑树中插入元素(key, value),递归算法
// 返回插入新节点后红黑树的根
private Node add(Node node, K key, V value){

    if(node == null){
        size ++;
        return new Node(key, value); // 默认插入红色节点
    }

    if(key.compareTo(node.key) < 0){
        node.left = add(node.left, key, value);
    }else if(key.compareTo(node.key) > 0){
        node.right = add(node.right, key, value);
    }else{ //比较后相等,则修改原来的值
        node.value = value;
    }
    return node;
}
// 左旋
private Node leftRotate(Node node){

    Node x = node.right;

    // 左旋转
    node.right = x.left;
    x.left = node;

    x.color = node.color;
    node.color = RED;

    return x;
}
// 递归算法
// 返回插入新节点后红黑树的根
private Node add(Node node, K key, V value){
    if(node == null){
        size ++;
        return new Node(key, value); // 默认插入红色节点
    }

    if(key.compareTo(node.key) < 0){
        node.left = add(node.left, key, value);
    }else if(key.compareTo(node.key) > 0){
        node.right = add(node.right, key, value);
    }else{ // key.compareTo(node.key) == 0
        node.value = value;
    }
    //左旋转:右孩子是红色,左孩子是黑色
    if (isRed(node.right) && !isRed(node.left)){
        node = leftRotate(node);
    }
    //右旋转:左孩子是红节点,左孩子的左孩子是红节点
    if (isRed(node.left) && isRed(node.left.left)){
        node = rightRotate(node);
    }
    //颜色翻转:左右节点都是红色
    if (isRed(node.left) && isRed(node.right)){
        flipColors(node);
    }
    return node;
}

写得匆忙,有错指正。部分代码来自《算法》一书。

猜你喜欢

转载自blog.csdn.net/Allen_Adolph/article/details/106950037