红黑树增删查详解

1.红黑树增删查相关操作

1.介绍

​ 由于AVL树虽然查询快,但是插入删除的时候需要不断的操作来维持自身平衡,导致性能下降。而红黑树(R-B Tree)黑色平衡的,在操作的时候只需维持黑色平衡即可(当然还有其他的特性)。红黑树是由2-3-4树变换而来,如果需要了解红黑树详细的操作流程,需要对2-3-4树有一定的了解。红黑树在JDK1.8中的HashMapConcurrentHashMapTreeMap等,以及Linux内核.

2.特性

1.根节点为黑色。

2.所有节点非黑即红。

3.红色节点的子节点一定是黑色节点(或为空节点,空节点也为黑色节点)。

4.从根节点到每一个子节点黑色数量是相等的(黑色平衡)。

3.操作

1.基础代码

public class RedBlackTree<K extends Comparable<K>, V> {
    private RBNode<K, V> root;//根节点
    
    //红黑树结构
    static class RBNode<K, V> {
        private RBNode<K, V> left;//左节点
        private RBNode<K, V> right;//右节点
        private RBNode<K, V> parent;//父节点
        private boolean red = true;//颜色  默认为红色
        private K kay;//key
        private V val;//value
        //省略其他的get set 以及默认构造方法   
        public RBNode(K key, V val) {
            this.key = key;
            this.val = val == null ? (V) key : val;
        } 
     }
}

RedBlackTreeK继承Comparable接口,用于比较K值的大小,判断值插入的位置是左节点还是右节点。插入的每个节点默认的颜色是红色,在有参构造里面如果val为空,插入的val就是key

2.旋转

1.说明

想要写出左旋右旋的代码,就需要先了解旋转的时候会发生变化的节点。 在旋转的过程发生的变化(以下以左旋为准,右旋变换左右即可)

三个节点的父节点发生改变:旋转点 、旋转点的右节点 、旋转点的右节点左子节点

三个节点的子节点发生变化:旋转点 、旋转点的右节点 、旋转点的父节点

根据以上变化逐一判断即可

2.左旋

在这里插入图片描述

左旋:以某个(h)节点左旋,旋转点(h)右节点的左子节点变为旋转点的右节点,旋转点之前的右节点变为父节点。

/**
 * 左旋
 *             hp                        hp
 *            /                         /
 *           h                        hr
 *         /  \                     /  \
 *        hl   hr                  h    hlr
 *             / \                / \
 *           hll hlr            hl  hll
 *
 * @param h 旋转点
 */
private void rotateLeft(RBNode<K, V> h) {
    if (h != null) {
        // pr h节点的右节点
        RBNode<K, V> hr = h.right;
        //h右节点的左节点变为h的右节点
        h.right = hr.left;
        if (hr.left != null) {//判断h的右节点是否存在左节点  存在就把它作为
            hr.left.parent = h;
        }
        //h的父节点 变为 hr的父节点
        hr.parent = h.parent;
        RBNode<K, V> hp;
        if ((hp = h.parent) != null) {//如果h就是根节点就会为空  否则将hr挂在hp下面
            if (h == pp.left) {
                hp.left = hr;
            } else {
                hp.right = hr;
            }
        } else {
            root = hr;
        }
        //h之前的右节点变为h的父节点
        h.parent = hr;
        hr.left = h;
    }
}
3.右旋

在这里插入图片描述

右旋:以某个点(h)旋转,旋转点(h)左节点的右子节点变为旋转点的左节点,旋转点之前的左节点变为父节点

/**
 * 右旋
 * <p>
 *            hp                      hp
 *            /                       /
 *           h                       hl
 *         /  \                    /   \
 *        hl   hr               hpl     h
 *        / \                          / \
 *       hll hlr                      hlr  hr
 *
 * @param h 旋转点
 */
private void rotateRight(RBNode<K, V> h) {
    if (h != null) {
        //hl  h的左节点
        RBNode<K, V> hl = h.left;
        //首先将h左孩子节点的右节点挂到h的左节点上
        h.left = hl.right;
        //判断hl的右节点是否存在
        if (hl.right != null) {//存在的话 将hlr变成p的左节点
            hl.right.parent = h;
        }
        //h的父节点变为hl的父节点
        hl.parent = h.parent;
        if (h.parent != null) {
            //h是父亲的左节点
            if (h == h.parent.left) {
                h.parent.left = hl;
            } else {
                h.parent.right = hl;
            }
        } else {
            root = hl;
        }
        //h变为左节点变为h的父节点
        hl.right = h;
        h.parent = hl;//将hl换成h的父节点
    }
}

3.新增

描述:当我们在插入节点的时候,首先根据大小来找到插入节点对应的位置,然后再插入数据。在插入节点的时候默认是红色,如果插入节点的父节点是红色这个时候就违反了红黑色的规则(第三条),这个时候就需要根据旋转以及变色来维持平衡。

1.寻找插入节点的位置
/**
 * 插入节点
 *
 * @param key
 * @param val
 * @return
 */
public RBNode<K, V> put(K key, V val) {
    if (key == null) {
        return null;
    }
    //根节点
    RBNode<K, V> _root = this.root;
    if (_root == null) {
        this.root = new RBNode<K, V>(key, val);
        this.root.red = false;//根节点变黑
        return null;
    }
    //插入节点的父节点
    RBNode<K, V> parent;
    //循环找到插入节点的父节点
    do {
        parent = _root;
        if (key.compareTo(_root.key) < 0) {//key < _root.key  往左边找
            _root = _root.left;//根据找到节点的左节点继续循环
        } else if (key.compareTo(_root.key) > 0) {//key > _root.key   往右边找
            _root = _root.right;//根据找到节点的右节点继续循环
        } else {//相等的话 就换成新的val值 并返回
            _root.val = val;
            return _root;
        }
    } while (_root != null);//左节点 或者 右节点没有节点了

    //创建新节点
    RBNode<K, V> child = new RBNode<>(key, val);
    //插到parent下面
    child.parent = parent;
    if (key.compareTo(parent.key) < 0) {//判断插入的是parent 的左边还是有右边
        parent.left = child;
    } else {
        parent.right = child;
    }

    //根据插入节点调整平衡
    balanceInsertion(child);

    return null;
}

插入数据的时候首先我们需要判断根节点是否为空,为空插入的节点就是根节点。否则就根据key(key实现了Comparable接口)的大小来找到插入节点的父节点,然后将节点插入父节点的下面。最后根据插入的节点调整平衡

2.根据插入的节点调整平衡

插入的节点默认是红色的,.如果插入节点的父节点是红色,这个时候及违反了红黑树的特性。

PS:以下我们根据插入节点的父节点是爷爷节点的左节点分析,另一种情况(插入节点的父节点是爷爷节点的右节点)操作完全相反.只要掌握其中一种情况即可.

1.插入节点§的父节点和叔叔节点都是红色

在这里插入图片描述

1.父节点和叔叔节点变黑,爷爷节点变红。然后将P节点指向爷爷节点,继续往上递归

2.p节点为根节点,直接将p节点变红

2.插入节点§的父节是红色,并且插入节点是父节点的左节点

在这里插入图片描述

首先父节点变黑,爷爷节点变红, 然后根据p的爷爷节点右旋.

3.插入节点§的父节是红色,并且插入节点是父节点的右节点

在这里插入图片描述

直接根据p的父节点左旋,转换为第二种情况,然后通过第二种情况处理

3.具体实现
/**
 * 插入数据后平衡操作
 * <p>
 * 分析: 插入的节点默认为红色,如果父节点为红色 那么就不满足红黑树的条件(红色节点有两个黑子节点)。所以当父节点为红色的时候需要调整,主要分以下三种情况(以p的父节点是爷爷的左子节点为例)
 * 一.p的父节点和叔叔节点都是红的
 * 解决:将p的父节点和叔叔节点变为黑色,p的爷爷节点变为红色。但p的爷爷节点的父节点可能也是红的那就又不满足红黑树的原则了,此时就需要把p的爷爷节点当作p节点继续往上递归了
 * 二.p的叔叔节点不是红色的(或者没有叔叔节点),并且p是父亲的左节点
 * 解决:以p的爷爷节点为支点向右旋
 * 三.p的叔叔节点不是红色的(或者没有叔叔节点),并且p是父亲的右节点
 * 解决:先以p的父亲节点左旋,变成了情况二了,以p的爷爷节点为支点向右旋
 *
 * @param p 插入的节点
 */
private void balanceInsertion(RBNode<K, V> p) {
    //p不是根节点 并且p的父节点是红色的时候需要调整
    while (p != null && p != root && p.parent.red) {
        //父节点在左边
        if (p.parent == p.parent.parent.left) {
            //叔叔节点
            RBNode<K, V> u;
            //如果叔叔节点是红色的(情况一)
            if ((u = p.parent.parent.right) != null && u.red) {
                //父节点和叔叔节点变黑  爷爷节点变红  然后以爷爷节点继续往上递归
                u.red = false;
                p.parent.red = false;
                p.parent.parent.red = true;
                //以爷爷节点继续往上递归
                p = p.parent.parent;
            } else {
                //p是父亲的右节点(情况三)
                if (p == p.parent.right) {
                    p = p.parent;
                    //以p的父亲节点左旋
                    rotateLeft(p);
                }
                //爷爷变红色
                p.parent.parent.red = true;
                //父亲边黑色
                p.parent.red = false;
                //以p的爷爷节点右旋
                rotateRight(p.parent.parent);

            }
        } else {//剩下的情况刚好与上相反 左改右 右改左
            RBNode<K, V> u;
            if ((u = p.parent.parent.left) != null && u.red) {
                u.red = false;
                p.parent.red = false;
                p.parent.parent.red = true;
                p = p.parent.parent;
            } else {
                if (p == p.parent.left) {
                    p = p.parent;
                    rotateRight(p);
                }
                p.parent.parent.red = true;
                p.parent.red = false;
                rotateLeft(p.parent.parent);

            }
        }
    }
    //根节点变黑色 这一布很重要(第一中情况后 就需要将根节点变黑)
    root.red = false;
}

4.查询

根据某个key查找树中对应的元素

/**
 * 根据key找到对应的节点
 *
 * @param key 寻找的key
 * @return 找到的节点值
 */
private RBNode<K, V> getNodeByKey(K key) {
    if (key != null) {
        RBNode<K, V> temp = root;
        do {
            //根据大小往叶节点循环查找
            if (key.compareTo(temp.key) < 0) {
                temp = temp.left;
            } else if (key.compareTo(temp.key) > 0) {
                temp = temp.right;
            } else {//找到对应的值
                return temp;
            }
        } while (temp != null);
    }
    return null;
}

5.删除

描述:在删除节点的时候,首先需要查找对应的节点.然后判断该节点是否是根节点,如果是根节点直接删除,否则找到对应的前驱或者后继节点,交换位置或者值.然后判断前驱或后继节点是否有子节点,如果有,子节点上去(替换前驱或后继节点).然后删除最后替换的节点(叶子节点).删除后,如果删除的是黑色,则需要调整.

1.找替换节点

红黑树里面,整棵树从左往右肯定是从小到大的.所以想找到某个节点前驱(小于本身的最大节点),肯定是从左节点最右节点.某个节点后继(大于本身的最小节点),肯定是从右节点最左节点.如下图所示

在这里插入图片描述

1.前驱节点

前驱节点:小于当前节点的最大值对应的节点

/**
 * 找到前驱节点(小于当前节点的最大值)
 * 	
 * @param node
 * @return
 */
private RBNode<K, V> predecessor(RBNode<K, V> node) {
    if (node != null) {
        //判断node左节点是否存在
        if (node.left != null) {
            RBNode<K, V> plr;
            if ((plr = node.left.right) != null) {
                //找到左节点的最右节点
                while (plr.right != null) {
                    plr = plr.right;
                }
                return plr;
            }
            return node.left;
        } else {//如果不存在  在删除节点是不会走这一步的  p.left != null && p.right != null
            RBNode<K, V> child = node;
            RBNode<K, V> p = node.parent;
            while (p != null && child == p.left) {
                child = p;
                p = p.parent;
            }
            return p;
        }
    }
    return null;
}
2.后继节点

前驱节点:大于当前节点的最小值对应的节点

/**
 * 找到后继节点(大于当前节点的最小值)
 *
 * @param node
 * @return Successor
 */
private RBNode<K, V> successor(RBNode<K, V> node) {
    if (node != null) {
         //判断node右节点是否存在
        if (node.right != null) {
            RBNode<K, V> pll;
            if ((pll = node.right.left) != null) {
                //找到最左节点
                while (pll.left != null) {
                    pll = pll.left;
                }
                return pll;
            }
            return node.right;
        } else {
            RBNode<K, V> child = node;
            RBNode<K, V> p = node.parent;
            while (p != null && child == p.right) {
                child = p;
                p = p.parent;
            }
            return p;
        }
    }
    return null;
}
2.删除节点

删除的时候,找到对应的前驱或者后继节点后,判断其是否有子节点。如果有,且可能只有一个子节点,将前驱或者后继节点和子节点替换(转变为删除子节点)。没有的话就说明删除的就是子节点或者根节点。最后删除子节点如果为黑就重新调整平衡。

  1. 没有子节点,就说明删除的就是子节点,根据删除的节点调整平衡后删除即可。
  2. 如果是根节点,那么直接将root制空。
  3. 如果存在一个子节点,将对应的子节点与前驱(后继)节点替换,然后删除替换后的子节点,调整平衡。
/**
 * 根据key移除某个节点
 *
 * @param key
 */
public void remove(K key) {
    //根据key查找对应的值
    RBNode<K, V> p = getNodeByKey(key);

    if (p == null) return;
    //如果p的左右节点都存在
    if (p.left != null && p.right != null) {
        //找到前驱节点
        RBNode<K, V> node = predecessor(p);
        //将node值赋给p
        p.key = node.key;
        p.val = node.val;
        p = node;
    }
    //p节点的替换节点(也就是p的子节点,如果存在只可能有一个)
    RBNode<K, V> replace = p.left != null ? p.left : p.right;
    if (replace != null) {
        RBNode<K, V> pp;
        //如果p节点的父节点是根节点 那么替换节点变为根节点
        if ((pp = p.parent) == null) {
            root = replace;
        } else if (p == pp.left) {
            pp.left = replace;
        } else {
            pp.right = replace;
        }
        replace.parent = pp;

        //删除p节点
        p.left = p.right = p.parent = null;
        //删除的是黑色节点需要调整
        if (!p.red) {
            //对replace 进行平衡处理
            balanceDeletion(replace);
        }
    } else if (p.parent == null) {
        root = null;
    } else {//p就是根节点,这个时候需要先调整平衡 再做删除
        if (!p.red) {
            balanceDeletion(p);
        }
        //删除p节点
        if (p.parent.left == p) {
            p.parent.left = null;
        } else {
            p.parent.right = null;
        }
        p.parent = null;
    }
}
3.调整平衡

删除节点的时候,需要和2-3-4树对照才能知道其中的所以然。由上可知在删除节点的时候,最终都转换为删除子节点。如果删除的子节点颜色是黑色那么就需要调整。

1.找兄弟节点借(兄弟节点的子节点都为空,或者都为黑色节点,借出去就会影响平衡),兄弟节点没得借,那么就直接将兄弟节点变红以父节点继续往上找。

2.兄弟节点有的借…

1.2-3-4树

2节点:有一个元素的节点,有两个子节点

3节点:有两个元素的节点,有三个子节点

4节点:有三个元素的节点,有四个子节点

在这里插入图片描述

首先我们看看红黑树和2-3-4树对应的关系

在这里插入图片描述

依次按照1-10插入10个元素,得到上图的红黑树和2-3-4树(忽略2-3-4的颜色)。从上可以看出红黑树其实就是2-3-4树演变而来。同时,如果红黑树的每个红色节点向上合并也是可以得到2-3-4树

2.删除调整平衡

了解了红黑树嘿2-3-4树的基本关系,以下我们还是分析左边的情况(删除的节点是父节点的左节点),假设删除的节点为X节点

找到真正的兄弟节点

如果兄弟节点存在,并且为红色,说明它不是真正的兄弟节点,红色节点需要往上合并(自己的理解)。

在这里插入图片描述

这个时候我们就需要根据X的父节点左旋,找到兄弟节点。

在这里插入图片描述

1.兄弟节点没得借

1.兄弟节点子节点为空

2.兄弟节点子节点为黑色

在这两中情况下是没有办法借的,只能将兄弟节点变为红色X指向父节点,以X父节点继续向上循环判断。

2.兄弟节点有得借

如果兄弟节点的右节点为黑色需要对兄弟节点进行右旋,个人理解如下:

  1. 兄弟结点为黑结点,说明是真正的兄弟结点。把红黑树转换为2-3-4树,可以看出,兄弟结点为3结点(上黑下红),属于兄弟结点有得借的情况(可以借出去一个当父结点)。
  2. 这时,为了能够让兄弟结点借出去一个结点,假设让父结点左旋,那么就会发生红节点脱钩,使得本该为3结点的兄弟结点强制被拆除,显然这方法不正确的,需要先调整兄弟结点再进行结点外借。
  3. 由于兄弟结点是3结点,调整兄弟结点可以先对兄弟结点进行右旋,再变色以维持3结点上黑下红的状态。这时,可以发现,如果对兄弟结点进行结点外借,不会发生红结点脱钩的现象了。
  4. 最后对父结点进行左旋,使得兄弟结点的其中一个结点成为新的父结点,这时,兄弟结点成功借出去一个。

在这里插入图片描述

兄弟节点的右节点不为黑色,根据父节点左旋

3.具体实现
/**
 * 删除调整
 *  左旋是为了把x节点凑成一个3节点或者4节点,这样才能删除x,要不然x单独就是个2节点,在234树里是不能删除的
 * @param x 调整的节点
 */
private void balanceDeletion(RBNode<K, V> x) {
    //p不等于根节点并且p是黑色
    while (x != root && !getColor(x)) {
        //x是父亲的左节点
        if (x == getLeft(getParent(x))) {
            //兄弟节点
            RBNode<K, V> xpr;
            //如果兄弟节点是红色,两个子节点是黑色 不能直接借
            if ((xpr = getRight(getParent(x))) != null && getColor(xpr)) {
                //兄弟点变黑色
                setColor(xpr, false);
                //p父节点变红
                setColor(getParent(x), true);
                //围绕x的父节点左旋
                rotateLeft(getParent(x));
                xpr = getRight(getParent(x));
            }
            //判断兄弟节点是否存在两个黑色节点(或者两个子节点都为空)
            if (!getColor(getLeft(xpr)) && !getColor(getRight(xpr))) {
                //将兄弟点变成红色
                setColor(xpr, true);
                //根据p的父节点继续往上递归
                x = getParent(x);
            } else {
                //兄弟节点的右节点如果是黑色
                if (!getColor(getRight(xpr))) {
                    //兄弟节点变红
                    setColor(xpr, true);
                    //兄弟节点的左节点变黑
                    setColor(getLeft(xpr), false);
                    //将兄弟节点右旋
                    rotateRight(xpr);
                    xpr = getRight(getParent(x));
                }
                //兄弟节点颜色变为p父节点颜色
                setColor(xpr, getColor(getParent(x)));
                //p父节点变黑
                setColor(getParent(x), false);
                //兄弟节点的右节点变黑
                setColor(getRight(xpr), false);
                //根据p的父节点节点左旋
                rotateLeft(getParent(x));
                break;
            }
        } else {
            RBNode<K, V> ppr;
            if ((ppr = getLeft(getParent(x))) != null && getColor(ppr)) {
                setColor(ppr, false);
                setColor(getParent(x), true);
                rotateRight(getParent(x));
                ppr = getLeft(getParent(x));
            }
            if (!getColor(getLeft(ppr)) && !getColor(getRight(ppr))) {
                setColor(ppr, true);
                x = getParent(x);
            } else {
                if (!getColor(getLeft(ppr))) {
                    setColor(ppr, true);
                    setColor(getRight(ppr), false);
                    rotateLeft(ppr);
                    ppr = getLeft(getParent(x));
                }
                setColor(ppr, getColor(getParent(x)));
                setColor(getParent(x), false);
                setColor(getLeft(ppr), false);
                rotateRight(getParent(x));
                break;
            }
        }

    }
    //将p节点变为黑色
    setColor(x, false);
}

/**
 * 获取节点的颜色
 *
 * @param node
 * @return 如果节点为空 返回false(黑色) 否则返回节点的颜色
 */
private boolean getColor(RBNode<K, V> node) {
    return node != null && node.red;
}

private RBNode<K, V> getParent(RBNode<K, V> node) {
    return node != null ? node.parent : null;
}

private RBNode<K, V> getLeft(RBNode<K, V> node) {
    return node != null ? node.left : null;
}

private RBNode<K, V> getRight(RBNode<K, V> node) {
    return node != null ? node.right : null;
}

private void setColor(RBNode<K, V> node, boolean color) {
    if (node != null) {
        node.red = color;
    }
}

4.总结

在红黑树操作里面,主要是当黑色节点发生变化的时候,围绕红黑树的特性进行逐一分析处理,最终的结果是维持平衡。而在这些判断中,有很多都是相似的只是操作的方向不同。在本文中我们主要以操作节点在左边这种情况,进行分析的。插入的时候判断操作节点的父节点是左子节点,删除的时候判断操作节点是左子节点。了解其中的一种即可。

源码地址:https://gitee.com/gaohwh/jdk-sources.git

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值