【C++入门到精通】C++入门 —— AVL 树(自平衡二叉搜索树)

news2024/11/13 16:17:48

在这里插入图片描述

阅读导航

  • 前言
  • 一、AVL树的概念
  • 二、AVL树节点的定义
  • 三、AVL树的插入
  • 四、AVL树的旋转(重点)
    • 1. 右单旋(新节点插入较高左子树的左侧)
    • 2. 左单旋(新节点插入较高右子树的右侧)
    • 3. 先左单旋再右单旋(新节点插入较高左子树的右侧)
    • 4. 先右单旋再左单旋(新节点插入较高右子树的左侧)
  • 五、AVL树的删除(了解)
  • 六、AVL树的性能
  • 附:详细的AVL模拟代码
  • 温馨提示

前言

前面我们讲了C语言的基础知识,也了解了一些初阶数据结构,并且讲了有关C++的命名空间的一些知识点以及关于C++的缺省参数、函数重载,引用 和 内联函数也认识了什么是类和对象以及怎么去new一个 ‘对象’ ,也了解了C++中的模版,以及学习了几个STL的结构也相信大家都掌握的不错,接下来博主将会带领大家继续学习有关C++比较重要的知识点—— AVL 树(自平衡二叉搜索树) 。下面话不多说坐稳扶好咱们要开车了😍

一、AVL树的概念

AVL树是一种自平衡的二叉搜索树,它在插入或删除节点时通过旋转操作来保持树的平衡。AVL树的名称来自发明者 Adelson-Velsky 和 Landis 的姓氏的首字母。

在AVL树中,每个节点都有一个平衡因子(balance factor),表示其左子树高度和右子树高度之间的差值。平衡因子可以是-1、0或1,如果平衡因子的绝对值超过1,则该节点被认为是不平衡的
在这里插入图片描述

⭕AVL树维护以下性质:

  1. 树的每个节点的平衡因子必须在-1、0和1之间。
  2. 所有左子树的高度与右子树的高度之差的绝对值不超过1。
  3. 如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)

当向AVL树中插入或删除节点时,可能会破坏树的平衡。为了恢复平衡,AVL树使用四种旋转操作:左旋、右旋、左右旋和右左旋。通过这些旋转操作,AVL树可以在插入或删除操作后保持平衡,从而提供较为稳定和高效的搜索、插入和删除操作。

由于AVL树的自平衡特性,它适用于需要频繁插入和删除操作的场景,尤其是对于需要快速搜索和有序遍历的数据集合。如果一棵二叉搜索树是高度平衡的,它就是AVL树。

二、AVL树节点的定义

AVL树的节点定义包括以下几个属性:

  1. :每个节点存储的值,可以是任意类型,通常是一个关键字或数据。

  2. 左子节点指针:指向当前节点的左子节点的指针。左子节点的值应该小于或等于当前节点的值。

  3. 右子节点指针:指向当前节点的右子节点的指针。右子节点的值应该大于当前节点的值。

  4. 父节点指针:指向当前节点的父节点的指针。根节点的父节点指针为空。

  5. 平衡因子:表示当前节点的左子树高度和右子树高度之差。平衡因子可以为-1、0或1。

下面是一个示例代码来定义一个AVL树的节点结构:

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf; // balance factor

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

三、AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

下面是AVL树的插入的基本算法:

  • 如果AVL树为空,将新节点作为根节点插入,并更新其高度和平衡因子。
  • 如果插入的值小于当前节点的值,则将其插入到当前节点的左子树中。如果左子节点为空,直接插入;否则,递归执行插入操作。
  • 如果插入的值大于当前节点的值,则将其插入到当前节点的右子树中。如果右子节点为空,直接插入;否则,递归执行插入操作。
  • 在递归返回的过程中,更新每个节点的高度和平衡因子,然后检查平衡因子是否超过了范围。
    如果发现平衡因子超出范围,进行旋转操作来修复平衡。

下面是一个基于C++实现的AVL树插入算法的伪代码:

bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        // 如果根节点为空,创建新节点作为根节点,并返回插入成功
        _root = new Node(kv);
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < kv.first)
        {
            // 当前节点的键值小于插入节点的键值,继续向右子树查找
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first)
        {
            // 当前节点的键值大于插入节点的键值,继续向左子树查找
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            // 当前节点的键值等于插入节点的键值,插入失败(不允许键值重复)
            return false;
        }
    }

    // 创建新节点并插入到树中
    cur = new Node(kv);
    if (parent->_kv.first > kv.first)
    {
        parent->_left = cur;
    }
    else
    {
        parent->_right = cur;
    }
    cur->_parent = parent;

    // 更新平衡因子
    while (parent)
    {
        if (cur == parent->_right)
        {
            parent->_bf++; // 右子树增加一个节点,平衡因子加1
        }
        else
        {
            parent->_bf--; // 左子树增加一个节点,平衡因子减1
        }

        if (parent->_bf == 1 || parent->_bf == -1)
        {
            // 平衡因子为1或-1,说明子树高度增加,继续更新父节点的平衡因子
            parent = parent->_parent;
            cur = cur->_parent;
        }
        else if (parent->_bf == 0)
        {
            // 平衡因子为0,说明子树高度没有变化,不需要进行旋转操作,结束循环
            break;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 平衡因子为2或-2,需要进行旋转处理来恢复平衡

            if (parent->_bf == 2 && cur->_bf == 1)
            {
                RotateL(parent); // 左旋
            }
            else if (parent->_bf == -2 && cur->_bf == -1)
            {
                RotateR(parent); // 右旋
            }
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                RotateLR(parent); // 先左旋再右旋
            }
            else if (parent->_bf == 2 && cur->_bf == -1)
            {
                RotateRL(parent); // 先右旋再左旋
            }
            else
            {
                assert(false); // 不应该出现的情况
            }

            break;
        }
        else
        {
            assert(false); // 不应该出现的情况
        }
    }

    return true;
}

⭕代码解释:

  1. 首先检查根节点是否为空,在空树中直接创建新节点作为根节点。
  2. 如果不是空树,则通过循环在适当的位置找到插入节点应该放置的父节点。
  3. 在插入节点时,根据父节点的键值与插入节点的键值的比较结果,确定插入节点是父节点的左子节点还是右子节点。
  4. 更新插入节点的父指针,并更新父节点及其祖先节点的平衡因子。
  5. 根据平衡因子的变化情况,决定是否需要进行旋转操作来调整树的平衡。
  6. 旋转操作分为左旋、右旋、先左旋再右旋和先右旋再左旋四种情况,具体根据平衡因子的值来确定。

四、AVL树的旋转(重点)

1. 右单旋(新节点插入较高左子树的左侧)

  • 右单旋是AVL树的一种旋转操作,用于解决插入节点位于较高左子树的左侧的情况。

下面是右单旋的具体实现代码:

void RotateR(Node* parent)
{
    Node* subL = parent->_left;    // 获取父节点的左子节点
    Node* subLR = subL->_right;    // 获取左子节点的右子节点
    Node* ppnode = parent->_parent;    // 获取父节点的父节点

    parent->_left = subLR;    // 将左子节点的右子节点作为父节点的左子节点
    if (subLR)
        subLR->_parent = parent;    // 更新左子节点的右子节点的父指针

    subL->_right = parent;    // 将父节点作为左子节点的右子节点
    parent->_parent = subL;    // 更新父节点的父指针

    if (parent == _root)
    {
        _root = subL;    // 如果原父节点是根节点,更新根节点
        _root->_parent = nullptr;
    }
    else
    {
        if (ppnode->_left == parent)
        {
            ppnode->_left = subL;    // 如果原父节点是其父节点的左子节点,更新父节点的左子节点
        }
        else
        {
            ppnode->_right = subL;    // 如果原父节点是其父节点的右子节点,更新父节点的右子节点
        }
        subL->_parent = ppnode;    // 更新左子节点的父指针
    }
    subL->_bf = parent->_bf = 0;    // 更新平衡因子
}

⭕具体而言,右单旋的操作步骤如下:

  1. 获取当前节点的左子节点,并将其保存在变量 subL 中。
  2. 获取左子节点的右子节点,并将其保存在变量 subLR 中。
  3. 获取当前节点的父节点,并将其保存在变量 ppnode 中。
  4. 将左子节点的右子节点作为当前节点的左子节点。
  5. 如果左子节点的右子节点存在,则将其父指针更新为当前节点。
  6. 将当前节点作为左子节点的右子节点。
  7. 更新当前节点的父指针为左子节点。
  8. 如果当前节点是根节点,则更新根节点为左子节点,并将根节点的父指针置为空。
  9. 如果当前节点不是根节点,则根据其在父节点的位置,更新父节点的相应子节点为左子节点,并将左子节点的父指针更新为父节点。
  10. 将左子节点和当前节点的平衡因子都设置为0,表示树已经平衡。

右单旋操作通过对节点间的指针进行调整,重新平衡了AVL树的结构。这样做的目的是保持AVL树的平衡性,从而提高树的查询和插入等操作的效率。

2. 左单旋(新节点插入较高右子树的右侧)

  • 左单旋是AVL树的一种旋转操作,用于解决插入节点位于较高右子树的右侧的情况。

下面是左单旋的具体实现代码:

void RotateL(Node* parent)
{
    // 保存父节点的右子节点
    Node* subR = parent->_right;
    // 保存右子节点的左子节点
    Node* subRL = subR->_left;
    // 保存父节点的父节点
    Node* ppnode = parent->_parent;

    // 将右子节点的左子节点作为父节点的右子节点
    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;

    // 将父节点作为右子节点的左子节点
    subR->_left = parent;
    parent->_parent = subR;

    // 判断原父节点是否为根节点
    if (ppnode == nullptr) 
    {
        // 更新根节点为右子节点
        _root = subR;
        // 将新根节点的父指针置为空
        _root->_parent = nullptr;
    }
    else
    {
        // 判断原父节点是其父节点的左子节点还是右子节点
        if (ppnode->_left == parent)
        {
            // 更新父节点的左子节点为右子节点
            ppnode->_left = subR;
        }
        else
        {
            // 更新父节点的右子节点为右子节点
            ppnode->_right = subR;
        }
        // 更新右子节点的父指针为父节点的父节点
        subR->_parent = ppnode;
    }

    // 将父节点和右子节点的平衡因子都设置为0,表示树已经平衡
    parent->_bf = subR->_bf = 0;
}

⭕具体而言,左单旋的操作步骤如下:

  1. 保存了需要进行旋转操作的父节点、父节点的右子节点和右子节点的左子节点。

  2. 更新父节点的右子节点为右子节点的左子节点,并将右子节点的左子节点的父指针指向父节点。

  3. 将父节点的父指针指向右子节点,并将右子节点的左子节点指向父节点。

  4. 判断原父节点是否为根节点,若是,更新根节点为右子节点,并将新根节点的父指针置为空;若不是,根据父节点在其父节点的位置,分别更新其父节点的左子节点或右子节点为右子节点,并将右子节点的父指针指向父节点的父节点。

  5. 最后,将父节点和右子节点的平衡因子都设置为0,表示树已经平衡。

3. 先左单旋再右单旋(新节点插入较高左子树的右侧)

在这里插入图片描述
将双旋变成单旋后再旋转,即:先对30这个节点进行左单旋,然后再对90这个节点进行右单旋,旋转完成后再考虑平衡因子的更新。

下面是先左单旋再右单旋的具体实现代码:

void RotateLR(Node* parent)
{
	// 获取节点C的左子节点A和节点A的右子节点D
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	// 获取节点D的平衡因子,以便后续调整平衡因子时使用
	int bf = subLR->_bf;

	// 对节点A进行左单旋,注意此时节点C的左子节点为节点D,节点D的右子节点为节点A
	RotateL(parent->_left);

	// 对节点C进行右单旋,此时节点D成为新的子树头节点,节点C成为节点D的右子节点
	RotateR(parent);

	// 调整平衡因子
	if (bf == 1) // 如果节点D的平衡因子为1,说明节点D的左子树比右子树高
	{
		parent->_bf = 0; // 节点C的平衡因子变为0
		subLR->_bf = 0; // 节点D的平衡因子变为0
		subL->_bf = -1; // 节点A的平衡因子变为-1,因为它的右子树高度比左子树高度大1
	}
	else if (bf == -1) // 如果节点D的平衡因子为-1,说明节点D的右子树比左子树高
	{
		parent->_bf = 1; // 节点C的平衡因子变为1
		subLR->_bf = 0; // 节点D的平衡因子变为0
		subL->_bf = 0; // 节点A的平衡因子变为0,因为它的左右子树高度相等
	}
	else if (bf == 0) // 如果节点D的平衡因子为0,说明节点D的左右子树高度相等
	{
		parent->_bf = 0; // 节点C的平衡因子变为0
		subLR->_bf = 0; // 节点D的平衡因子变为0
		subL->_bf = 0; // 节点A的平衡因子变为0,因为它的左右子树高度相等
	}
	else // 如果节点D的平衡因子不是1、-1或者0,则说明AVL树已经失去了平衡,这是一个不合法的状态,应该立即报错退出程序。
	{
		assert(false);
	}
}

⭕具体而言,先左单旋再右单旋的操作步骤如下:

  • 首先获取节点C的左子节点A(subL)和节点A的右子节点D(subLR);
  • 然后对节点A进行左单旋(RotateL),此时节点C的左子节点应为节点D,节点D的右子节点应为节点A;
  • 最后对节点C进行右单旋(RotateR),此时节点D成为新的子树头节点,节点C成为节点D的右子节点。

最后一部分使用了if语句判断旋转后各个节点的平衡因子,并进行相应的调整,以便使AVL树保持平衡。

  • 如果节点D的平衡因子为1,说明节点D的左子树比右子树高,需要进行右旋操作,这一次旋转中节点C和节点A都向右移动了一位,而节点D的平衡因子变为0,节点A和节点C的平衡因子都变为-1;
  • 如果节点D的平衡因子为-1,说明节点D的右子树比左子树高,需要进行左旋操作,这一次旋转中节点C和节点A都向左移动了一位,而节点D的平衡因子变为0,节点A和节点C的平衡因子都变为1;
  • 如果节点D的平衡因子为0,说明节点D的左右子树高度相等,不需要进行旋转操作,各个节点的平衡因子均设置为0;
  • 如果节点D的平衡因子不是1、-1或者0,则说明AVL树已经失去了平衡,这是一个不合法的状态,应该立即报错退出程序。
  • 经过这两次旋转后,AVL树重新保持了平衡性和有序性。

4. 先右单旋再左单旋(新节点插入较高右子树的左侧)

在这里插入图片描述
将双旋变成单旋后再旋转,即:先对90这个节点进行右单旋,然后再对30这个节点进行左单旋,旋转完成后再考虑平衡因子的更新。

下面是先右单旋再左单旋的具体实现代码:

void RotateRL(Node* parent)
{
	// 获取节点C的右子节点B和节点B的左子节点E
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	// 获取节点E的平衡因子,以便后续调整平衡因子时使用
	int bf = subRL->_bf;

	// 对节点B进行右单旋,注意此时节点C的右子节点为节点E,节点E的左子节点为节点B
	RotateR(parent->_right);

	// 对节点C进行左单旋,此时节点E成为新的子树头节点,节点C成为节点E的左子节点
	RotateL(parent);

	// 调整平衡因子
	if (bf == 1) // 如果节点E的平衡因子为1,说明节点E的左子树比右子树高
	{
		parent->_bf = -1; // 节点C的平衡因子变为-1
		subRL->_bf = 0; // 节点E的平衡因子变为0
		subR->_bf = 0; // 节点B的平衡因子变为0,因为它的左右子树高度相等
	}
	else if (bf == -1) // 如果节点E的平衡因子为-1,说明节点E的右子树比左子树高
	{
		parent->_bf = 0; // 节点C的平衡因子变为0
		subRL->_bf = 0; // 节点E的平衡因子变为0
		subR->_bf = 1; // 节点B的平衡因子变为1,因为它的右子树高度比左子树高度大1
	}
	else if (bf == 0) // 如果节点E的平衡因子为0,说明节点E的左右子树高度相等
	{
		parent->_bf = 0; // 节点C的平衡因子变为0
		subRL->_bf = 0; // 节点E的平衡因子变为0
		subR->_bf = 0; // 节点B的平衡因子变为0,因为它的左右子树高度相等
	}
	else // 如果节点E的平衡因子不是1、-1或者0,则说明AVL树已经失去了平衡,这是一个不合法的状态,应该立即报错退出程序。
	{
		assert(false);
	}
}

具体过程跟先左单旋再右单旋的过程一样,只不过是顺序有所不一样,这里就不多赘述了。

五、AVL树的删除(了解)

AVL树节点的删除是一个相对复杂的操作,需要考虑多种情况来保持树的平衡性,下面是对它的步骤简单的介绍:

  1. 首先,按照二叉搜索树的规则找到需要删除的节点。如果目标节点不存在,则删除操作结束。

  2. 如果删除的节点是叶子节点(没有子节点),可以直接删除它。此时,只需将其父节点指向它的指针置为空即可。

  3. 如果删除的节点有一个子节点,可以用子节点替代删除的节点。此时,只需将删除节点的父节点指向删除节点的子节点,并删除删除节点。

  4. 如果删除的节点有两个子节点,需要选择一个合适的替代节点来代替删除的节点。一般可以选择删除节点的中序遍历前驱或后继节点作为替代节点。

  5. 选择中序遍历的前驱节点作为替代节点的一种常见策略是:在删除节点的左子树上找到最大的节点,它将成为替代节点。如果选择后继节点作为替代节点,则在删除节点的右子树上找到最小的节点。

  6. 将选择的替代节点的值复制到删除的节点上,并删除替代节点。这样相当于删除了目标节点,但保持了二叉搜索树的结构。

  7. 此时可能导致树失去平衡,需要进行平衡调整。从替代节点的父节点开始,向上遍历到根节点,在每个遍历的节点上根据需要进行左旋、右旋或双旋转等操作。

  8. 在每个遍历节点上,需要更新其高度和平衡因子。如果删除节点后,遍历节点的平衡因子绝对值大于1,则需要进行旋转操作来恢复平衡。

  9. 最后,验证树是否仍然满足AVL树的平衡性和二叉搜索树的性质。可以从根节点开始递归地检查每个节点的平衡因子是否在[-1, 1]的范围内,且左子树的所有节点值小于节点值,右子树的所有节点值大于节点值。

🚨🚨注意:AVL树的节点删除可能触发多次旋转以保持树的平衡,这可能导致性能开销较大。因此,在实际应用中,可以考虑使用其他平衡二叉搜索树的变种,如红黑树,它在插入和删除操作上可能更加高效。后面博主也会对红黑树进行详细的介绍。

六、AVL树的性能

  1. 查找操作:AVL树是一种二叉搜索树,查找操作的平均时间复杂度为O( l o g n log n logn),其中n是树中节点的数量。由于AVL树保持了平衡性,树的高度较低,因此在大多数情况下,查找操作非常高效。

  2. 插入和删除操作:由于插入和删除操作可能引起树的不平衡,需要进行旋转操作来恢复平衡。这些旋转操作的时间复杂度为O( 1 1 1)或O( l o g n log n logn),但由于旋转操作只在沿着路径上最多影响O( l o g n log n logn)个节点,所以插入和删除操作的平均时间复杂度仍然是O( l o g n log n logn)。

  3. 空间复杂度:AVL树与普通二叉搜索树相比,需要额外存储平衡因子来维护树的平衡性。平衡因子通常使用一个额外的字节来表示,因此额外的空间消耗也是O( n n n)。

  4. 平衡维护开销:当进行插入和删除操作时,AVL树需要保持树的平衡性,这可能导致一系列旋转操作。这些旋转操作的开销取决于树的深度和失衡点的位置。在最坏情况下,进行插入和删除操作时可能需要进行O( l o g n log n logn)次旋转操作,因此平衡维护的开销相对较高。

附:详细的AVL模拟代码

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf; // balance factor

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

		// 更新平衡因子
		while (parent)
		{
			if (cur == parent->_right)
			{
				parent->_bf++;
			}
			else
			{
				parent->_bf--;
			}

			if (parent->_bf == 1 || parent->_bf == -1)
			{
				// 继续更新
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 需要旋转处理 -- 1、让这颗子树平衡 2、降低这颗子树的高度
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);
				}

				break;
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	int Height()
	{
		return _Height(_root);
	}
private:

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
	int _Height(Node* root)
	{
		if (root == NULL)
			return 0;

		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);

		return leftH > rightH ? leftH + 1 : rightH + 1;
	}
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);

		if (rightH - leftH != root->_bf)
		{
			cout << root->_kv.first << "节点平衡因子异常" << endl;
			return false;
		}

		return abs(leftH - rightH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);

	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppnode = parent->_parent;
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;
		subR->_left = parent;
		parent->_parent = subR;
		if (ppnode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}
		parent->_bf = subR->_bf = 0;
	}
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppnode = parent->_parent;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}
		subL->_bf = parent->_bf = 0;
	}
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		if (bf == 1)
		{
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subLR->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);
		if (bf == 1)
		{
			parent->_bf = -1;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
			assert(false);
		}

	}


	Node* _root = nullptr;
};

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1061170.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka安装记录

目录 安装依赖 安装zookeeper 可能遇到的报错 下载安装包 解压 修改配置 其他相关配置 修改日志的位置 修改Zookeeper 启动 测试 创建主题 查看主题 插入数据 查看数据量 消费数据 删除主题 安装依赖 由于Kafka是用Scala语言开发的&#xff0c;运行在JVM上&am…

Elasticsearch安装访问

Elasticsearch 是一个开源的、基于 Lucene 的分布式搜索和分析引擎&#xff0c;设计用于云计算环境中&#xff0c;能够实现实时的、可扩展的搜索、分析和探索全文和结构化数据。它具有高度的可扩展性&#xff0c;可以在短时间内搜索和分析大量数据。 Elasticsearch 不仅仅是一个…

C++人事管理系统

一、设计目的 企业员工管理系统主要是针对企业员工的基本信息进行增、删、改、查的相关操作&#xff0c;以便用户使用本管理系统时可以快速对企业员工的信息进行管理。 二、设计内容 1.用户首次使用本系统时进行密码设置和初始化操作。 2.实现添加功能&#xff0c;即添加员工…

计算机竞赛 身份证识别系统 - 图像识别 深度学习

文章目录 0 前言1 实现方法1.1 原理1.1.1 字符定位1.1.2 字符识别1.1.3 深度学习算法介绍1.1.4 模型选择 2 算法流程3 部分关键代码 4 效果展示5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 毕业设计 图像识别 深度学习 身份证识别…

【算法训练-二分查找 四】【模拟二分】X的平方根

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【二分查找】&#xff0c;使用【数组】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…

stm32-SPI协议

SPI协议详解&#xff08;图文并茂超详细&#xff09; SPI通讯协议 于是我们想有没有更好一点的串行通讯方式&#xff1b;相比较于UART&#xff0c;SPI的工作方式略有不同。 SPI是一个同步的数据总线&#xff0c;也就是说它是用单独的数据线和一个单独的时钟信号来保证发送端和…

Django的模版使用(Django-03)

一 模版的使用 模板引擎是一种可以让开发者把服务端数据填充到html网页中完成渲染效果的技术。它实现了 把前端代码和服务端代码分离 的作用&#xff0c;让项目中的业务逻辑代码和数据表现代码分离&#xff0c;让前端开发者和服务端开发者可以更好的完成协同开发。 静态网页&…

Linux文本三剑客---awk

AWK是什么 Linux文本三剑客之一&#xff08;grep,sed,awk&#xff09;&#xff0c;功能最强大的文本工具。 逐行读取输入的文本内容&#xff0c;默认以空格和tab键作为分隔符。但是多个空格或者tab键的空格&#xff0c;会自动压缩成一个&#xff0c;然后按照指定的模式和条件执…

3D 毛玻璃晶质见证卡

效果展示 页面结构 从上面的效果展示来看&#xff0c;页面主要成员是一张卡片&#xff0c;并且卡片上有三个小矩形&#xff0c;而小矩形上会展示对应的内容。 当鼠标悬停在卡片上时&#xff0c;卡片会随着鼠标的移动而改变视角。 CSS3 知识点 transform-style 属性的 prese…

用OpenCV(Python)获取图像的SIFT特征

import cv2 as cv import numpy as np import matplotlib.pyplot as plt imgcv.imread("../Lena.png") img_graycv.cvtColor(img,cv.COLOR_BGR2GRAY)#创建一个SIFI对象 siftcv.SIFT_create()#使用SIFT对象在灰度图像img_gray中检测关键点&#xff0c;结果存储在变量k…

MATLAB中plot3函数用法

目录 语法 说明 向量和矩阵数据 表数据 其他选项 示例 绘制三维螺旋图 绘制多个线条 使用矩阵绘制多个线条 指定等间距刻度单位和轴标签 将点绘制为不带线的标记 自定义颜色和标记 指定线型 在绘图后修改线条 绘制表中的数据 在 x 和 y 轴上绘制多个表变量 指…

【国漫逆袭】人气榜,小医仙首次上榜,霍雨浩排名飙升,不良人热度下降

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析国漫资讯。 为了提升作品和角色的讨论度&#xff0c;增加平台的用户活跃度&#xff0c;小企鹅推出了动漫角色榜&#xff0c;该榜单以【年】【周】【日】为单位&#xff0c;通过角色的点赞量和互动量进行排名 上周的动漫角…

解决dockerfile创建镜像时pip install报错的bug

项目场景&#xff1a; 使用docker-compose创建django容器 问题描述 > [5/5] RUN /bin/bash -c source ~/.bashrc && python3 -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple: 0.954 Looking in indexes: https://…

关于PointHeadBox类的理解

forward函数 def forward(self, batch_dict):"""Args:batch_dict:batch_size:point_features: (N1 N2 N3 ..., C) or (B, N, C)point_features_before_fusion: (N1 N2 N3 ..., C)point_coords: (N1 N2 N3 ..., 4) [bs_idx, x, y, z]point_labels (opti…

Folium笔记:HeatMap

在地图上生成热力图 0 举例 import folium from folium.plugins import HeatMap# 创建一个地图对象 m folium.Map(location(1.34084, 103.83637), zoom_start13)# 创建一个坐标点的数据集 data [(1.431656, 103.827896),(1.424789, 103.789902),(1.325781, 103.860446),(1.…

【算法训练-搜索算法 一】【DFS网格搜索框架】岛屿数量、岛屿的最大面积、岛屿的周长

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【搜索算法】&#xff0c;使用【数组】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…

多卡片效果悬停效果

效果展示 页面结构 从页面的结构上看&#xff0c;在默认状态下毛玻璃卡片是有层次感的效果叠加在一起&#xff0c;并且鼠标悬停在卡片区域后&#xff0c;卡片整齐排列。 CSS3 知识点 transform 属性的 rotate 值运用content 属性的 attr 值运用 实现页面整体布局 <div …

代码随想录算法训练营第五十七天 | 动态规划 part 15 | 392.判断子序列、115.不同的子序列

目录 392.判断子序列思路代码 115.不同的子序列思路代码 392.判断子序列 Leetcode 思路 dp[i][j] 表示以下标i-1为结尾的字符串s&#xff0c;和以下标j-1为结尾的字符串t&#xff0c;相同子序列的长度为dp[i][j]递推公式&#xff1a; 初始化&#xff1a;为0遍历顺序&#xff…

日常工作报告生成器微信小程序源码 支持日报,周报,月报,年终终结

相信大家上班都会有做工作报告的情况吧 那么这款小程序就是大家的福音了 只要输入你的工作内容或者岗位自动生成你的工作报告 支持报,周报,月报,年终终结 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88391810 源码下载2&#xff1a;评论留言或私信…

JVM篇---第二篇

系列文章目录 文章目录 系列文章目录一、简述一下JVM的内存模型二、说说堆和栈的区别三、什么时候会触发FullGC一、简述一下JVM的内存模型 1.JVM内存模型简介 JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一 些区域的数据是线程性独…