数据与结构--AVL树

news2024/12/28 6:02:14

目录

AVL树的概念

AVL树的性质

AVL树结点的定义 

AVL树的插入 

AVL树的旋转 

左单旋

右单旋

左右双旋

右左单旋

 AVL树的验证

AVL树的查找 

AVL树的修改

 AVL树的删除


AVL树的概念

二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的。

AVL树是一种自平衡二叉搜索树,由Adelson-Velsky和Landis于1962年发明,因其发明者的名字而得名。AVL树通过在每次插入或删除节点后进行旋转操作来保持树的平衡,从而保证其高度始终保持在O(log n)的范围内,从而实现高效的查找、插入和删除操作。

AVL树的性质

  1. 二叉搜索树的性质:AVL树首先是一棵二叉搜索树,所以它必须满足二叉搜索树的性质:对于每个节点N,左子树中所有节点的值都小于N的值,右子树中所有节点的值都大于N的值。

  2. 平衡因子:对于每个节点,AVL树的左子树和右子树的高度差不超过1。这种高度差称为平衡因子(balance factor),即

    平衡因子=左子树高度-右子树高度

    平衡因子只能是-1、0或1。

在AVL树中,平衡因子左子树高度右子树高度是用来评估树的平衡状态的重要指标。

  1. 平衡因子(Balance Factor):平衡因子是指某个节点的左子树高度与右子树高度之差。它表示了该节点子树的平衡情况。平衡因子的计算公式为:

平衡因子=左子树高度-右子树高度

如果平衡因子为正数,则表示左子树高度大于右子树高度;如果为负数,则表示右子树高度大于左子树高度;如果为零,则表示左右子树高度相等。

     2.左子树高度(Height of Left Subtree):左子树的高度是指以某个节点为根的左子树的最大深度,即左子树中从根节点到最深叶子节点的路径长度。

     3.右子树高度(Height of Right Subtree):右子树的高度是指以某个节点为根的右子树的最大深度,即右子树中从根节点到最深叶子节点的路径长度。

假设我们有一个 AVL 树的节点,如下所示:

现在让我们给这个节点的左子树和右子树分别设置一些节点,如下所示:

现在我们来计算平衡因子、左子树高度和右子树高度:

  1. 平衡因子:平衡因子等于左子树的高度减去右子树的高度。 平衡因子 = 左子树高度 - 右子树高度 在这个例子中,平衡因子等于 2 - 2 = 0。

  2. 左子树高度:左子树的高度是指从节点 节点 开始,一直向左走到最底层的高度。 在这个例子中,左子树高度为2,因为从节点 节点 到节点 L2 的路径经过了2个节点。

  3. 右子树高度:右子树的高度是指从节点 节点 开始,一直向右走到最底层的高度。 在这个例子中,右子树高度为2,因为从节点 节点 到节点 R2 的路径经过了2个节点。

如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,其高度可保持O(logN),搜索时间复杂度也是O(logN)。

注意: 这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之差的绝对值不超过1,因为只有满二叉树才能做到每个结点左右子树高度之差均为0。

AVL树结点的定义 

    在这个示例中,我们定义了一个AVL树节点的模板结构体 AVLTreeNode。结构体包含了三个指针 _left_right_parent,分别指向左子节点、右子节点和父节点,还有存储键值对的 _kv 成员变量,以及用于平衡因子的 _bf 成员变量。构造函数初始化了这些成员变量,同时将平衡因子初始化为0,因为新构造的节点的左右子树均为空树。

// 定义AVL树节点模板结构体
template<class K, class V>
struct AVLTreeNode {
    AVLTreeNode<K, V>* _left;    // 左子节点指针
    AVLTreeNode<K, V>* _right;   // 右子节点指针
    AVLTreeNode<K, V>* _parent;  // 父节点指针
    std::pair<K, V> _kv;         // 存储的键值对
    int _bf;                      // 平衡因子

    // 构造函数
    AVLTreeNode(const std::pair<K, V>& kv)
        : _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _kv(kv)
        , _bf(0) // 平衡因子初始设置为0
    {}
};

AVL树的插入 

 AVL树插入结点时有以下三个步骤:

1.按照二叉搜索树的插入方法,找到待插入位置。
2.找到待插入位置后,将待插入结点插入到树中。
3.更新平衡因子,如果出现不平衡,则需要进行旋转。
因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:

1.待插入结点的key值比当前结点小就插入到该结点的左子树。
2.待插入结点的key值比当前结点大就插入到该结点的右子树。
3.待插入结点的key值与当前结点的key值相等就插入失败。

如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。

由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

1.新增结点在parent的右边,parent的平衡因子+ + 。
2.新增结点在parent的左边,parent的平衡因子− − 。
3.每更新完一个结点的平衡因子后,都需要进行以下判断:

1.如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
2.如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
3.如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
判断理由说明:

将节点插入到 AVL 树中时,最初遵循与插入二叉搜索树相同的过程。如果树为空,则将节点作为树的根插入。如果树不为空,则我们沿着根目录向下走,然后递归地沿着树向下搜索插入新节点的位置。此遍历由比较函数引导。在这种情况下,节点始终替换树中外部节点的 NULL 引用(左或右),即该节点要么成为外部节点的左子节点,要么成为外部节点的右子节点。

在此插入之后,如果树变得不平衡,则只有新插入节点的祖先不平衡。这是因为只有这些节点的子树发生了变化。因此,有必要检查每个节点的祖先是否与AVL树的不变量一致:这称为“回溯”。这是通过考虑每个节点的平衡因子来实现的。

由于单次插入时 AVL 子树的高度不能增加超过 1,因此插入后节点的临时平衡因子将在 [–2,+2] 范围内。对于检查的每个节点,如果临时平衡因子保持在 –1 到 +1 的范围内,则只需更新平衡因子,无需旋转。但是,如果临时平衡因子为 ±2,则根于此节点的子树是 AVL 不平衡的,需要轮换。 

在图中,通过插入新节点 Z 作为节点 X 的子节点,该子树 Z 的高度从 0 增加到 1。

代码如下:

#include <cassert> // for assert

template<class K, class V>
bool AVLTree<K, V>::Insert(const std::pair<K, V>& kv) {
    // 如果树为空,则将新节点作为根节点插入
    if (_root == nullptr) {
        _root = new Node(kv); // 创建新节点作为根节点
        return true; // 插入成功
    }

    // 在树中按照二叉搜索树的规则找到待插入位置
    Node* cur = _root; // 从根节点开始查找
    Node* parent = nullptr; // 记录当前节点的父节点
    while (cur) {
        if (kv.first < cur->_kv.first) { // 如果新节点的键值小于当前节点的键值
            parent = cur; // 记录当前节点为父节点
            cur = cur->_left; // 继续在左子树中查找
        } else if (kv.first > cur->_kv.first) { // 如果新节点的键值大于当前节点的键值
            parent = cur; // 记录当前节点为父节点
            cur = cur->_right; // 继续在右子树中查找
        } else {
            // 不允许重复插入相同的键值对
            return false; // 插入失败
        }
    }

    // 创建新节点,并插入到树中
    cur = new Node(kv); // 创建新节点
    if (kv.first < parent->_kv.first) { // 如果新节点的键值小于父节点的键值
        parent->_left = cur; // 插入到父节点的左子树
        cur->_parent = parent; // 设置新节点的父节点
    } else { // 如果新节点的键值大于父节点的键值
        parent->_right = cur; // 插入到父节点的右子树
        cur->_parent = parent; // 设置新节点的父节点
    }

    // 更新从插入节点到根节点的平衡因子,并进行平衡调整
    while (cur != _root) {
        if (cur == parent->_left) { // 如果新节点在父节点的左子树中
            parent->_bf--; // 父节点的平衡因子减1
        } else if (cur == parent->_right) { // 如果新节点在父节点的右子树中
            parent->_bf++; // 父节点的平衡因子加1
        }

        if (parent->_bf == 0) { // 如果父节点的平衡因子为0
            break; // 更新结束
        } else if (parent->_bf == -1 || parent->_bf == 1) { // 如果父节点的平衡因子为-1或1
            cur = parent; // 更新当前节点为父节点
            parent = parent->_parent; // 更新父节点为爷爷节点
        } else if (parent->_bf == -2 || parent->_bf == 2) { // 如果父节点的平衡因子为-2或2
            if (parent->_bf == -2) { // 如果父节点的平衡因子为-2
                if (cur->_bf == -1) { // 如果新节点在父节点的左子树的左子树中
                    RotateR(parent); // 进行右单旋
                } else { // 如果新节点在父节点的左子树的右子树中
                    RotateLR(parent); // 进行左右双旋
                }
            } else { // 如果父节点的平衡因子为2
                if (cur->_bf == -1) { // 如果新节点在父节点的右子树的左子树中
                    RotateRL(parent); // 进行右左双旋
                } else { // 如果新节点在父节点的右子树的右子树中
                    RotateL(parent); // 进行左单旋
                }
            }
            break; // 平衡调整完成
        } else {
            // 树的平衡因子不符合规范
            assert(false); // 报错
        }
    }

    return true; // 插入成功
}

AVL树的旋转 

显示将多个元素插入到 AVL 树中的动画。它包括左、右、左-右和右-左旋转。

左单旋

当AVL树中的节点的左子树高度比右子树高度多两层以上时,就需要进行左单旋操作,以恢复AVL树的平衡。左单旋操作是AVL树中的一种旋转操作,用于减小左子树的高度,增加右子树的高度,以达到平衡。

下面是左单旋的具体步骤:

  1. 找到需要进行左单旋的节点:首先,需要找到AVL树中需要进行左单旋的节点。这个节点的左子树高度必须比右子树高度多两层以上。

  2. 执行左单旋

    • 让该节点的左孩子成为新的根节点。
    • 将新的根节点的右孩子变为原根节点的左孩子。
    • 原根节点成为新根节点的右孩子。
  3. 更新高度:左单旋操作后,需要更新所有受影响节点的高度信息,以确保树的平衡性。

下面通过示例图来展示左单旋的过程: 

现在,我们来计算每个节点的平衡因子:

  • A的平衡因子为2(右子树的高度为0,左子树的高度为2)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • C的平衡因子为0(左右子树的高度均为0)。

现在我们明确了为什么需要左单旋操作:节点A的平衡因子为2,而左子树的平衡因子为2。这导致了不平衡,因此我们需要通过左单旋来解决这个问题。

现在我们来执行左单旋操作:

在执行左单旋后,树的结构变为这样。现在,让我们重新计算每个节点的平衡因子:

  • B的平衡因子为0(左右子树的高度均为1)。
  • A的平衡因子为0(左右子树的高度均为0)。
  • C的平衡因子为0(左右子树的高度均为0)。

现在,整棵树重新达到平衡,每个节点的平衡因子都在合理范围内。左单旋操作通过将节点A的左孩子B提升为根节点,调整了树的结构,使得树重新平衡。

代码如下:

//左单旋
void RotateL(Node* parent)
{
    // 1、声明临时指针变量
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

    // 2、建立新的父子关系
	parent->_parent = subR;
	subR->_left = parent;

    // 3、建立新的父子关系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

    // 4、建立新的祖父子关系
	if (parentParent == nullptr)
	{
		_root = subR;
		subR->_parent = nullptr; //subR的_parent指向需改变
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}

    // 5、更新平衡因子
	subR->_bf = parent->_bf = 0;
}

这个函数实现了左单旋操作,将以指定节点 parent 为根的子树进行左单旋,以调整AVL树的结构并保持平衡。下面是对每一行代码的解释:

  1. 声明临时指针变量:subR 指向父节点的右孩子,subRL 指向 subR 的左孩子,parentParent 指向父节点的父节点。
  2. 建立新的父子关系:将 parent 的父节点指针指向 subR,将 subR 的左孩子指针指向 parent,完成了新的根节点和其左子节点的关系建立。
  3. 建立新的父子关系:将 parent 的右孩子指针指向 subRL,如果 subRL 不为空,则将 subRL 的父节点指针指向 parent,这样完成了新的父节点与其右子节点的关系建立。
  4. 建立新的祖父子关系:如果 parent 的父节点为空,说明 parent 是根节点,更新树的根节点指针 _rootsubR,否则根据 parent 在其父节点中的位置,更新其父节点的左孩子或右孩子为 subR,同时更新 subR 的父节点为 parentParent
  5. 更新平衡因子:将被旋转的节点(原先的父节点 parent)以及新的根节点 subR 的平衡因子都设置为0,因为它们的左右子树高度相等,树重新达到平衡状态。

右单旋

右单旋是AVL树中的一种旋转操作,用于解决树中某个节点的右子树高度比左子树高度多两层以上的情况。右单旋通过将当前节点的右孩子向上提升为新的根节点,以减小右子树的高度,增加左子树的高度,从而保持树的平衡。

下面是右单旋的具体步骤:

  1. 找到需要进行右单旋的节点:首先,需要找到AVL树中需要进行右单旋的节点。这个节点的右子树高度必须比左子树高度多两层以上。

  2. 执行右单旋

    • 让该节点的右孩子成为新的根节点。
    • 将新的根节点的左孩子变为原根节点的右孩子。
    • 原根节点成为新根节点的左孩子。
  3. 更新高度:右单旋操作后,需要更新所有受影响节点的高度信息,以确保树的平衡性。

现在,让我们通过一个示例来演示右单旋的过程:

示例:

假设我们有以下的AVL树结构,其中节点A需要进行右单旋:

初始状态:

 

现在我们来计算每个节点的平衡因子:

  • A的平衡因子为-2(右子树的高度为1,左子树的高度为0)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • C的平衡因子为0(左右子树的高度均为0)。

节点A的平衡因子为-2,表示右子树的高度比左子树的高度高2。这导致了不平衡,因此我们需要通过右单旋来解决这个问题。

现在我们来执行右单旋操作:

在执行右单旋后,树的结构变为这样。现在,让我们重新计算每个节点的平衡因子:

  • B的平衡因子为0(左右子树的高度均为1)。
  • A的平衡因子为0(左右子树的高度均为0)。
  • C的平衡因子为0(左右子树的高度均为0)。

现在,整棵树重新达到平衡,每个节点的平衡因子都在合理范围内。右单旋操作通过将节点A的右孩子B提升为根节点,调整了树的结构,使得树重新平衡。

代码如下:

//右单旋
void RotateR(Node* parent)
{
    // 1、声明临时指针变量
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

    // 2、建立新的父子关系
	subL->_right = parent;
	parent->_parent = subL;

    // 3、建立新的父子关系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

    // 4、建立新的祖父子关系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}

	// 5、更新平衡因子
	subL->_bf = parent->_bf = 0;
}

解释:

  1. 声明临时指针变量:subL 指向父节点的左孩子,subLR 指向 subL 的右孩子,parentParent 指向父节点的父节点。
  2. 建立新的父子关系:将 subL 的右孩子指针指向 parent,将 parent 的父节点指针指向 subL,完成了新的根节点和其右子节点的关系建立。
  3. 建立新的父子关系:将 parent 的左孩子指针指向 subLR,如果 subLR 不为空,则将 subLR 的父节点指针指向 parent,这样完成了新的父节点与其左子节点的关系建立。
  4. 建立新的祖父子关系:如果 parent 的父节点为空,说明 parent 是根节点,更新树的根节点指针 _rootsubL,否则根据 parent 在其父节点中的位置,更新其父节点的左孩子或右孩子为 subL,同时更新 subL 的父节点为 parentParent
  5. 更新平衡因子:将被旋转的节点(原先的父节点 parent)以及新的根节点 subL 的平衡因子都设置为0,因为它们的左右子树高度相等,树重新达到平衡状态。

左右双旋

左右双旋是AVL树中的一种旋转操作,用于解决某个节点的左子树的右子树高度大于左子树高度,右子树的左子树高度大于右子树高度的情况。这种情况需要通过先进行右单旋再进行左单旋的方式来进行调整,以保持AVL树的平衡。

下面是左右双旋操作的一般步骤:

  1. 找到需要进行左右双旋的节点,假设为节点A。
  2. 首先对A的左子节点进行左单旋操作,然后再对A进行右单旋操作。

下面是一个示例,展示了左右双旋的过程:

假设我们有以下的AVL树结构,其中节点A需要进行左右双旋:

初始状态:

  • A的平衡因子为2(右子树的高度为0,左子树的高度为2)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • C的平衡因子为-2(左子树的高度为1,右子树的高度为3)。
  • D的平衡因子为0(左右子树的高度均为1)。

左单旋操作后:

 

  • A的平衡因子为1(右子树的高度为1,左子树的高度为0)。
  • C的平衡因子为0(左右子树的高度均为1)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • D的平衡因子为0(左右子树的高度均为1)。

右单旋操作后:

 

  • C的平衡因子为0(左右子树的高度均为1)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • A的平衡因子为0(左右子树的高度均为1)。
  • D的平衡因子为0(左右子树的高度均为1)。

通过这个分析,我们可以看到,在左右双旋的过程中,每个节点的平衡因子都被正确地调整为了0,使得整棵树重新达到平衡状态。

代码如下:

//左右双旋
void RotateLR(Node* parent)
{
    // 1、声明临时指针变量
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf; //记录subLR的平衡因子,必定不为nullptr,因为subL的平衡因子为1

    // 2、以subL为旋转点进行左单旋
	RotateL(subL);

    // 3、以parent为旋转点进行右单旋
	RotateR(parent);

    // 4、更新平衡因子
	if (bf == 1) // subLR原先的平衡因子为1
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1) // subLR原先的平衡因子为-1
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0) // subLR原先的平衡因子为0
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false); // 在旋转前树的平衡因子就有问题
	}
}
  1. 声明临时指针变量:subL 指向父节点的左孩子,subLR 指向 subL 的右孩子,bf 存储 subLR 的平衡因子。由于左单旋操作之前,subL 的平衡因子为1,因此 subLR 的平衡因子必定存在且不为nullptr。
  2. subL 为旋转点进行左单旋操作。
  3. parent 为旋转点进行右单旋操作。
  4. 更新平衡因子:根据 subLR 的原先平衡因子的情况,更新 subLRsubLparent 的平衡因子。

右左单旋

右左单旋是AVL树中的一种旋转操作,用于解决某个节点的右子树的左子树高度大于右子树高度,左子树的右子树高度大于左子树高度的情况。这种情况需要通过先进行左单旋再进行右单旋的方式来进行调整,以保持AVL树的平衡。

下面是右左单旋操作的一般步骤:

  1. 找到需要进行右左单旋的节点,假设为节点A。
  2. 首先对A的右子节点进行右单旋操作,然后再对A进行左单旋操作。

下面是一个示例,展示了右左单旋的过程:

假设我们有以下的AVL树结构,其中节点A需要进行右左单旋:

初始状态:

  • A的平衡因子为-2(右子树的高度为1,左子树的高度为3)。
  • B的平衡因子为0(左右子树的高度均为2)。
  • C的平衡因子为-2(右子树的高度为1,左子树的高度为3)。
  • D的平衡因子为0(左右子树的高度均为1)。

右单旋操作后:

 

  • A的平衡因子为-1(右子树的高度为1,左子树的高度为2)。
  • C的平衡因子为0(左右子树的高度均为2)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • D的平衡因子为0(左右子树的高度均为1)。

左单旋操作后:

 

  • C的平衡因子为0(左右子树的高度均为2)。
  • A的平衡因子为0(左右子树的高度均为1)。
  • B的平衡因子为0(左右子树的高度均为1)。
  • D的平衡因子为0(左右子树的高度均为1)。

通过这个分析,我们可以看到,在右左单旋的过程中,每个节点的平衡因子都被正确地调整为了0,使得整棵树重新达到平衡状态。

代码如下:

//右左双旋
void RotateRL(Node* parent)
{
    // 1、声明临时指针变量
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

    // 2、以subR为轴进行右单旋
	RotateR(subR);

    // 3、以parent为轴进行左单旋
	RotateL(parent);

    // 4、更新平衡因子
	if (bf == 1) // subRL原先的平衡因子为1
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1) // subRL原先的平衡因子为-1
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0) // subRL原先的平衡因子为0
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false); // 在旋转前树的平衡因子就有问题
	}
}
  1. 声明临时指针变量:subR 指向父节点的右孩子,subRL 指向 subR 的左孩子,bf 存储 subRL 的平衡因子。由于右单旋操作之前,subR 的平衡因子为-1,因此 subRL 的平衡因子必定存在且不为nullptr。
  2. subR 为轴进行右单旋操作。
  3. parent 为轴进行左单旋操作。
  4. 更新平衡因子:根据 subRL 的原先平衡因子的情况,更新 subRLparentsubR 的平衡因子。

 AVL树的验证

AVL树的验证主要包括两个方面:结构的正确性和平衡性。

  1. 结构的正确性

    • 确保树中不存在重复的节点。
    • 确保每个节点的左子树和右子树都是二叉搜索树,即左子树中的所有节点值小于当前节点的值,右子树中的所有节点值大于当前节点的值。
  2. 平衡性

    • 对于每个节点,其左子树的高度与右子树的高度之差的绝对值不超过1。
    • 遍历整棵树,验证每个节点的平衡因子是否满足AVL树的定义。
#include <iostream>
#include <algorithm>

// AVL树节点结构
struct Node {
    int key;        // 节点的关键值
    int height;     // 节点的高度
    Node* left;     // 左子节点指针
    Node* right;    // 右子节点指针
};

// 计算节点的高度
int height(Node* node) {
    if (node == nullptr) return 0;
    return node->height;
}

// 计算节点的平衡因子
int balanceFactor(Node* node) {
    if (node == nullptr) return 0;
    return height(node->left) - height(node->right);
}

// 检查AVL树的平衡性
bool isBalanced(Node* root) {
    if (root == nullptr) return true;
    
    int bf = balanceFactor(root);
    if (std::abs(bf) > 1) return false;

    return isBalanced(root->left) && isBalanced(root->right);
}

// 验证AVL树
bool validateAVL(Node* root) {
    // 检查结构的正确性
    // 这一步是根据具体实现来进行的,通常需要确保树中不存在重复的节点,并且每个节点的左子树中所有节点的值小于当前节点的值,右子树中所有节点的值大于当前节点的值。

    // 检查平衡性
    return isBalanced(root);
}

int main() {
    // 构建AVL树,这里假设已经构建好了AVL树的结构
    
    // 验证AVL树
    if (validateAVL(root)) {
        std::cout << "这是一个有效的AVL树。" << std::endl;
    } else {
        std::cout << "这不是一个有效的AVL树。" << std::endl;
    }

    return 0;
}
  1. #include <iostream>#include <algorithm>:这两行代码是预处理指令,用于包含标准输入输出流和标准库中的算法函数。
  2. struct Node:定义了 AVL 树节点的结构,包括节点的关键值 key、高度 height,以及指向左右子节点的指针 leftright
  3. height(Node* node):计算节点的高度的函数。如果节点为空,则返回高度为0;否则返回节点的高度。
  4. balanceFactor(Node* node):计算节点的平衡因子的函数。如果节点为空,则返回0;否则返回节点的左子树高度减去右子树高度。
  5. isBalanced(Node* root):检查 AVL 树的平衡性的函数。如果根节点为空,则树是平衡的;否则计算根节点的平衡因子,如果绝对值大于1,则树不平衡;否则递归地检查左右子树的平衡性。
  6. validateAVL(Node* root):验证 AVL 树的函数。在这个函数中,首先会检查树的结构的正确性(此处未提供具体实现),然后调用 isBalanced 函数来检查树的平衡性。
  7. main() 函数:在 main() 函数中,构建了 AVL 树的结构(这里未提供具体实现),然后调用 validateAVL 函数来验证 AVL 树的有效性。如果返回值为 true,则打印 "这是一个有效的AVL树。";否则打印 "这不是一个有效的AVL树。"。

AVL树的查找 

AVL树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

若树为空树,则查找失败,返回nullptr。
若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
若key值等于当前结点的值,则查找成功,返回对应结点。
代码如下:

// 查找函数
Node* Find(const K& key) {
    Node* cur = _root; // 从根节点开始查找
    while (cur) { // 循环直到当前节点为空(即查找到叶子节点)
        if (key < cur->_kv.first) { // 如果目标键值小于当前节点的键值
            cur = cur->_left; // 则在当前节点的左子树中继续查找
        } else if (key > cur->_kv.first) { // 如果目标键值大于当前节点的键值
            cur = cur->_right; // 则在当前节点的右子树中继续查找
        } else { // 如果目标键值等于当前节点的键值,表示找到了目标节点
            return cur; // 返回指向目标节点的指针
        }
    }
    return nullptr; // 如果循环结束仍未找到目标节点,则返回空指针表示查找失败
}

AVL树的修改

实现修改AVL树当中指定key值结点的value,我们可以实现一个Modify函数,该函数当中的逻辑如下:

  1. 调用查找函数获取指定key值的结点。
  2. 对该结点的value进行修改。
//修改函数
bool Modify(const K& key, const V& value)
{
	Node* ret = Find(key);
	if (ret == nullptr) //未找到指定key值的结点
	{
		return false;
	}
	ret->_kv.second = value; //修改结点的value
	return true;
}

 AVL树的删除

AVL树的删除操作是指从树中删除一个特定的节点。删除操作可能会破坏AVL树的平衡性,因此在删除节点后,需要进行平衡操作以确保AVL树的平衡性。

删除概念:

AVL树的删除操作分为以下几种情况:

  1. 如果要删除的节点是叶子节点(即没有子节点),则直接删除该节点。
  2. 如果要删除的节点只有一个子节点,则将其子节点替换为当前节点。
  3. 如果要删除的节点有两个子节点,则找到该节点的中序后继节点(即右子树中的最小节点),将其值复制到当前节点,然后删除中序后继节点。

在进行这些操作后,需要重新计算每个节点的高度和平衡因子,并执行旋转操作来恢复AVL树的平衡性。

删除规则:

删除节点后,为了保持AVL树的平衡性,需要进行以下步骤:

  1. 删除目标节点。
  2. 从被删除节点的父节点开始向上回溯,重新计算每个祖先节点的高度和平衡因子。
  3. 如果发现某个祖先节点的平衡因子超过了1或-1,则进行相应的旋转操作来恢复平衡。

删除步骤:

  1. 找到要删除的目标节点。
  2. 根据目标节点的情况,分情况处理:
    • 如果目标节点是叶子节点,直接删除。
    • 如果目标节点只有一个子节点,用其子节点替换当前节点。
    • 如果目标节点有两个子节点,找到其中序后继节点,将其值复制到当前节点,然后删除中序后继节点。
  3. 删除节点后,从其父节点开始向上回溯,重新计算每个祖先节点的高度和平衡因子。
  4. 如果需要,进行旋转操作以恢复AVL树的平衡性。

 代码如下:

// 删除节点
void Delete(const K& key) {
    _root = DeleteNode(_root, key); // 递归地删除节点并更新根节点
}

Node* DeleteNode(Node* root, const K& key) {
    if (root == nullptr) {
        return root; // 如果当前节点为空,直接返回
    }

    if (key < root->_kv.first) {
        root->_left = DeleteNode(root->_left, key); // 如果删除值小于当前节点值,递归地删除左子树节点
    } else if (key > root->_kv.first) {
        root->_right = DeleteNode(root->_right, key); // 如果删除值大于当前节点值,递归地删除右子树节点
    } else {
        if (root->_left == nullptr || root->_right == nullptr) {
            Node* temp = (root->_left != nullptr) ? root->_left : root->_right;
            if (temp == nullptr) {
                temp = root;
                root = nullptr;
            } else {
                *root = *temp;
            }
            delete temp;
        } else {
            Node* temp = MinValueNode(root->_right); // 找到右子树的最小值节点
            root->_kv = temp->_kv; // 将右子树的最小值节点的值复制给当前节点
            root->_right = DeleteNode(root->_right, temp->_kv.first); // 递归地删除右子树的最小值节点
        }
    }

    if (root == nullptr) {
        return root; // 如果当前节点为空,直接返回
    }

    // 更新当前节点的高度
    root->height = 1 + std::max(height(root->_left), height(root->_right));

    // 检查当前节点的平衡因子是否超过了1或-1,如果超过则进行旋转操作
    int balance = balanceFactor(root);
    if (balance > 1 && balanceFactor(root->_left) >= 0) {
        return RotateRight(root);
    }
    if (balance > 1 && balanceFactor(root->_left) < 0) {
        root->_left = RotateLeft(root->_left);
        return RotateRight(root);
    }
    if (balance < -1 && balanceFactor(root->_right) <= 0) {
        return RotateLeft(root);
    }
    if (balance < -1 && balanceFactor(root->_right) > 0) {
        root->_right = RotateRight(root->_right);
        return RotateLeft(root);
    }

    return root;
}

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

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

相关文章

福昕PDF使用技巧

因为突然间学校的企业版WPS突然很多功能就不能使用了&#xff0c;所以转向福昕PDF。 一、合并文件 添加需要合并的文件&#xff0c;可以使用ctrla等方式全选 找到最上方的“合并文件” 二、文本注释

linux中最常用的文件管理命令

linux中最常用的文件管理命令 linux中最常用的文件管理命令最常用的且没有之一的 ls命令格式不加任何参数使用-l显示更多细节使用-t按照时间排序使用-r按照时间逆序使用-S根据文件大小排序 查看庐山真面貌的cat实例 &#xff1a;简单显示内容实例 &#xff1a;显示行号 -n实例 …

Python实现xml解析并输出到Excel上

1.编写xml文件 2.使用Python的ElementTree模块来解析XML import xml.etree.ElementTree as ET from openpyxl import Workbook # 解析XML函数 def parse_xml(xml_file):tree ET.parse(xml_file)root tree.getroot() --打开根节点data []for user in root.findall(Users/Us…

LeetCode 124 —— 二叉树中的最大路径和

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 二叉树的问题首先我们要想想是否能用递归来解决&#xff0c;本题也不例外&#xff0c;而递归的关键是找到子问题。 我们首先来看看一棵最简单的树&#xff0c;也就是示例 1。这样的一棵树总共有六条路径&#xf…

【三维修复、分割与编辑】InFusion、Bootstrap 3D、GaussianGrouping、GaussianEditor等(论文总结)

提示&#xff1a; 文章目录 前言一、InFusion&#xff1a;扩散模型助力&#xff0c;效率提高20倍&#xff01;(2024)1. 摘要2. 算法3. 效果 二、2D Gaussian Splatting三、Bootstrap 3D:从扩散模型引导三维重建1.摘要2.相关工作3.方法1.Boostrapping by Diffusion 通过扩散模型…

文心一言 VS 讯飞星火 VS chatgpt (267)-- 算法导论20.2 2题

二、写出 PROTO-vEB-DELETE 的伪代码。通过扫描簇内的相关位&#xff0c;来更新相应的 summary 位。并且你实现的伪代码的最坏情况运行时间是多少&#xff1f;如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 首先&#xff0c;让我们回顾一下vEB&#xff08;Van …

项目管理-质量管理

目录 一、质量管理概述 1.1 GB/T16260.1-2006 定义 1.2 GB/T19000-ISO 9000(2000)系列标准定义 二、软件质量模型 2.1 软件全生命周期质量模型 2.1.1 内部和外部质量的质量模型 2.1.2 使用质量的质量模型 2.1.3 McCall 质量模型 2.1.4 质量特性度量 2.1.5 相关概念 三…

对话掌阅科技CTO孙凯:如何用生成式AI重新定义阅读体验

导读&#xff1a;生成式AI能对阅读做本质的改造吗&#xff1f; 读一本好书&#xff0c;犹如和一位智者对话。 对于很多热爱读书的人&#xff0c;这是一种令人憧憬的阅读体验。以往&#xff0c;这种对话只能在读者的头脑和思维中进行。 有没有可能&#xff0c;读者可以随时随地和…

使OpenCV可以读取中文路径图片的方法

一&#xff0e;问题复现 1.代码 #! /usr/bin/env python # -*- coding: utf-8 -*-# File: show_img.pyimport cv2# 读取图片 img cv2.imread("车牌素材/冀A.png")# 显示图片 cv2.imshow("img", img) cv2.waitKey(0)2.报错截图 3.报错内容 [ WARN:00.05…

[JAVASE] 类和对象综合应用 -- 图书管理系统

目录 零. 概览 一. 抽象出图书管理系统所涉及的对象 1.1 Book 1.2 User 1.3 Operation 二. 实现 User 包中的对象 2.1 User父类 2.2 NormalUser 对象 2.3 AdminUser 对象 2.4 小总结(1) 三. 实现Book包中的对象 3.1 Book 对象 3.2 BookList 对象 四. 实现 Operation…

大数据工具之HIVE-参数调优,调度乱码(二)

一、调度乱码 在利用HUE工具,搭建WORKFLOW流程的过程中,如果直接执行hivesql数据正常,不会出现乱码现象,如果利用WORKFLOW搭建的流程,进行数据的拉取,会出现数据中文乱码现象,这些乱码主要是由于select 中的硬编码中文导致出现的现象 具体现象如下: select case when …

【WEB前端2024】开源智体世界:乔布斯3D纪念馆-第26节-内嵌blender展厅

【WEB前端2024】开源智体世界&#xff1a;乔布斯3D纪念馆-第26节-内嵌blender展厅 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界…

人工智能万卡 GPU 集群的硬件和网络架构

万卡 GPU 集群互联&#xff1a;硬件配置和网络设计 一、背景 自从 OpenAI 推出 ChatGPT 以来&#xff0c;LLM 迅速成为焦点关注的对象&#xff0c;并取得快速发展。众多企业纷纷投入 LLM 预训练&#xff0c;希望跟上这一波浪潮。然而&#xff0c;要训练一个 100B 规模的 LLM&a…

OpenWrt U盘安装使用 详细教程 x86/64平台 软路由实测 系列一

1 官方稳定 版:OpenWrt 23.05 OpenWrt Downloads #根据实际情况选择 PC支持uefi,选择版本&#xff1a;https://downloads.openwrt.org/releases/23.05.3/targets/x86/64/openwrt-23.05.3-x86-64-generic-ext4-combined-efi.img.gz 2 rufus 制作U盘启动 3 制作好的U盘,接入主…

经典链表题-链表回文结构

&#x1f389;&#x1f389;&#x1f389;欢迎莅临我的博客空间&#xff0c;我是池央&#xff0c;一个对C和数据结构怀有无限热忱的探索者。&#x1f64c; &#x1f338;&#x1f338;&#x1f338;这里是我分享C/C编程、数据结构应用的乐园✨ &#x1f388;&#x1f388;&…

传输层——UDP

在学习计算机网络的过程中&#xff0c;我们知道OSI七层协议模型&#xff0c;但是在实际开发应 用中我们发现OSI七层协议模型并不适合实施&#xff0c;因为OSI上三层通常都是由开 发人员统一完成的&#xff0c;这三层之间在实现过程中没有一个明确的界限&#xff0c;所以我 们更…

Windows平台C#版RTSP转RTMP直播推送定制版

技术背景 前几年我们发布了C版的多路RTMP/RTSP转RTMP转发官方定制版。在秉承低延迟、灵活稳定、低资源占用的前提下&#xff0c;客户无需关注开发细节&#xff0c;只需图形化配置转发等各类参数&#xff0c;实现产品快速上线目的。 如监控类摄像机、NVR等&#xff0c;通过厂商…

关于堆排序

今天我们不刷力扣了&#xff0c;我们来复习&#xff08;手撕&#xff09;一下数据结构中的八大排序算法之一&#xff0c;堆排序 基本概念&#xff1a; 堆是一种特殊的树形数据结构&#xff0c;即完全二叉树。 堆分为大顶堆和小顶堆&#xff1a; 大顶堆&#xff1a;每个节点的值…

手机上制作证件照

最近由于需要给老姐弄一组证件照&#xff0c;找了一通手机上的软件&#xff0c;找到一款性价比较高的&#xff0c;详细流程记录下来。vx小程序上搜索"泰世茂证件照"&#xff0c;打开首页如下图所示∶ 单击"开始制作" &#xff0c;选择一个证件照类别&#…

Python中Web开发-FastAPI框架

大家好&#xff0c;在当今Web开发领域&#xff0c;高性能、易用性和可扩展性是开发者们追求的目标。Python作为一种流行的编程语言&#xff0c;在Web开发领域也有着强大的影响力。而在众多的Python Web框架中&#xff0c;FastAPI凭借其快速、现代和易用的特性&#xff0c;成为了…