【数据结构与算法】红黑树 详解

简介

红黑树是一种特殊的二叉搜索树,满足如下三个性质:
1.颜色:节点要么红要么黑,根节点和叶子节点为黑色;
2.红色节点不能相邻,或者说红色节点的孩子节点必为黑色;
3.任意节点到其叶子结点的所有路径包含相同数目的黑色节点;

好,红黑树的定义就是这么简单,那么这么定义的意义何在?我们知道普通的二叉搜索树已经具备了一定的查询效率,但是由于插入和删除操作,树的高度可能变高,甚至退化为链表,这种情况就导致各种操作的效率变低,因为对二叉树的操作都是正比于树高的。如果我们规定了红黑树的这些性质,可以保证红黑数的树高有一个上届,那么任意操作就会有了保证。下面我们就来介绍这个性质的证明过程。

证明

一棵n个节点的红黑树高度至多是2lg(n+1).

即证明:
h <= 2lg(n+1)

两边取指数,等价于证明:2^(h/2) - 1 <= n,即高度为h的红黑树至少包含2^h/2 - 1个节点。这样就将证明中的条件从节点数固定转移到了高度固定。
我们知道,高度h的红黑树至多包含2^h - 1的节点,但是这里要求证明一个下届。

引理一:高度为h的二叉树,至多包含2^h - 1个节点。
引理二:高度为h的红黑树,根节点到叶节点的路径上,至少包含h/2个黑节点。否则就可能出现红节点相连或者叶节点和根节点不是黑节点的情况。这里也可以理解为任意节点的黑高即bh(x)至少为h(x)/2。黑高bh(x)就是节点x到其任意叶子结点路径上黑节点的个数。
引理三:任意节点x至少包含2^bh(x) - 1个节点。

引理三的证明:任意节点x的两棵子树的黑高至少为bh(x) - 1,减不减一取决于x是否为黑色。那么,考虑节点最少的情况,即x的子树全部为黑节点。因为每条路径至少包含bh(x) - 1个黑节点,如果全是黑节点,子树高度就是bh(x) - 1,那么每一棵子树包含的节点为2^(bh(x) - 1) - 1个,此时x为根的子树节点数为2^(bh(x) - 1) - 1 + 2^(bh(x) - 1) - 1 + 1 = 2^bh(x) - 1。

有了这三个引理以后,我们知道高度为h的子树,节点数至少为2^bh(x) - 1。即n >= 2^bh(x) - 1。又根据引理二bh(x) >= h/2,可以得到n >= 2^(h/2) - 1,于是得证。

左旋右旋

左旋右旋可以改变左右子树的高度,并且最关键的是保留二叉树的大小性质。使用左旋右旋,可以在删除和插入节点以后维护红黑树的性质。概念可以从下面这个图理解。

代码:

// 以x为轴左旋
    public TreeNode leftRotate(TreeNode root, TreeNode x) {
        TreeNode y = x.right;
        //第一步,将y的左子树设置为x的右子树
        x.right = y.left;
        if (y.left != null) {
            y.left.parent = x;
        }

        //第二步,x的父节点的孩子设置为y
        if (x.parent == null) {
            root = y;
        } else if (x.parent.left == x) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }
        y.parent = x.parent;

        //第三步,x设置为y的左子树
        y.left = x;
        x.parent = y;
        return root;
    }

    // 以x为轴右旋
    public TreeNode rightRotate(TreeNode root, TreeNode x) {
        TreeNode y = x.left;
        x.left = y.right;
        if (y.right != null) {
            y.right.parent = x;
        }

        if (x.parent == null){
            root = y;
        } else if (x.parent.left == x) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }
        y.parent = x.parent;

        y.right = x;
        x.parent = y;
        return root;
    }

    public static void main(String args[]){
        Solution solution = new Solution();
        TreeNode root = new TreeNode(10);
        TreeNode n1 = new TreeNode(1);
        TreeNode n2 = new TreeNode(90);
        TreeNode n3 = new TreeNode(100);
        TreeNode n4 = new TreeNode(80);

        solution.insert(root, n1);
        solution.insert(root, n2);
        solution.insert(root, n3);
        solution.insert(root, n4);
        //System.out.println(new Solution().successor(n1).val);
        solution.print(root);
        solution.print(solution.leftRotate(root, root));
    }

不论左旋还是右旋,都可以分为3个基本的步骤来完成。

 

插入算法

首先介绍一个小技巧:哨兵元素。
正常来说,数据结构诸如链表或者树,最后一个节点或者叶子节点的指针都会指向null,这样会导致我们的算法有空指针的风险,不得不做很多判空处理。但如果我们把null也变成一个节点,只不过是一个我们约定的特殊节点,这个特殊节点就是哨兵节点,所有的叶子节点都可以指向唯一一个哨兵节点,那么我们就将判空修改为检测是否为哨兵节点。看上去好像没少多少操作啊,还是要检测,但是在某些情况下我们可以不检测,后面会有具体例子。

所有在之前的bst的算法基础上,稍作修改,所有的叶子节点的null都变成哨兵节点,为黑色。根节点的父节点也变成黑色哨兵节点。那么插入算法还和之前bst的一样。插入之后,我们将新的节点染成红色。为什么是红色?因为如果是黑色,“含有相同数目的黑色节点的条件就会被打破”,这个想要修复比较困难。而如果是红色,可能会打破“红色节点不相邻”的条件。这个只要区分三种情况做修复。

当插入一个红色节点z,z的后代肯定都是哨兵。
如果z.p是黑色,那么不违反红黑树性质,不需要修复。
如果z.p是红色,这时需要修复。
定义y是z的叔节点,也就是z的父节点的父节点的另一个子节点。y有可能是空吗?不可能,因为至少也会是一个哨兵节点,这里就体现出了哨兵节点的作用,否则的话,我们还需要考虑y是空的情况,那就很麻烦了。然后分三类:
注意,z.p.p一定是黑色的。1.如果y是红色,那么我们只需要把z.p.p染红,把z.p和y染黑即可,相当于是把z.p.p的黑色下放到两个子节点,这样总的黑色节点数没有变化。但是呢?z.p.p又变红了,仍然可能与z.p.p.p冲突,这就是一个递归处理的问题了,等价于此时的z.p.p变成了z,再处理新的z即可。
2.那如果z.p.p是黑色呢?此时就不能通过下放黑色来解决了。需要通过左右旋解决。再分两种情况。
2.1一字型,也就是z以及z.p和y呈一条直线。更具体,z是z.p的左子树,那么y就是y.p的右子树,反之,也是。呈现“一字型”。这种情况,我们只需要:把z.p从红色染成黑色,把z.p.p从黑色染成红色,再以z.p.p为轴朝y一侧旋转即可。
2.2z字型,也就是z以及z.p和y不是直线。更具体,z是z.p的右子树,y也是y.p的右子树,反之,也是。呈现“z字型”。这种情况,我们需要先执行一次内部旋转:以y是右子树,z也是右子树为例。需要以z.p为轴左旋一次。左旋以后,就变成了“一字型”,然后再按照2.1的情况处理即可。
总之,如果插入红色节点没有问题,就不处理;如果有问题且叔节点为红色,需要递归处理,z往上移;如果有问题且叔节点为黑色,需要进行旋转操作,之后不需要再递归处理。
下面是《算法导论》的具体例子。


代码由于之前写的是bst的,没有哨兵元素,暂时没写。

 

删除算法

红黑树删除远比插入复杂的多 整体思路和插入一样 先按照bst删除 然后fix红黑树性质

关于删除操作 和bst的类似 只是多了几步 会标记几个特殊节点 z是需要删除节点 y是实际被删除的节点 x是需要被调整的节点 举例来说 如果z至多有一个子树 那么y就是z y的子树就是x 否则呢 y就是的z后继 x是y的子节点 此时 我们会用y代替x y的颜色和z染成一样的 然后把x代替y 其实呢 删除的是y 因为我们用原始的y代替了z 包括颜色 所以说 y才是真正删除的节点 x是需要被调整的节点 这是删除操作

之后就要根据y的原始颜色来从x调整红黑树 那我们就先要明白哪些性质可能被违反了 就删除后的红黑树而言
如果y原始颜色为红色 不会违反性质 因为首先不影响黑色节点数目 其次 x和x.p都是黑色 
那如果y原始颜色为黑色呢 就可能违反了
1 x和x.p可能都是红色 违反性质
2 删除y后 原本包含y的路径上少了一个黑色

我们先看2 少了黑色 我们其实可以让代替y的x额外加上一层黑色 这样 就会保持黑色节点数目不变了 但是呢 会违反另一个性质 即红黑树节点要么黑色要么红色 不能双色 另外注意这个额外的黑色不体现在节点的color属性上 而是提现在指针上 不是说会固定在某一个节点上 而是会转移到其他节点 那么我们的fix过程其实就是消除这一层额外黑色的过程

看下如何fix的 fix是从x节点开始的 这里假设x是x.p的左子树 右子树情况是镜像式的 再定义w为x.p的右子树
如果w是红色 我们需要把w变成黑色 为什么?因为黑色可以终结循环,为什么可以终结在下面的case说明。好,w是红色,那么w.p也即x.p是黑色 w的子节点也是黑色。只需交换w.p和w的颜色,然后以w为轴左旋,此时x的兄弟节点就变了,变成了w的左子树,左子树之前提过是黑色,这样新的w就成了黑色。我们就可以进入下面的case处理了。
如果w是黑色 又可以分为3种情况。
1)w的子树都是黑色 
2)w的左子树是红色,右子树是黑色
3)w的右子树是红色
以上三种情况,x.p的颜色都是未知的。

针对1),因为w的子树都是黑色的,所以我们可以把w染成红色,把x的额外的黑色和w的黑色上移至x.p,此时x.p成了新的双色节点。如果x.p本身是红色的,这就是一个终结条件,只需要再把x.p染成黑色即可,在x.p处消化了额外的黑色。那如果x.p原本是黑色的,接下来就需要递归处理了,x.p变成了新的x。
针对2),我们其实要把2)转化为3),为什么?因为通过对3)的分析可以知道3)也是一种终结条件。如何变成3)呢?只需要交换w和w左子树的颜色,再以w为轴右旋即可。新的w的左右子树的黑色节点数目没有变,因为增加和删除的都是红色节点。这样就变成了3)。
针对3),此时只知道w右子树是红色的,w左子树是未知的。需要交换w和w.p的颜色,并且将w右子树染成红色,再以w.p为轴左旋,然后就可以直接消除x的额外黑色了。可以通过严格证明来解释。那么从感官上如何理解?其实我们是使用w的右子树的红色,染黑增加x.p右侧的黑色,再通过旋转把x.p移到左侧,为原本的x.p的左子树增加了一个黑色节点,同时维持右侧黑色节点数目不变,这样,就可以删除x的额外黑色了。大致是这样的。通过染黑红色节点和旋转可以为子树的一侧增加黑色节点从而移除额外黑色。

《算法导论》四种case例子:


插入操作的技巧在于一个是黑色下放 来抵消红色,另一个是通过交换颜色加上旋转来调整红色 本质都是调整节点的颜色 来维持红黑树性质
删除操作的技巧则是要掌握1)和3)的两个终结条件,其余的都是为了转移到这两个case的。然后再通过红色节点染黑和旋转来消除额外黑色。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值