树的概念以及AVL树、红黑树

本文详细介绍了树的概念,包括定义、名词解释及二叉树的相关知识。接着,重点讲解了两种特殊平衡二叉树——AVL树和红黑树,阐述了它们的定义、特性、平衡因子、旋转类型及其应用场景。AVL树通过四种类型的旋转(LL、LR、RL、RR)来保持平衡,而红黑树则遵循特定的规则保持相对平衡,确保操作时间复杂度为O(log2n)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义

1.有且仅有一个根结点;
2.从一个结点到另一个结点只有一条通路;

名词解释

1.孩子结点(子结点)/双亲结点(父结点)/兄弟结点(具有同一双亲结点);
2.结点的度:子结点的数量;
3.叶子结点:度为0的结点;

二叉树

每一个结点的度不超过2;
1.二叉树的深度:高度,树的最大层次;
2.二叉树的第k层最多具有2^(k-1)结点;
3.拥有k层的树,最多拥有2^k - 1个结点;
4.满二叉树:拥有k层的树,最多2^k - 1个结点;
5.完全二叉树:1-n个结点,与满二叉树的1-n的结点位置完全相同;
6.叶子结点的数量 = 度为2的结点数量 + 1;

普通树转二叉树

左孩子,右兄弟。
1.连线:将所有的兄弟结点的连线。
2.抹线:将除了与第一个字结点的线以外,其余抹除;
3.旋转

二叉树的遍历

1.层次遍历:从左往右,从上往下一一遍历;
2.前中后序遍历:相对于根结点;
前序遍历(先序):根 - 左 - 右
中序遍历:左 - 根 - 右
后序遍历:左 - 右 - 根

特殊的树

1.平衡二叉树:每一个结点的左右子树的深度差不超过1;
2.自平衡二叉树:AVL树/红黑树(R-B Tree)/替罪羊树;
3.四种类型:
LL型:在左子树的左孩子上插入元素;(右旋)
RR型:在右子树的右孩子上插入元素;(左旋)
LR型:在左子树的右孩子上插入元素;(先左旋再右旋)
RL型:在右子树的左孩子上插入元素;(先右旋再左旋)

AVL树

定义

平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
平衡因子 : 树中某结点其左子树的高度和右子树的高度之差
AVL树中的任意一个结点, 其平衡因子的绝对值小于2

在这里插入图片描述

概述

对于一般的二叉搜索树,其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度O(log2n)同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。

例如:我们按顺序将一组数据1,2,3,4,5,6分别插入到一颗空二叉查找树和AVL树中,插入的结果如下图:
在这里插入图片描述
由上图可知,同样的结点,由于插入方式不同导致树的高度也有所不同。特别是在带插入结点个数很多且正序的情况下,会导致二叉树的高度是O(N),而AVL树就不会出现这种情况,树的高度始终是O(lgN)。高度越小,对树的一些基本操作的时间复杂度就会越小。

AVL树不仅是一颗二叉查找树,它还有其他的性质。如果我们按照一般的二叉查找树的插入方式可能会破坏AVL树的平衡性。同理,在删除的时候也有可能会破坏树的平衡性,所以我们要做一些特殊的旋转处理来重新恢复平衡。

四种类型的旋转

如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。这种失去平衡的可以概括为4种姿态:LL(左左),LR(左右),RR(右右)和RL(右左)。下面给出它们的示意图:
在这里插入图片描述
上图中的4棵树都是"失去平衡的AVL树",从左往右的情况依次是:LL、LR、RL、RR。除了上面的情况之外,还有其它的失去平衡的AVL树,如下图:
在这里插入图片描述

上面的两张图都是为了便于理解,而列举的关于"失去平衡的AVL树"的例子。总的来说,AVL树失去平衡时的情况一定是LL、LR、RL、RR这4种之一,它们都由各自的定义:
LL:LeftLeft,也称为"左左"。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。
例如,在上面LL情况中,由于"根节点(8)的左子树(4)的左子树(2)还有非空子节点",而"根节点(8)的右子树(12)没有子节点";导致"根节点(8)的左子树(4)高度"比"根节点(8)的右子树(12)“高2。
LR:LeftRight,也称为"左右”。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。
例如,在上面LR情况中,由于"根节点(8)的左子树(4)的左子树(6)还有非空子节点",而"根节点(8)的右子树(12)没有子节点";导致"根节点(8)的左子树(4)高度"比"根节点(8)的右子树(12)“高2。
RL:RightLeft,称为"右左”。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
例如,在上面RL情况中,由于"根节点(8)的右子树(12)的左子树(10)还有非空子节点",而"根节点(8)的左子树(4)没有子节点";导致"根节点(8)的右子树(12)高度"比"根节点(8)的左子树(4)“高2。
RR:RightRight,称为"右右”。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
例如,在上面RR情况中,由于"根节点(8)的右子树(12)的右子树(14)还有非空子节点",而"根节点(8)的左子树(4)没有子节点";导致"根节点(8)的右子树(12)高度"比"根节点(8)的左子树(4)"高2。
如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。AVL失去平衡之后,可以通过旋转使其恢复平衡,下面分别介绍"LL(左左),LR(左右),RR(右右)和RL(右左)"这4种情况对应的旋转方法。

1. LL的旋转
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡。如下图:
在这里插入图片描述

图中左边是旋转之前的树,右边是旋转之后的树。从中可以发现,旋转之后的树又变成了AVL树,而且该旋转只需要一次即可完成。

对于LL旋转,你可以这样理解为:LL旋转是围绕"失去平衡的AVL根节点"进行的,也就是节点k2;而且由于是LL情况,即左左情况,就用手抓着"左孩子,即k1"使劲摇。将k1变成根节点,k2变成k1的右子树,“k1的右子树"变成"k2的左子树”。

2. RR的旋转
理解了LL之后,RR就相当容易理解了。RR是与LL对称的情况!RR恢复平衡的旋转方法如下:
在这里插入图片描述

图中左边是旋转之前的树,右边是旋转之后的树。RR旋转也只需要一次即可完成。

3. LR的旋转
LR失去平衡的情况,需要经过两次旋转才能让AVL树恢复平衡。如下图:

在这里插入图片描述

第一次旋转是围绕"k1"进行的"RR旋转",第二次是围绕"k3"进行的"LL旋转"。

4. RL的旋转
RL是与LR的对称情况!RL恢复平衡的旋转方法如下:

在这里插入图片描述

第一次旋转是围绕"k3"进行的"LL旋转",第二次是围绕"k1"进行的"RR旋转"。

代码实现


public class AVLTree {

    public AVLTreeNode root; // 根结点

    /**
     * - 插入操作的入口 
     * - @param insertValue
     */
    public void insert(long insertValue) {
        root = insert(root, insertValue);
    }

    /**
     * - 插入的地递归实现
     * <p>
     * - @param subTree
     * <p>
     * - @param insertValue
     * <p>
     * - @return
     */
    private AVLTreeNode insert(AVLTreeNode subTree, long insertValue) {
        if (subTree == null) {
            return new AVLTreeNode(insertValue, null, null);
        }

        if (insertValue < subTree.value) { // 插入左子树

            subTree.left = insert(subTree.left, insertValue);
            if (unbalanceTest(subTree)) { // 插入后造成失衡
                if (insertValue < subTree.left.value) { // LL型失衡
                    subTree = leftLeftRotation(subTree);
                } else { // LR型失衡
                    subTree = leftRightRotation(subTree);
                }
            }

        } else if (insertValue > subTree.value) { // 插入右子树

            subTree.right = insert(subTree.right, insertValue);
            if (unbalanceTest(subTree)) { // 插入后造成失衡
                if (insertValue < subTree.right.value) { // RL型失衡
                    subTree = rightLeftRotation(subTree);
                } else { // RR型失衡
                    subTree = rightRightRotation(subTree);
                }
            }

        } else {
            throw new RuntimeException("duplicate value: " + insertValue);
        }

        return subTree;
    }

    /**
     * - RL型旋转
     * <p>
     * - @param k1 子树根节点
     * <p>
     * - @return
     */
    private AVLTreeNode rightLeftRotation(AVLTreeNode k1) {
        k1.right = leftLeftRotation(k1.right);

        return rightRightRotation(k1);
    }

    /**
     * - RR型旋转
     * <p>
     * - @param k1 k1 子树根节点
     * <p>
     * - @return
     */
    private AVLTreeNode rightRightRotation(AVLTreeNode k1) {
        AVLTreeNode k2;

        k2 = k1.right;
        k1.right = k2.left;
        k2.left = k1;

        return k2;
    }

    /**
     * - LR型旋转
     * <p>
     * - @param k3
     * <p>
     * - @return
     */
    private AVLTreeNode leftRightRotation(AVLTreeNode k3) {
        k3.left = rightRightRotation(k3.left);

        return leftLeftRotation(k3);
    }

    /**
     * - LL型旋转
     * <p>
     * - @param k2
     * <p>
     * - @return
     */
    private AVLTreeNode leftLeftRotation(AVLTreeNode k2) {
        AVLTreeNode k1;

        k1 = k2.left;
        k2.left = k1.right;
        k1.right = k2;

        return k1;
    }

    /**
     * - 获取树的深度
     * <p>
     * - @param treeRoot 根节点
     * <p>
     * - @param initDeep 初始深度
     * <p>
     * - @return
     */
    private static int getDepth(AVLTreeNode treeRoot, int initDeep) {
        if (treeRoot == null) {
            return initDeep;
        }
        int leftDeep = initDeep;
        int rightDeep = initDeep;
        if (treeRoot.left != null) {
            leftDeep = getDepth(treeRoot.left, initDeep++);
        }
        if (treeRoot.right != null) {
            rightDeep = getDepth(treeRoot.right, initDeep++);
        }

        return Math.max(leftDeep, rightDeep);
    }

    /**
     * - 判断是否失衡 - @param treeRoot - @return
     */
    private boolean unbalanceTest(AVLTreeNode treeRoot) {
        int leftHeight = getDepth(treeRoot.left, 1);
        int righHeight = getDepth(treeRoot.right, 1);
        int diff = Math.abs(leftHeight - righHeight);
        return diff > 1;
    }

    /**
     * - 删除操作的入口 - @param value
     */
    public void remove(long value) {
        root = remove(root, value);
    }

    /**
     * - 删除操作的递归实现
     * <p>
     * - @param tree
     * <p>
     * - @param value
     * <p>
     * - @return
     */
    private AVLTreeNode remove(AVLTreeNode tree, long value) {
        if (tree == null) {
            return tree;
        }

        if (value < tree.value) { // 要删除的节点在左子树

            tree.left = remove(tree.left, value);

        } else if (value > tree.value) { // 要删除的节点在右子树

            tree.right = remove(tree.right, value);

        } else if (tree.value == value) { // 要删除的节点就是本身

            if (tree.left != null && tree.right != null) { // 左右子树都存在
                if (getDepth(tree.left, 1) > getDepth(tree.right, 1)) {
                    /*
                     * - 如果tree的左子树比右子树高:
                     * 
                     * - 1. 找出tree的左子树中的最大节点 
                     * - 2. 将该最大节点的值赋值给tree。 
                     * - 3. 删除该最大节点。 
                     * - 这类似于用"tree的左子树中最大节点"做"tree"的替身 
                     * - 采用这种方式的好处是:删除"tree的左子树中最大节点"之后,AVL树仍然是平衡的
                     */
                    AVLTreeNode max = getMaxNode(tree.left);
                    tree.value = max.value;
                    tree.left = remove(tree.left, max.value);
                } else {
                    /*
                     * - 如果tree的左子树不高于右子树: 
                     * - 1. 找出tree的右子树中的最小节点 
                     * - 2. 将该最小节点的值赋值给tree。 
                     * - 3. 删除该最小节点。
                     * - 这类似于用"tree的右子树中最小节点"做"tree"的替身 
                     * - 采用这种方式的好处是:删除"tree的左子树中最大节点"之后,AVL树仍然是平衡的
                     */
                    AVLTreeNode min = getMinNode(tree.right);
                    tree.value = min.value;
                    tree.right = remove(tree.right, min.value);

                }

            } else {

                tree = tree.left == null ? tree.right : tree.left;

            }
        } else {
            System.out.println("no node matched value: " + value);
        }

        return tree;
    }

    /**
     * - 获取值最大的节点
     * <p>
     * - @param node
     * <p>
     * - @return
     */
    private AVLTreeNode getMaxNode(AVLTreeNode node) {
        if (node == null) {
            return null;
        }

        if (node.right != null) {
            return getMaxNode(node.right);
        } else {
            return node;
        }
    }

    /**
     * - 获取值最小的节点
     * <p>
     * - @param node
     * <p>
     * - @return
     */
    private AVLTreeNode getMinNode(AVLTreeNode node) {
        if (node == null) {
            return null;
        }

        if (node.left != null) {
            return getMinNode(node.left);
        } else {
            return node;
        }
    }

}

// AVL树的节点
class AVLTreeNode {
    long value; // 节点存储的数值
    AVLTreeNode left; // 左孩子
    AVLTreeNode right; // 右孩子

    public AVLTreeNode(long value, AVLTreeNode left, AVLTreeNode right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    public long getValue() {
        return this.value;
    }

    public void setValue(long value) {
        this.value = value;
    }

    public AVLTreeNode getLeft() {
        return this.left;
    }

    public void setLeft(AVLTreeNode left) {
        this.left = left;
    }

    public AVLTreeNode getRight() {
        return this.right;
    }

    public void setRight(AVLTreeNode right) {
        this.right = right;
    }
}


    /**
     * 
     * - 前序遍历
     * 
     * - @param currentRoot
     */
    public static void preorder(AVLTreeNode currentRoot) {
        if (currentRoot != null) {
            System.out.print(currentRoot.value + "\t");
            preorder(currentRoot.left);
            preorder(currentRoot.right);
        }
    }

    public static void main(String[] args) {
        AVLTree tree = new AVLTree();
        int arr[] = { 3, 2, 1, 4, 5, 6, 7, 16, 15, 14, 13, 12, 11, 10, 8, 9 };
        for (int a : arr) {
            tree.insert(a);

        }
        preorder(tree.root);

    }

打印结果如下:

3 2 1 4 5 6 7 16 15 14 13 12 11 10 8 9

红黑树

特性规则

(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
(6)新加入到红黑树的节点为红色节点;

注意:
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

红黑树的示意图:
在这里插入图片描述

首先解读一下规则

第一. 从根节点到叶子节点的最长路径不大于最短路径的2倍
怎么样的路径算最短路径
从规则5中,我们知道从根节点到每个叶子节点的黑色节点数量是一样的,那么纯由黑色节点组成的路径就是最短路径

什么样的路径算是最长路径
根据规则4和规则3,若有红色节点,则必然有一个连接的黑色节点,当红色节点和黑色节点数量相同时,就是最长路径,也就是黑色节点(或红色节点)* 2

第二. 为什么说新加入到红黑树中的节点为红色节点
从规则4中知道,当前红黑树中从根节点到每个叶子节点的黑色节点数量是一样的,此时假如新的黑色节点的话,必然破坏规则,但加入红色节点却不一定,除非其父节点就是红色节点,因此加入红色节点,破坏规则的可能性小一些

什么情况下,红黑树的结构会被破坏呢?破坏后又怎么维持平衡,维持平衡主要通过两种方式【变色】和【旋转】,【旋转】又分【左旋】和【右旋】,两种方式可相互结合。

红黑树的基本操作

添加

步骤:
将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:

第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。

好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!

第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。

第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"特性(4)",是有可能违背的!
那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。

例如:当我们插入值为66的节点时,红黑树变成了这样

在这里插入图片描述
很明显,这个时候结构依然遵循着上述6大规则,无需启动自动平衡机制调整节点平衡状态;

如果再向里面插入值为51的节点呢,这个时候红黑树变成了这样

在这里插入图片描述
很明显现在的结构不遵循规则 4 了,这个时候就需要启动自动平衡机制调整节点平衡状态

变色
我们可以通过变色的方式,使结构满足红黑树的规则

首先解决结构不遵循规则 4 这一点(红色节点相连,节点49-51),需将节点49改为黑色;
此时我们发现又违反了规则5(56-49-51-XX路径中黑色节点超过了其他路径),那么我们将节点45改为红色节点;
这时发现,又违反了规则4(红色节点相连,节点56-45-43),那么我们将节点56和节点43改为黑色节点;
但是我们发现此时又违反了规则5(60-56-XX路径的黑色节点比60-68-XX的黑色节点多),因此我们需要调整节点68为黑色;

在这里插入图片描述

最终调整完成后的树为:

在这里插入图片描述
但并不是什么时候都那么幸运,可以直接通过变色就达成目的,大多数时候还需要通过旋转来解决。

如在下面这棵树的基础上,加入节点65.

在这里插入图片描述

插入节点65后进行以下步骤

在这里插入图片描述

这个时候,你会发现对于节点64无论是红色节点还是黑色节点,都会违反规则5,路径中的黑色节点始终无法达成一致,这个时候仅通过【变色】已经无法达成目的。我们需要通过旋转操作,当然【旋转】操作一般还需要搭配【变色】操作。

这里重新讲一下旋转:
旋转包括【左旋】和【右旋】,

左旋:
逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点

左旋操作步骤如下:
首先断开节点PL与右子节点G的关系,同时将其右子节点的引用指向节点C2;然后断开节点G与左子节点C2的关系,同时将G的左子节点的应用指向节点PL

在这里插入图片描述

右旋:
顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点

右旋操作步骤如下:
首先断开节点G与左子节点PL的关系,同时将其左子节点的引用指向节点C2;然后断开节点PL与右子节点C2的关系,同时将PL的右子节点的应用指向节点G

在这里插入图片描述

无法通过变色而进行旋转的场景分为以下四种:

  1. 左左节点旋转
    这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点65
    规则如下:以祖父节点【右旋】,搭配【变色】

在这里插入图片描述

按照规则,步骤如下:

在这里插入图片描述

2.左右节点旋转
这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图1中,我们要插入节点67

规则如下:先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】

按照规则,步骤如下:

在这里插入图片描述

3.右左节点旋转
这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图2)这种情况,我们要插入节点68

规则如下:先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】

在这里插入图片描述

按照规则,步骤如下:

在这里插入图片描述

4.右右节点旋转
这种情况下,父节点和插入的节点都是右节点,在旋转原始图2中,我们要插入节点70

规则如下:以祖父节点【左旋】,搭配【变色】

按照规则,步骤如下:

在这里插入图片描述
红黑树插入总结

在这里插入图片描述

删除

将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:

第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。

第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。

接下来看操作
1. 子节点至少有一个为null
当待删除的节点的子节点至少有一个为null节点时,删除了该节点后,将其有值的节点取代当前节点即可,若都为null,则将当前节点设置为null,当然如果违反规则了,则按需调整,如【变色】以及【旋转】。

在这里插入图片描述

2.子节点都是非null节点
这种情况下,

第一步:找到该节点的前驱或者后继

前驱:左子树中值最大的节点(可得出其最多只有一个非null子节点,可能都为null)

后继:右子树中值最小的节点(可得出其最多只有一个非null子节点,可能都为null)

前驱和后继都是值最接近该节点值的节点,类似于该节点.prev = 前驱,该节点.next = 后继。

第二步:将前驱或者后继的值复制到该节点中,然后删掉前驱或者后继

如果删除的是左节点,则将前驱的值复制到该节点中,然后删除前驱;如果删除的是右节点,则将后继的值复制到该节点中,然后删除后继;

这相当于是一种“取巧”的方法,我们删除节点的目的是使该节点的值在红黑树上不存在,因此专注于该目的,我们并不关注删除节点时是否真是我们想删除的那个节点,同时我们也不需考虑树结构的变化,因为树的结构本身就会因为自动平衡机制而经常进行调整。

前面我们已经说了,我们要删除的实际上是前驱或者后继,因此我们就以前驱为主线来讲解,后继的学习可参考前驱,包括几种情况
(1) 前驱为黑色节点,并且有一个非null子节点

在这里插入图片描述

分析:

因为要删除的是左节点64,找到该节点的前驱63;

然后用前驱的值63替换待删除节点的值64,此时两个节点(待删除节点和前驱)的值都为63;

删除前驱63,此时成为上图过程中间环节,但我们发现其不符合红黑树规则4,因此需要进行自动平衡调整;

这里直接通过【变色】即可完成。

(2)前驱为黑色节点,同时子节点都为null

在这里插入图片描述

分析:

因为要删除的是左节点64,找到该节点的前驱63;

然后用前驱的值63替换待删除节点的值64,此时两个节点(待删除节点和前驱)的值都为63;

删除前驱63,此时成为上图过程中间环节,但我们发现其不符合红黑树规则5,因此需要进行自动平衡调整;

这里直接通过【变色】即可完成。

(3) 前驱为红色节点,同时子节点都为null

在这里插入图片描述

分析:

因为要删除的是左节点64,找到该节点的前驱63;

然后用前驱的值63替换待删除节点的值64,此时两个节点(待删除节点和前驱)的值都为63;

删除前驱63,树的结构并没有打破规则。

红黑树删除总结

红黑树删除的情况比较多,但也就存在以下情况:

删除的是根节点,则直接将根节点置为null;

待删除节点的左右子节点都为null,删除时将该节点置为null;

待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可;

待删除节点的左右子节点都不为null,则找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继;

标题“51单片机通过MPU6050-DMP获取姿态角例程”解析 “51单片机通过MPU6050-DMP获取姿态角例程”是一个基于51系列单片机(一种常见的8位微控制器)的程序示例,用于读取MPU6050传感器的数据,并通过其内置的数字运动处理器(DMP)计算设备的姿态角(如倾斜角度、旋转角度等)。MPU6050是一款集成三轴加速度计和三轴陀螺仪的六自由度传感器,广泛应用于运动控制和姿态检测领域。该例程利用MPU6050的DMP功能,由DMP处理复杂的运动学算法,例如姿态融合,将加速度计和陀螺仪的数据进行整合,从而提供稳定且实时的姿态估计,减轻主控MCU的计算负担。最终,姿态角数据通过LCD1602显示屏以字符形式可视化展示,为用户提供直观的反馈。 从标签“51单片机 6050”可知,该项目主要涉及51单片机和MPU6050传感器这两个关键硬件组件。51单片机基于8051内核,因编程简单、成本低而被广泛应用;MPU6050作为惯性测量单元(IMU),可测量设备的线性和角速度。文件名“51-DMP-NET”可能表示这是一个与51单片机及DMP相关的网络资源或代码库,其中可能包含C语言等适合51单片机的编程语言的源代码、配置文件、用户手册、示例程序,以及可能的调试工具或IDE项目文件。 实现该项目需以下步骤:首先是硬件连接,将51单片机与MPU6050通过I2C接口正确连接,同时将LCD1602连接到51单片机的串行数据线和控制线上;接着是初始化设置,配置51单片机的I/O端口,初始化I2C通信协议,设置MPU6050的工作模式和数据输出速率;然后是DMP配置,启用MPU6050的DMP功能,加载预编译的DMP固件,并设置DMP输出数据的中断;之后是数据读取,通过中断服务程序从DMP接收姿态角数据,数据通常以四元数或欧拉角形式呈现;再接着是数据显示,将姿态角数据转换为可读的度数格
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值