数据结构-二叉树-应用-BST&AVL&RBT

文章目录

数据结构-二叉树-应用-BST&AVL&RBT

二叉排序树(BST)

基础

需求

给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加。

  1. 使用数组

    (1)数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢.

    (2)数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。

  2. 使用链式存储-链表

    不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。

解决方案 - 使用二叉排序树

二叉排序树介绍

数据结构-二叉树-应用-BST&AVL&RBT

二叉排序树又名二叉搜索树和二叉查找树。

二叉排序树为上述问题的的一个很好的解决方案,它对数据的查询和添加都有很高的性能。(当然,指的是平均情况下)

二叉排序树代码详解

全代码(创建树,(中序)遍历,添加结点,删除结点,和其他方法)

// 测试代码
public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7,3};
        BinarySortTree binarySortTree = new BinarySortTree();

        for (int i = 0;i < arr.length;i++){
            binarySortTree.addNode(new Node(arr[i]));
        }

        System.out.println("中序遍历二叉排序树~");
        binarySortTree.infixOrder();

        System.out.println("*******");
//        binarySortTree.deleteNode(2);
//        binarySortTree.deleteNode(1);
//        binarySortTree.deleteNode(1);
        binarySortTree.deleteNode(7);
        binarySortTree.infixOrder();

    }

}

// BST
class BinarySortTree {
    private Node root;

    //添加结点的方法
    public void addNode(Node node){
        if (root == null){
            root = node;
        } else {
            root.addNode(node);
        }
    }

    // 删除结点的方法
    public void deleteNode(int value){
        if (root == null){
            return;
        } else {
            Node targetNode = search(value);
            // 没有找到
            if (targetNode == null){
                return;
            }
            // 如果找到,首先有一个特殊情况:整个数只有目标数
            if (root.right == null && root.left == null){
                root = null;
            }

            Node parent = searchParent(value);
            // 叶子结点的删除
            if (targetNode.left == null && targetNode.right == null){
                if (parent.left != null && parent.left.value == value){
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value){
                    parent.right = null;
                }
            } else if (targetNode.left != null && targetNode.right != null){// 有两个子节点的结点的删除
                int replaceValue = delRightTreeMin(targetNode.right);
//                int replaceValue = findMinNodeAndReturnValue(targetNode.right);
                targetNode.value = replaceValue;

            } else {// 带有一个子节点(左节点或右节点)的删除
                if (targetNode.left != null){// 子节点为左节点
                    if (parent != null){
                        if (parent.left.value == value){// 该点为它父节点的左节点
                            parent.left = targetNode.left;
                        } else { // 该点为它父节点的右节点
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else {// 子节点为右节点
                    if (parent != null){
                        if (parent.left.value == value){
                            parent.left = targetNode.right;
                        } else {
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }
            }


        }
    }

    public void infixOrder(){
        if (root == null){
            System.out.println("树为空,不能遍历");
        } else {
            root.infixOrder();
        }
    }

    // 查找要删除的结点
    private Node search(int value){
        if (root == null){
            return null;
        } else {
            return root.search(value);
        }
    }

    // 查找父节点
    private Node searchParent(int value){
        if (root == null){
            return null;
        } else {
            return root.searchParent(value);
        }
    }

    // 编写一个特殊作用的方法:用于删除结点方法中用来找到并删除一个子树的最小结点
    // 注:还会返回该结点的值。
    /**
     * @param node 传入的结点(当做二叉排序树的根结点)
     * @return 返回的 以node为根结点的二叉排序树的最小结点的值
     */
    // 老师标准,可多多评鉴,很妙
    private int delRightTreeMin(Node node){
        Node target = node;
        // 循环的查找左子节点,就会找到最小值
        while (target.left != null){
            target = target.left;
        }
        // 这时 target就指向了最小结点
        // 删除最小结点
        deleteNode(target.value);
        return target.value;
    }

}

class Node {
    public int value;
    public Node left;
    public Node right;

    public Node(int value) {
        this.value = value;
    }

    // 添加结点的方法
    // 递归的形式添加结点,注意需要满足二叉排序树的要求
    public void addNode(Node node){
        if (node == null){
            return;
        }

        // 判断传入的结点的值,和当前子树的根结点的值关系
        if (node.value < this.value){
            if (this.left == null){
                this.left = node;
            } else {
                this.left.addNode(node);
            }
        } else {
            if (this.right == null){
                this.right = node;
            } else {
                this.right.addNode(node);
            }
        }

    }

    //中序遍历
    public void infixOrder(){
        if (this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.infixOrder();
        }
    }

    // 查找要删除的结点
    /**
     * @param value 希望删除的结点的值
     * @return 如果找到则返回该结点,否则返回null
     */
    public Node search(int value){
        if (this.value == value){
            return this;
        } else if (this.value > value){
            if (this.left == null){
                return null;
            }
            return this.left.search(value);
        } else {
            if (this.right == null){
                return null;
            }
            return this.right.search(value);
        }
    }


    // 查找要删除结点的父节点
    /**
     * @param value 要找到的结点的值(找它的父节点)
     * @return 返回的是要删除的结点的父节点,如果没有就返回null
     */
    public Node searchParent(int value){
        if ((this.left != null && this.left.value == value)
                ||
                (this.right != null && this.right.value == value)){
            return this;
        } else if (this.value > value){
            if (this.left == null){
                return null;
            }
            return this.left.searchParent(value);
        } else {
            if (this.right == null){
                return null;
            }
            return this.right.searchParent(value);
        }
    }


    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}

删除功能详解

二叉排序树的功能除了删除比较复杂点,其他都挺简单,主要分析一下删除功能的实现。

删除的结点分为三种情况:

  • 需要删除的结点为叶子结点(没有子树)

  • 需要删除的结点有一个子节点(左子节点或右子节点)

  • 需要删除的结点有两个子节点

需要删除的结点为叶子结点(没有子树)

​ 这种情况,只需要找到该结点,然后再找到该节点的父节点,然后用该节点的父节点删除直接删除该节点即可。

需要删除的结点有一个子树(左子树或者右子树)

​ 这种情况,需要先找到该结点,然后找到该结点的父节点,然后,需要确定该结点是父节点的右子树还是左子树,如果是右子树,那么删除该结点后,原来该结点的子节点需要接到该结点的父节点的右子树去,如果是左子树,那么原来该结点的子节点需要接到该结点的父节点的左子树去。

需要删除的结点有两个子树

​ 这种情况,有两种解决方法,我这里学的老韩的视频,讲的是去找该结点的右子树结点。方法是找到需要删除的结点的右子树中的最小结点并删除这个结点(代码实现需要将这个值返回),并把这个结点的值(返回的值)赋给需要删除的结点。

​ 可以想象一下这个树的结构,就是要删除的点,右子树找最小的结点,如果这个右子树不是单个结点,那么肯定是往左边左子树去找,然后一直左子节点的循环或递归的去找,找到后赋值给那个要删除的值,那要删除的值就相当于被删除,然后这个值赋给的那个要删除的值其实就是相当于是为了保持这个树是二叉排序树的结构,因为那个数是右子树的最小的值,所以它肯定比右子树的所有值小嘛,所以右子树都比它大,它放在需要删除的那个结点上的话,右子树首先都成立嘛,然后它原来就是右子树的,所以它肯定比要删除的那个结点的左子树的所有数大,所以左子树就都比它小,左子树也都满足!这样就删除了那个结点,而且也没有破坏二叉排序树的结构。

特殊情况考虑–root结点没有父节点

​ 按照上面的三种情况来写代码实现,可以提取出来的方法就是:查找删除结点、查找父节点、查找并删除一个结点为根结点的子树的最小值并返回该值,以及删除主方法。

​ 但是,在写删除方法的时候,会有一个问题,父节点的话,不是每一个都有,比如根结点root就没有父节点,然后关于根结点的删除有两种情况:

​ 1.整个树只有目标数,即整个树只有root结点,而且需要删除该结点,那么直接赋值null即可。(图一为解决代码)

​ 2.需要删除root,root有左节点或root有右节点。这种情况就需要在第二种情况的判断前面加上判断父节点是否存在了,如果不存在,则是需要删除root结点,而且进入了第二种情况的判断域中,那么肯定有一个左或者右结点或子树,那么需要将该结点直接接到root上面去。(就是赋值给root)(图二中,红线为这种情况套上的代码)

​ 3.有两个结点的情况其实在第三种情况中自动解决了,所以不需要改动或添加什么。

图一:

数据结构-二叉树-应用-BST&AVL&RBT

图二:

数据结构-二叉树-应用-BST&AVL&RBT

平衡二叉树(AVL)

基础

对于BST的一种情况(需求)

数据结构-二叉树-应用-BST&AVL&RBT

​ 如上图,二叉排序树会有一些极端的情况,这些情况下的二叉排序树的查询速度会受到影响,而简单的改进方法就是平衡二叉树,平衡二叉树是在二叉排序的基础上进行改进得到的。

二叉平衡树介绍

  1. 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
  2. 具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。(注:上面的AVL是指算法。)

平衡二叉树代码详解

平衡二叉树全代码(在前面BST基础上给增加功能加平衡功能)

// 测试代码
public class AVLTreeDemo {
    public static void main(String[] args) {
//        int[] arr = {4,3,6,5,7,8};
//        int[] arr = {10,12,8,9,7,6};
        int[] arr = {10,11,7,6,8,9};

        AVLTree avlTree = new AVLTree();
        for (int i = 0;i < arr.length;i++){
            avlTree.addNode(new Node(arr[i]));
        }

        avlTree.infixOrder();

        System.out.println("平衡后的高度");
        System.out.println(avlTree.getRoot().height());
        System.out.println(avlTree.getRoot().leftHeight());
        System.out.println(avlTree.getRoot().rightHeight());
        System.out.println(avlTree.getRoot().left);


    }
}

class AVLTree {
    private Node root;

    public Node getRoot() {
        return root;
    }

    //添加结点的方法
    public void addNode(Node node){
        if (root == null){
            root = node;
        } else {
            root.addNode(node);
        }
    }

    // 删除结点的方法
    public void deleteNode(int value){
        if (root == null){
            return;
        } else {
            Node targetNode = search(value);
            // 没有找到
            if (targetNode == null){
                return;
            }
            // 如果找到,首先有一个特殊情况:整个数只有目标数
            if (root.right == null && root.left == null){
                root = null;
            }

            Node parent = searchParent(value);
            // 叶子结点的删除
            if (targetNode.left == null && targetNode.right == null){
                if (parent.left != null && parent.left.value == value){
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value){
                    parent.right = null;
                }
            } else if (targetNode.left != null && targetNode.right != null){// 有两个子节点的结点的删除
                int replaceValue = delRightTreeMin(targetNode.right);
//                int replaceValue = findMinNodeAndReturnValue(targetNode.right);
                targetNode.value = replaceValue;

            } else {// 带有一个子节点(左节点或右节点)的删除
                if (targetNode.left != null){// 子节点为左节点
                    if (parent != null){
                        if (parent.left.value == value){// 该点为它父节点的左节点
                            parent.left = targetNode.left;
                        } else { // 该点为它父节点的右节点
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else {// 子节点为右节点
                    if (parent != null){
                        if (parent.left.value == value){
                            parent.left = targetNode.right;
                        } else {
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }
            }


        }
    }

    public void infixOrder(){
        if (root == null){
            System.out.println("树为空,不能遍历");
        } else {
            root.infixOrder();
        }
    }

    // 查找要删除的结点
    private Node search(int value){
        if (root == null){
            return null;
        } else {
            return root.search(value);
        }
    }

    // 查找父节点
    private Node searchParent(int value){
        if (root == null){
            return null;
        } else {
            return root.searchParent(value);
        }
    }

    // 编写一个特殊作用的方法:用于删除结点方法中用来找到并删除一个子树的最小结点
    // 注:还会返回该结点的值。
    /**
     * @param node 传入的结点(当做二叉排序树的根结点)
     * @return 返回的 以node为根结点的二叉排序树的最小结点的值
     */
    // 老师标准,可多多评鉴,很妙
    private int delRightTreeMin(Node node){
        Node target = node;
        // 循环的查找左子节点,就会找到最小值
        while (target.left != null){
            target = target.left;
        }
        // 这时 target就指向了最小结点
        // 删除最小结点
        deleteNode(target.value);
        return target.value;
    }

}


class Node {
    public int value;
    public Node left;
    public Node right;

    public Node(int value) {
        this.value = value;
    }

    // AVL树添加的方法:
    // 1.计算左子树的方法
    public int leftHeight(){
        if (left == null){
            return 0;
        }
        return left.height();
    }

    // 2.计算右子树的方法
    public int rightHeight(){
        if (right == null){
            return 0;
        }
        return right.height();
    }

    // 基本方法:计算以输入节点为根节点的树的高度。
    public int height(){
        return Math.max(left == null ? 0 : left.height()
                ,right == null ? 0 : right.height()) + 1;
    }

    // 左旋转方法
    public void leftRotate(){
        // 首先,需要制造一个新的节点,将root的值赋给它
        Node newNode = new Node(value);
        // 然后,需要将root节点的左子树(左节点)赋给它的左子节点。
        newNode.left = left;
        // 再将root的右子节点的左子节点赋给它的右子节点
        newNode.right = right.left;
        // 将root的右子节点值(权)赋给root节点
        value = right.value;
        // 将root的右子节点指向root自己的右子节点的右子节点。
        right = right.right;
        // 最后,将root的左子节点指向制造出来的那个新节点。
        left = newNode;
    }

    // 右旋转方法
    public void rightRotate(){
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }



    // 添加结点的方法
    // 递归的形式添加结点,注意需要满足二叉排序树的要求
    public void addNode(Node node){
        if (node == null){
            return;
        }

        // 判断传入的结点的值,和当前子树的根结点的值关系
        if (node.value < this.value){
            if (this.left == null){
                this.left = node;
            } else {
                this.left.addNode(node);
            }
        } else {
            if (this.right == null){
                this.right = node;
            } else {
                this.right.addNode(node);
            }
        }

        // 当添加完一个节点后,如果右子树的高度-左子树的高度>1,左旋转
        if (rightHeight() - leftHeight() > 1){
            // 特殊情况:如果一个点的右子树的左子树比右子树高度高
            if (left != null && right.leftHeight() > right.rightHeight()){
                // 需要先将它的右子树进行右旋转
                right.rightRotate();
                // 然后再将它左旋转
                leftRotate();// 左旋转
            } else {
                leftRotate();// 左旋转
            }
            return;// 必须要,课程中老师说的,额,我其实想不出来哪种场景会出现。
        }

        // 当添加完一个节点后,如果左子树的高度-右子树的高度>1,右旋转
        if (leftHeight() - rightHeight() > 1){
            // 特殊情况:如果一个点的左子树的右子树比左子树的左子树高度高
            if (left != null && left.rightHeight() > left.leftHeight()){
                // 需要先将它的左子进行左旋转
                left.leftRotate();
                // 然后再将它右旋转
                rightRotate();// 右旋转
            } else {
                rightRotate();// 右旋转
            }
        }

    }

    //中序遍历
    public void infixOrder(){
        if (this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.infixOrder();
        }
    }

    // 查找要删除的结点
    /**
     * @param value 希望删除的结点的值
     * @return 如果找到则返回该结点,否则返回null
     */
    public Node search(int value){
        if (this.value == value){
            return this;
        } else if (this.value > value){
            if (this.left == null){
                return null;
            }
            return this.left.search(value);
        } else {
            if (this.right == null){
                return null;
            }
            return this.right.search(value);
        }
    }


    // 查找要删除结点的父节点
    /**
     * @param value 要找到的结点的值(找它的父节点)
     * @return 返回的是要删除的结点的父节点,如果没有就返回null
     */
    public Node searchParent(int value){
        if ((this.left != null && this.left.value == value)
                ||
                (this.right != null && this.right.value == value)){
            return this;
        } else if (this.value > value){
            if (this.left == null){
                return null;
            }
            return this.left.searchParent(value);
        } else {
            if (this.right == null){
                return null;
            }
            return this.right.searchParent(value);
        }
    }


    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}

添加方法补充平衡过程详解

平衡二叉树与二叉排序树的区别是在添加和删除结点(这里删除略了和添加一样的思路)的时候平衡二叉树会自动对树进行平衡,使树保持为一个AVL树的结构,这样,查询的效率就在各种情况下得到了保证。

简单的说就是添加结点后需要判断左右子树的高度,然后有几种情况

第一种如下:(这种情况是右子树比左子树高,且右子树的右子树比左子树高。)

数据结构-二叉树-应用-BST&AVL&RBT

思路:左旋转

顾名思义,看起来就像是像左旋转一样,原理的话,其实是可以直接看出来的。

首先,需要造一个结点,这个结点的值是这个树的根结点的值,然后将这个结点的左子节点指向根结点的左子节点,这样的思路是这个结点在之后作为根结点的左节点,因为这个造出来的新节点的值是和根结点一样的,所以它的左结点指向原来根结点的左节点就不会影响结构。

然后,需要将这个新造出来的结点的右子节点指向根结点的右节点的左子节点,因为根结点的右子节点的左子节点肯定比根结点大,所以也就比这个新造出来的结点大,所以这个结点放在新造出来的结点的右子节点就不会破坏这个树的结构。

接下来很重要,需要将根结点的右子节点的值赋给根结点,这一步中,由于原来根结点是和新造出来的结点的值相同,而原来根结点的右子节点肯定比根结点大,所以此时赋值后根结点一定比这个新造出来的结点大,满足将新造出来结点放在根结点左子节点的要求,然后还需要细说一下,上一步之所以将新造结点的右节点指向根结点的右子节点的左子节点其实是因为它不会破坏结构,将它放过去上面说了是成立为新造结点的右节点的,然后同时因为它本来是根结点的右子节点的左子节点嘛,那它和它的结点也肯定比根结点的右子节点小了,那么赋值后的根结点和根结点的右子节点值一样,也就大于它了,所以整个新造结点其实就比根结点小了,这也是后面将根结点的左子节点指向新造结点成立的原因。

下一步,将根结点的右子节点指针指向右子节点的右子节点,首先这不会改变结构,然后这一步相当于将原来的根结点的右子节点的删除了,因为根结点的右子节点指针不在指向它了,它就成垃圾,会被垃圾回收。然后就是将根结点的左子节点的指针指向新造结点,这个流程如上图右边,最后形成了一个平衡后的树,整个过程其实就是在把右子树的值往左子树挪位子,之所以这样就是前面这个过程的分析,是为了保证符合二叉排序树的规则。

第二种情况:(这种情况是左子树比右子树高,且左子树的左子树比右子树高。)

数据结构-二叉树-应用-BST&AVL&RBT

思路:左旋转

与左旋转思路一样,只是对称了而已。略。

第三种/第四种情况:

右子树比左子树高,但是右子树的右子树比右子树的左子树矮。

左子树比右子树高,但是左子树的左子树比左子树的右子树矮。

用单旋转会变成这样子:

数据结构-二叉树-应用-BST&AVL&RBT

解决方案:双旋转

后面这两种情况其实就是一种特殊情况,这两种情况如果使用单旋转的话,旋转后依然不满足平衡二叉树的规则(两子树的高度差超过了1),所以需要新的解决方法。

课上老韩讲解了双旋转的方式,简单的说,就是左右旋转各用一次。如果是右子树比左子树高,但是右子树的右子树比右子树的左子树矮,那么需要将右子树先进行右旋转,这样会使右子树的右子树的高度高于右子树的左子树,这样就满足了第一种情况,再进行一次该结点的左旋转即可。另外一种情况则旋转情况刚好相反。

总结:

右子树比左子树高,但是右子树的右子树比右子树的左子树矮。–> 先右子树右旋转再左旋转

左子树比右子树高,但是左子树的左子树比左子树的右子树矮。–> 先左子树左旋转再右旋转

如下图红框部分可以完成。(截图的上面的代码)

数据结构-二叉树-应用-BST&AVL&RBT

红黑树 (RBT)

基础

定义

老韩的课程出来时间有点久了,算早先的课程,没有讲到红黑树,这里我看的黑马的红黑树。

首先是定义,我理解的是红黑树就是用二叉排序树模拟2-3树结构,通过红黑链接的概念完成,所以学习红黑树前,需要先学习2-3树的基本概念。

详细的截一张黑马的图:

数据结构-二叉树-应用-BST&AVL&RBT

数据结构-二叉树-应用-BST&AVL&RBT

如上图,看得出来,因为模拟了2-3树,那么这种二叉排序树的高度问题也能得到解决,不过不像AVL树那样“完全平衡”。

注:红黑树还可以是模拟2-3-4树,是算法导论中的,也是普遍的一种红黑树,原理都差球不多,略了。

红黑树和平衡二叉树比较

如下图总结很好:原链接https://www.jianshu.com/p/37436ed14cc6

数据结构-二叉树-应用-BST&AVL&RBT

红黑树代码详解

红黑树全代码(2-3树版本)

package redblacktree;

public class RedBlackTree<Key extends Comparable<Key>,Value> {
    // 根结点
    private Node root;
    // 记录树中元素的个数
    private int N;
    // 红色链接
    private static final boolean RED = true;
    // 黑色链接
    private static final boolean BLACK = false;

    // 结点类
    private class Node{
        // 存储键
        public Key key;
        // 存储值
        private Value value;
        // 记录左子节点
        public Node left;
        // 记录右子节点
        public Node right;
        // 由其父节点指向它的链接的颜色
        public boolean color;

        public Node(Key key, Value value, Node left, Node right, boolean color) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
            this.color = color;
        }

        // 自己写一个中序遍历,用于看红黑树的结构
        public void infixOrder(Node x){
            if (x.left != null){
                infixOrder(x.left);
            }
            System.out.println(x.key);
            if (x.right != null){
                infixOrder(x.right);
            }
        }
    }

    // 获取树中元素的个数
    public int size(){
        return N;
    }

    // 判断当前结点的父指向链接是否为红色
    private boolean isRed(Node x){
        if (x == null){
            return false;
        }
        return x.color == RED;
    }

    /**
     * 左旋转
     * @param h
     * @return
     */
    private Node rotateLeft(Node h){

        // 获取h结点的右子节点,使用x指针指向它,确定好它的地址
        Node x = h.right;
        // 将h的右指针指向x的左子节点
        h.right = x.left;
        // 将x的左指针指向h结点
        x.left = h;
        // 将x的color属性 等于 h的color属性
        x.color = h.color;
        // 将h的color属性设置为RED(红色)
        h.color = RED;

        return x;
    }

    /**
     * 右旋转
     * @param h
     * @return
     */
    private Node rotateRight(Node h){

        // 获取h结点的左子节点,使用x指针指向它,确定好它的地址
        Node x = h.left;
        // 将h的左指针指向x的右子节点
        h.left = x.right;
        // 将x的右指针指向h结点
        x.right = h;
        // 将x的color属性 等于 h的color属性
        x.color = h.color;
        // 将h的color属性设置为RED(红色)
        h.color = RED;

        return x;
    }

    /**
     * 颜色反转,相当于完成拆分4-结点
     * @param h
     */
    private void flipColors(Node h){
        // 当前结点变为红色
        h.color = RED;
        // 左子节点和右子节点变为黑色
        h.left.color = BLACK;
        h.right.color = BLACK;
    }

    /**
     * 在整个树上完成插入操作
     * @param key
     * @param val
     */
    public void put(Key key,Value val){
        root = put(root,key,val);
        // 根结点的颜色总是黑色。
        root.color = BLACK;
    }

    /**
     * 在指定树中,完成插入操作,并返回添加元素后新的树
     * @param h
     * @param key
     * @param val
     */
    private Node put(Node h,Key key,Value val){

        // 判断h,如果h为null,则已经到最后了,直接封装数据为一个结点然后返回即可。
        if (h == null){
            // 数量+1
            N++;
            return new Node(key,val,null,null,RED);
        }
        // 如果h不为空,则需要比较h结点的键和key的大小
        int cmp = key.compareTo(h.key);
        if (cmp < 0){
            // 比较结果小于h的key值,向左继续查找插入
            // 注:赋值给 h.left 是因为可能发生翻转。
            h.left = put(h.left,key,val);
        } else if (cmp > 0){
            // 比较结果大于h的key值,向右继续查找插入
            h.right = put(h.right,key,val);
        } else {
            // 等于h的key值,需要替换原来的key值
            h.value = val;
        }

        // 进行左旋:当当前结点h的左子结点为黑色,右子节点为红色,需要左旋
        if (isRed(h.right) && !isRed(h.left)){
            h = rotateLeft(h);
        }
        // 进行右旋:当当前结点h的左子节和左子节点的左子节点都为红色,需要右旋
        if (isRed(h.left) && isRed(h.left.left)){
            h = rotateRight(h);
        }
        // 颜色反转:当前结点的左子节点和右子节点都为红色时,需要颜色反转
        if (isRed(h.left) && isRed(h.right)){
            flipColors(h);
        }

        return h;
    }

    // 根据key,从树中找出对应的值
    public Value get(Key key){
        return get(root,key);
    }

    // 从指定的树x中,查找key对应的值
    private Value get(Node x,Key key){
        if (x == null){
            return null;
        }

        // 比较x结点的键和key的大小
        int cmp = key.compareTo(x.key);
        if (cmp < 0){
            return get(x.left,key);
        } else if (cmp > 0){
            return get(x.right,key);
        } else {
            return x.value;
        }
    }

    // 用查看红黑树的结构
    public void infixOrder(){
        root.infixOrder(root);
    }
}

测试代码:

package redblacktree;

public class RedBlackTreeTest {
    public static void main(String[] args) {
        // 创建红黑树
        RedBlackTree<Integer,String> tree = new RedBlackTree<>();

        // 向树中插入元素
        tree.put(1,"张三");
        tree.put(2,"李四");
        tree.put(3,"王五");
        tree.put(4,"看看");
        // 从树中获取元素
//        String r1 = tree.get(1);
//        System.out.println(r1);
//
//        String r2 = tree.get(2);
//        System.out.println(r2);
//
//        String r3 = tree.get(3);
//        System.out.println(r3);
//
//        String r4 = tree.get(4);
//        System.out.println(r4);

        tree.infixOrder();
    }
}

红黑树详解-红黑树的结点插入

红黑树的难点主要在于插入结点,首先,插入结点需要分为四种情况:

  1. 向单个2-结点插入新键
  2. 向底部的2-结点插入新键
  3. 向一颗双键树中插入新键
  4. 向树底部的3-结点插入新键
向单个2-结点插入新键

两种情况:如下图

数据结构-二叉树-应用-BST&AVL&RBT

数据结构-二叉树-应用-BST&AVL&RBT

补充:关于旋转

黑马的旋转方法和尚硅谷老韩的有些不一样,老韩的方法上面avl树讲了,是用一个新节点来辅助连接,黑马视频的方法更加简洁一下(不过不确定影响理解,因为我先会了老韩的方法后,理解它这个感觉都差不多),没有用新节点,不过需要返回旋转后的子树的根结点重新接上原来父节点的位置才醒。

截图一个,方便理解:(右旋类比)

数据结构-二叉树-应用-BST&AVL&RBT

向底部的2-结点插入新键

向底部的2-结点插入和上面一样的,如果小于直接插入,如果大于插入后左旋即可。

数据结构-二叉树-应用-BST&AVL&RBT

向一颗双键树(3-结点)中插入新键

向一颗双键树中插入新键有三种情况

第一种情况

数据结构-二叉树-应用-BST&AVL&RBT

如上图为第一种情况,新键插入后b指向左右两个子节点的两个链接(指针)都为红色,这不符合红黑树规则,需要进行颜色反转。颜色反转补充在下方。

注:这个过程中第二个树形态其实就是2-3树中插入结点过程中的临时4-结点。

颜色反转:

数据结构-二叉树-应用-BST&AVL&RBT

如上图,颜色反转后变为该子树的根结点的父链接为红色。注:所以结点的的父链接的颜色是结点的属性。

然后,这里还需要补充一点,那就是根结点的颜色总是黑色的,原因是根结点没有父节点。同时,代码实现的时候还需要注意,每次插入操作后都需要将根结点的颜色设置为黑色(因为插入旋转后,root根结点指针需要指向的结点是有可能变化的)。

第二种情况

数据结构-二叉树-应用-BST&AVL&RBT

数据结构-二叉树-应用-BST&AVL&RBT

第三种情况

数据结构-二叉树-应用-BST&AVL&RBT

数据结构-二叉树-应用-BST&AVL&RBT

向树底部的3-结点插入新键

数据结构-二叉树-应用-BST&AVL&RBT

数据结构-二叉树-应用-BST&AVL&RBT

数据结构-二叉树-应用-BST&AVL&RBT

上一篇:BST & AVL & 红黑树


下一篇:验证二叉搜索树(BST) Leetcode98题(巨简洁)