红黑树的那些事

这篇文章基本来自《码出高效》这本书, 由我自己总结归纳一些基础性的知识。
部分图和源代码来自于CarpenterLee博客

从最简单的树说起

1.树(Tree)

相对来说,树是一个很基础的概念, 不需要去多谈。
需要掌握两个概念:
深度:从根节点出发,到某节点边的条数。
高度:从某结点出发,到叶子节点为止, 最长简单路径上边的条数。

2.平衡二叉树

高度差为 1 的二叉树。
其性质如下:
(1)树的左右高度不能超过 1
(2)任何往下递归的左子树和右子树, 必须符合第一条性质。
(3)没有任何节点的空树或只有根节点的树也是平衡二叉树。

3.二叉查找树(又名二叉搜索树,Binary Search Tree)

性质:对于任意节点,它的左子树上所有节点的值都小于他, 而他的右子树上的所有节点的值都大于他。
遍历节点的三种方式:前序遍历、中序遍历、后序遍历。**他们三者规律如下**
(1)在任何递归子树中, 左节点一定在右节点之前遍历。
(2)前序、中序、后序,仅指根节点在遍历时的位置顺序。

AVL 树与树形旋转

AVL是一种平衡二叉查找树, 增加或删除节点后通过树形旋转重新达到平衡。

右旋:以某个节点为中心, 将他沉入当前子节点的位置, 而让他当前的子节点作为新树的根节点,也称为顺时针旋转;
左旋:以某个节点为中心, 将他沉入当前子节点的位置, 而让他当前的子节点作为新树的根节点,也称为逆时针旋转;

左旋示意图
在这里插入图片描述

左旋源代码

//Rotate Right
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

红黑树

红黑树和 AVL 树类似,但不准求所有的递归子树高度差不超过 1,而是保证**从根节点到叶尾的最长路径不超过最短路径的两倍,所以他的最坏运行时间也是 O ( log n ) O(\text{log}n) O(logn)
它的五个约束条件:
1.节点只能是红色或者黑色
2.根节点必须是黑色
3.所有 NIL 1 ^1 1 节点都是黑色
4.一条路径上不能出现相邻的两个红色节点
5.在任何递归子树内,根节点到叶子节点的所有路径上包含相同数目的黑色节点

【说明1】NIL 指在叶子节点不存在的两个虚拟节点,默认是黑色的。

红黑树 VS AVL树

再插入时, 红黑树和AVL树都能在至多两次旋转内恢复平衡。在删除时由于红黑树只追求大致上的平衡, 因此红黑树能在至多三次旋转内恢复和平;而 AVL 树追求绝对平衡, 至多旋转** O ( log n ) O(\text{log}n) O(logn)**次。

因此,面对频繁的插入和删除,红黑树较为合适;面对低频修改、大量查询时,AVL相对合适。

TreeMap

TreeMap 是按照 Key 的排序结果来组织内部结构的 Map 类集合, 它改变了 Map 类散乱无序的形象。 虽然 TreeMap 没有 ConcurrentHashMap 和 HashMap 普及(毕竟插入和删除的效率远没有后两者高),但是在 Key 有排序要求的场景下, 使用 TreeMap 可以事半功倍。

在 TrreMap 的接口继承树, 有两个接口:SortedMap 和 NavigableMap。SortedMap 接口表示它的 Key 是有序不可重复的, 支持获取头尾 Key-Value 元素, 或者根据 Key 指定范围获取子集合。插入的 Key 必须实现 Comparable 或提供额外的比较器 Comparator ,所以 Key 不能是 null ,但是 Value 可以;
NavigableMap 接口继承了 SortedMap ,根据指定的搜索条件返回最匹配的 Key-Value 元素。 不同于 HashMap ,TreeMap 并非一定要覆写 hashCode()equals() 方法来达到Key去重的目的。

关于 TreeMap 和 HashMap 的例子请看《码处高效》P191-P192 ,这里不多做赘述。

类名与属性

public class TreeMap<K,V> extends AbstractMap<K,V> 
    implements NavigableMap<K,V>, Cloneable,java.io.Serializable{
    //排序时用的比较器,put源码解析时会用到
    private final Comparator<? super K> Comparator;
    //根节点,put源码时会提到
    private transient Entry<K,V> root;
    //定义成为字面含义的常量。下方fixAfterInsertion()解析时会用到
    private static final boolean RED = false;
    private static final boolean BLACK = true;

    //TreeMap 的内部类, 存储红黑树节点的载体类 ,在整个TreeMap 中高频出现
    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left; //指向左子树的引用
        Entry<K,V> right; //指向右子树的引用
        Entry<K,V> parent; //指向父节点的引用
        //节点颜色信息是红黑树的精髓所在,默认是黑色
    }
}

TreeMap 通过 put()deleteEntry() 实现红黑树的增加和删除节点操作,下面的源码分析以插入主流程为例。
再插入新节点之前,要明确三个前提条件:

  1. 需要调整的新节点总是红色的。
  2. 如果插入新节点的父节点是黑的,无需调整,因为依然能符合红黑树的5个约束条件。
  3. 如果插入新节点的父节点是红色的,因为红黑树规定不能出现相邻的两个红色节点,所以进入循环判断,或重新着色,或左右旋转,最终达到红黑树的五个约束条件,退出条件如下:
while(x != null && x != root && x.parent.color == RED)

put 源码

public V put(K key, V value) {
    // t 表示当前节点,记住这个很重要! 先把 TreeMap 的根节点root引用赋值给当前节点
    Entry<K,V> t = root;
    // 如果当前节点为 null ,即是空树,新增的 KV 形成的节点就是根节点
    if (t == null) {
        //看似多此一举, 实际上预检了Key是否可以比较
        compare(key, key);

        //使用 KV 构造出的新的 Entry 对象, 其中第三个参数是 parent,根节点没有父节点
        root = new Entry<K,V>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    // 记录比较结果
    int cmp;
    Entry<K,V> parent;
    // 构造方法中置入的外部比较器
    Comparator<? super K> cpr = comparator ;
    // 重要步骤:根据二叉树的性质, 找到新节点插入合适的位置
    if (cpr != null) {
         // 循环的目标:根据参数 Key 与当前节点的 Key 不断进行对比
        do {
            // 当前节点赋值给父节点, 故从根节点开始遍历比较
            parent = t;
            // 比较输入参数 Key 和当前节点 Key 的大小
            cmp = cpr.compare(key, t. key);
             // 参数的 Key 更小,向左面走,把当前接待你引用移动到他的左子节点上
            if (cmp < 0)
                t = t. left;
            // 新插入的key大的话,则以当前节点的右孩子节点为新的比较节点
            else if (cmp > 0)
                t = t. right;
            else
          // 如果当前节点的key和新插入的key相等的话,则覆盖map的value,返回
                return t.setValue(value);
        // 如果没有相等的 Key, 一直会遍历到 NIL 节点为止
        } while (t != null);
    }
    //在没有指定比较器的情况下, 调用自然排序的 Comparable 比较
    else {
        // 这里要求key不能为空,并且必须实现Comparable接口
        if (key == null)
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;
        // 和上面一样,喜欢查找新节点要插入的位置
        do {
            parent = t;
            cmp = k.compareTo(t. key);
            if (cmp < 0)
                t = t. left;
            else if (cmp > 0)
                t = t. right;
            else
                return t.setValue(value);
        } while (t != null);
    }

    // 找到新节点的父节点后,创建节点对象
    Entry<K,V> e = new Entry<K,V>(key, value, parent);
    // 如果新节点key的值小于父节点key的值,则插在父节点的左侧
    if (cmp < 0)
        parent. left = e;
    // 如果新节点key的值大于父节点key的值,则插在父节点的右侧
    else
        parent. right = e;
    // 插入新的节点后,为了保持红黑树平衡,对红黑树进行调整(重新着色、旋转操作)
    fixAfterInsertion(e);
    // map元素个数+1
    size++;
    modCount++;
    return null;
}

重点:fixAfterInsertion() 源码

/** 新增节点后对红黑树的调整方法 */
private void fixAfterInsertion(Entry<K,V> x) {
    // 虽然内部类 Entry 的属性 color 默认为黑色, 但新节点一律先赋值为红色
    x. color = RED;

    // 新节点是根节点或者其父节点(简称父亲)为黑色,
    // 插入红色节点并不会破坏红黑树的性质,无需调整。
    // x 值的改变已被特别标记为(*),改变的过程是在不断地向上游遍历(或内部调整)
    // 直到父亲为黑色, 或者到达根节点
    while (x != null && x != root && x. parent.color == RED) {
        // 如果新插入节点x的父节点是祖父节点的左孩子
        if (parentOf(x) == leftOf(parentOf (parentOf(x)))) {
            // 取得新插入节点x的叔叔节点
            Entry<K,V> y = rightOf(parentOf (parentOf(x)));
            // 如果新插入x的父节点是红色-------------------①
            if (colorOf(y) == RED) {
                // 将x的父节点设置为黑色
                setColor(parentOf (x), BLACK);
                // 将x的叔叔节点设置为黑色
                setColor(y, BLACK);
                // 将x的祖父节点设置为红色
                setColor(parentOf (parentOf(x)), RED);
                // 将x指向祖父节点,如果x的祖父节点的父节点是红色,按照上面的步奏继续循环
                x = parentOf(parentOf (x));
            } else {
                // 如果新插入x的叔叔节点是黑色或缺少,且x的父节点是祖父节点的右孩子-------------------②
                if (x == rightOf( parentOf(x))) {
                    // 左旋父节点
                    x = parentOf(x);
                    rotateLeft(x);
                }
                // 如果新插入x的叔叔节点是黑色或缺少,且x的父节点是祖父节点的左孩子-------------------③
                // 将x的父节点设置为黑色
                setColor(parentOf (x), BLACK);
                // 将x的祖父节点设置为红色
                setColor(parentOf (parentOf(x)), RED);
                // 右旋x的祖父节点
                rotateRight( parentOf(parentOf (x)));
            }
        } else { // 如果新插入节点x的父节点是祖父节点的右孩子,下面的步奏和上面的相似,只不过左旋右旋的区分,不再细讲
            Entry<K,V> y = leftOf(parentOf (parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf (x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf (parentOf(x)), RED);
                x = parentOf(parentOf (x));
            } else {
                if (x == leftOf( parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf (x), BLACK);
                setColor(parentOf (parentOf(x)), RED);
                rotateLeft( parentOf(parentOf (x)));
            }
        }
    }
    // 最后将根节点设置为黑色,不管当前是不是红色,反正根节点必须是黑色
    root.color = BLACK;
}

这里只展开说一下左旋:

/**
 * 对红黑树的节点(x)进行左旋转
 *
 * 左旋示意图(对节点x进行左旋):
 *      px                              px
 *     /                               /
 *    x                               y               
 *   /  \      --(左旋)--           / \                
 *  lx   y                          x  ry    
 *     /   \                       /  \
 *    ly   ry                     lx  ly 
 *
 */
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        // 取得要选择节点p的右孩子
        Entry<K,V> r = p. right;
        // "p"和"r的左孩子"的相互指向...
        // 将"r的左孩子"设为"p的右孩子"
        p. right = r.left ;
        // 如果r的左孩子非空,将"p"设为"r的左孩子的父亲"
        if (r.left != null)
            r. left.parent = p;

        // "p的父亲"和"r"的相互指向...
        // 将"p的父亲"设为"y的父亲"
        r. parent = p.parent ;
        // 如果"p的父亲"是空节点,则将r设为根节点
        if (p.parent == null)
            root = r;
        // 如果p是它父节点的左孩子,则将r设为"p的父节点的左孩子"
        else if (p.parent. left == p)
            p. parent.left = r;
        else             
            // 如果p是它父节点的左孩子,则将r设为"p的父节点的左孩子"
            p. parent.right = r;
        // "p"和"r"的相互指向...
        // 将"p"设为"r的左孩子"
        r. left = p;
        // 将"p的父节点"设为"r"
        p. parent = r;
    }
}

在这里插入图片描述

具体例子请看《码出高效》P197-P199

更多数据结构

请访问我的博客-数据结构分类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值