数据结构之红黑树

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构(Java版) 

 数据结构之AVL树-CSDN博客 通过上篇文章的学习,我们知道了AVL树的一些情况,同时也实现了AVL树的插入与验证,但同时也知道了AVL树在查找方面性能非常高,但是在频繁的插入和删除时的效率就比较低了。因此我们今天来学习一种相对效率较高的数据结构——红黑树。

目录

红黑树概念

红黑树的性质

红黑树节点的定义

红黑树的插入

红黑树的验证 

AVL树和红黑树的比较 


红黑树概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。如下图所示:

注意:这里的 NIL 是空节点的意思,但是在红黑树中空节点是我们平常所说的叶子节点,并且这个节点的颜色为黑色。

红黑树的性质

1、每个结点不是红色就是黑色;
2、根节点是黑色的;
3、如果一个节点是红色的,则它的两个孩子结点是黑色的,也就是说 没有2个连续的红色节点;
4、对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点数;
5、每个叶子结点都是黑色的(此处的叶子结点指的是空结点);

由上述性质,便可推出:其中最长路径中节点个数不会超过最短路径节点个数的两倍。如下验证:

红黑树节点的定义

    static class TreeNode {
        public int val;
        public COLOR color; // 颜色
        public TreeNode left; // 左孩子
        public TreeNode right; // 右孩子
        public TreeNode parent; // 父亲

        public TreeNode(int val) {
            this.val = val;
            this.color = COLOR.RED; // 默认是红色的节点
        }
    }

之所以把红黑树的节点默认定义为红色,是因为在插入元素时,如果节点为黑色,就会改变从根结点不同路径到叶子结点之间的黑色节点的个数,修改十分困难,而节点为红色,只是可能会违反不能出现两个连续是红色节点的情况,修改起来比较容易。

红黑树的插入

枚举颜色:

public enum COLOR {
    RED,BLACK;
}

根结点: 

public TreeNode root;

思路:和二叉搜索树一样,先得找到合适的位置进行插入节点的操作。 

        // 判断根结点是不是为 null
        if (root == null) {
            root = new TreeNode(val);
            root.color = COLOR.BLACK; // 根结点要改为黑色
            return true;
        }

        // 开始寻找要插入节点的位置
        TreeNode cur = root;
        TreeNode parent = null;
        while (cur!= null) {
            parent = cur;
            if (cur.val < val) {
                cur = cur.right;
            } else if (cur.val > val) {
                cur = cur.left;
            } else {
                return false; // 不能插入相同的元素
            }
        }
        // parent 是要插入的前一个位置(判断是哪棵树)
        TreeNode node = new TreeNode(val);
        if (val < parent.val) {
            parent.left = node;
        }else {
            parent.right = node;
        }
        node.parent = parent;
        cur = node;

插入操作完成之后,就得判断当前红黑树是否违反了其性质:不能出现连续的红色节点。

        // 判断是否需要调整
        if (parent.color == COLOR.BLACK) {
            // 父亲节点为黑色,则没有改变性质,无需调整
            return true;
        }

接下来就是需要调整的, 根据叔叔节点是否存在和颜色划分了大概两种情况:

因为 grandfather节点原先的颜色是黑色,当调整为红色之后,可能会出现:有连续的红色节点的情况,因此还得继续往上判断,直至parent为null即可。

从这里我们可以发现:其实叔叔节点为黑色和叔叔节点不存在的处理方式是一样的。 

当然,上面两种只是主要的情况,还有 parent 和 uncle 所处的位置不同,以及cur 所处的位置不同步,会细分出很多种情况。

检查修改代码实现:

        // 判断是否需要调整
        if (parent.color == COLOR.BLACK) {
            // 父亲节点为黑色,则没有改变性质,无需调整
            return true;
        }

        // 父亲节点为红色,则与性质发生冲突,需要调整
        while (parent!= null && parent.color == COLOR.RED) {
            // 判断叔叔节点的颜色是啥,根据这个来判断调整方式
            TreeNode grandfather = parent.parent;
            // 根据叔叔节点和父亲节点位置来划分不同的情况
            if (parent == grandfather.left) {
                // 再判断叔叔节点是否存在,根据叔叔节点的情况来处理
                TreeNode uncle = grandfather.right;
                if (uncle!= null && uncle.color == COLOR.RED) {
                    // 修改叔叔节点、父亲节点和祖父节点的颜色,然后继续判断
                    grandfather.color = COLOR.RED;
                    uncle.color = COLOR.BLACK;
                    parent.color = COLOR.BLACK;
                    cur = grandfather;
                    parent = cur.parent;
                } else { // uncle 为 null 或者 uncle 为黑色
                    // 判断 cur 的位置在 parent 的哪边
                    if (cur == parent.left) {
                        // uncle 为 null 和不为 null 直接右旋祖父节点,然后再修改颜色
                        rotateRight(grandfather);
                        grandfather.color = COLOR.RED;
                        parent.color = COLOR.BLACK;
                    } else { // cur == parent.right
                        // 左旋parent,再交换
                        rotateLeft(parent);
                        TreeNode tmp = parent;
                        parent = cur;
                        cur = tmp;
                        // 经过上面的处理,变成了cur为parent的left的情况
                        rotateRight(grandfather);
                        grandfather.color = COLOR.RED;
                        parent.color = COLOR.BLACK;
                    }
                }
            } else { // parent == grandfather.right
                // 再判断叔叔节点是否存在,根据叔叔节点的情况来处理
                TreeNode uncle = grandfather.left;
                if (uncle!= null && uncle.color == COLOR.RED) {
                    // 修改叔叔节点、父亲节点和祖父节点的颜色,然后继续判断
                    grandfather.color = COLOR.RED;
                    uncle.color = COLOR.BLACK;
                    parent.color = COLOR.BLACK;
                    cur = grandfather;
                    parent = cur.parent;
                } else { // uncle 为 null 或者 uncle 为黑色
                    // 判断 cur 的位置在 parent 的哪边
                    if (cur == parent.right) {
                        rotateLeft(grandfather);
                        grandfather.color = COLOR.RED;
                        parent.color = COLOR.BLACK;
                    } else { // cur == parent.left
                        // 先变为cur == parent.right的情况
                        rotateRight(parent);
                        TreeNode tmp = parent;
                        parent = cur;
                        cur = tmp;
                        // 再进行上面的处理
                        rotateLeft(grandfather);
                        grandfather.color = COLOR.RED;
                        parent.color = COLOR.BLACK;
                    }
                }
            }
        }
        root.color = COLOR.BLACK; // 把根结点修改为黑色
        return true;

左单旋代码实现:(思路可见AVL树的博客文章)

    private void rotateLeft(TreeNode parent) {
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;

        TreeNode parentP = parent.parent;
        // 修改四个指针的指向
        parent.parent = subR;
        parent.right = subRL;
        if (subRL != null) {
            subRL.parent = parent;
        }
        subR.left = parent;
        // 修改本级根结点和上级树的指向
        if (root == parent) {
            root = subR;
            root.parent = null;
        } else {
            subR.parent = parentP;
            if (parentP.left == parent) {
                parentP.left = subR;
            } else {
                parentP.right = subR;
            }
        }
    }

右单旋代码实现:(思路可见AVL树博客文章)

    private void rotateRight(TreeNode parent) {
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;

        TreeNode parentP = parent.parent;
        // 修改四个指针的指向
        parent.parent = subL;
        parent.left = subLR;
        if (subLR != null) {
            subLR.parent = parent;
        }
        subL.right = parent;
        // 修改本级根结点和上级树的指向
        if (parent == root) {
            root = subL;
            root.parent = null;
        } else {
            subL.parent = parentP;
            if (parentP.left == parent) {
                parentP.left = subL;
            } else {
                parentP.right = subL;
            }
        }
    }

处理的思路:

1、先判断父亲节点和叔叔节点在哪边;

2、再根据叔叔节点是否存在以及颜色来分别处理:

        2.1、叔叔节点存在且为红色,就是修改颜色再继续检查;

        2.2、叔叔节点不存在(NIL也是黑色)或者叔叔节点为红色,就是进行旋转处理(旋转需要判断cur具体是在哪边,从而进行不同的旋转); 

红黑树构建完成之后,就得开始验证这棵二叉树是不是一棵红黑树。

红黑树的验证 

我们是利用红黑树的性质来进行验证的。

1、是一棵二叉搜索树。因此采取中序遍历的方式输出,看是不是一棵二叉搜索树;

代码实现:

    // 中序遍历:检查是否为有序序列
    public void inorder(TreeNode root) {
        if(root == null) {
            return;
        }
        inorder(root.left);
        System.out.print(root.val+" ");
        inorder(root.right);
    }

  2、根结点一定得是黑色的;

3、不能出现两个连续的红色节点;

思路:遍历树的所有结点,判断这个节点的颜色是不是和其父亲节点一样都是红色。如果是,则返回false,否则一直判断下去,直至全部遍历完成,返回true。

代码实现:

    private boolean isContinuousRedNodes(TreeNode root) {
        if (root == null) {
            return true;
        }
        // 该节点为红色
        if (root.color == COLOR.RED) {
            // 其父亲节点也为红色
            if (root.parent != null && root.parent.color == COLOR.RED) {
                System.out.println("违反了性质:连续出现了两个红色的节点");
                // return false; 这个可有可无
            }
        }
        // 递归判断左子树和右子树
        return isContinuousRedNodes(root.left) && isContinuousRedNodes(root.right);
    }

4、从根结点到不同路径上的叶子节点之间的黑色节点个数要相同。 

思路:先记录任意一条从根结点到叶子结点之间的黑色节点的个数。然后再遍历树的路径判断是否一致。如果不一致,就返回false,一致就一直判断,直至返回true。

代码实现:

    // 求某条路径上的所有黑色节点个数
    private int getBlckCount(TreeNode root) {
        TreeNode cur = root;
        int count = 0;
        while (cur != null) {
            if (cur.color == COLOR.BLACK) {
                count++;
            }
            cur = cur.left;
        }
        return count;
    }


    private boolean sameBlackNodes(TreeNode root, int pathBlack, int sameBlackCount) {
        if (root == null) {
            return true;
        }
        if (root.color == COLOR.BLACK) {
            pathBlack++;
        }
        // 到叶子节点了,就得进行比较
        if (root.left == null && root.right == null) {
            if (pathBlack != sameBlackCount) {
                System.out.println("违反了性质:每条路径上黑色的节点个数是不一样的!");
                // return false; 可有可无
            } 
        }
        return sameBlackNodes(root.left, pathBlack, sameBlackCount)
                && sameBlackNodes(root.right, pathBlack, sameBlackCount);
    }

剩余代码:

    public boolean isRBTree(TreeNode root) {
        // 就是看是否满足红黑树的性质
        // 1、根结点是不是黑色
        if (root.color != COLOR.BLACK) {
            System.out.println("违反了性质:根节点必须是黑色的!");
        }
        int sameBlackCount = getBlckCount(root);
        // 是不是存在两个连续的红色节点 && 是不是不同;路径上有相同的黑色节点个数
        return isContinuousRedNodes(root) && sameBlackNodes(root, 0, sameBlackCount);
    }

测试代码:

public class Test {
    public static void main(String[] args) {
        //int[] array =  {16, 3, 7, 11, 9, 26, 18, 14, 15};
        int[] array = {4, 2, 6, 1, 3, 5, 15, 7, 16,14};
        RBTree rbTree = new RBTree();
        for (int i = 0; i < array.length; i++) {
            rbTree.insert(array[i]);
        }
        System.out.println(rbTree.isRBTree(rbTree.root));
        rbTree.inorder(rbTree.root);
    }
}

AVL树和红黑树的比较 

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比 AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。 就像在 Java集合框架中的:TreeMap、TreeSet底层使用的就是红黑树。

好啦!本期 数据结构之红黑树 的学习之旅就到此结束啦!我们下一期再一起学习吧!

评论 88
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要学编程(ಥ_ಥ)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值