数据与结构--AVL树

目录

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 asserttemplate<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) { // 如果父节点的平衡因子为0break; // 更新结束} else if (parent->_bf == -1 || parent->_bf == 1) { // 如果父节点的平衡因子为-1或1cur = parent; // 更新当前节点为父节点parent = parent->_parent; // 更新父节点为爷爷节点} else if (parent->_bf == -2 || parent->_bf == 2) { // 如果父节点的平衡因子为-2或2if (parent->_bf == -2) { // 如果父节点的平衡因子为-2if (cur->_bf == -1) { // 如果新节点在父节点的左子树的左子树中RotateR(parent); // 进行右单旋} else { // 如果新节点在父节点的左子树的右子树中RotateLR(parent); // 进行左右双旋}} else { // 如果父节点的平衡因子为2if (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; //修改结点的valuereturn 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.mzph.cn/diannao/15214.shtml

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

相关文章

LFSR线性反馈移位寄存器及Verilog实现

一、LFSR LFSR线性反馈移位寄存器&#xff0c;通常由移位寄存器和异或门组成&#xff0c;主要用于产生伪随机序列等。 线性反馈的含义是各个寄存器的输出通过一个反馈函数连接到第一级触发器的输入&#xff1b;LFSR中的寄存器的个数被称为LFSR的级数。 LFSR分为两类&#xff…

开源的在线JSON数据可视化编辑器jsoncrack本地部署与远程访问

文章目录 1. 在Linux上使用Docker安装JSONCrack2. 安装Cpolar内网穿透工具3. 配置JSON Crack界面公网地址4. 远程访问 JSONCrack 界面5. 固定 JSONCrack公网地址 JSON Crack 是一款免费的开源数据可视化应用程序&#xff0c;能够将 JSON、YAML、XML、CSV 等数据格式可视化为交互…

Iphone自动化指令每隔固定天数打开闹钟关闭闹钟

1.业务需求&#xff1a;小z每隔五天有一个夜班&#xff0c;然后下午会有三个小时的休息时间&#xff0c;如果闹钟不响就会错过交班日期&#xff0c;但是如果设置闹钟&#xff0c;iPhone的闹钟只能设定固定循环日期闹钟&#xff0c;或者一次的闹钟&#xff0c;导致要么忘记设闹钟…

【C语言】指针运算

前言 前面在“走进指针世界”中我已经讲解过指针相关的很多前置知识&#xff0c;其实还有一个很重要的部分就是指针的运算。这篇博客&#xff0c;就让我们一起了解一下指针的运算吧&#xff01; 指针作为变量&#xff0c;是可以进行算术运算的&#xff0c;只不过情况会和整型…

LLM-Llama在 MAC M1上体验Llama.cpp和通义千问Qwen 1.5-7B

Llama.cpp的主要目标是在各种硬件上&#xff08;本地和云端&#xff09;实现LLM推断&#xff0c;同时保持最小的设置和最先进的性能。 纯C/C实现&#xff0c;没有任何依赖关系Apple芯片是一级的支持对象 - 通过ARM NEON、Accelerate和Metal框架进行优化对x86架构的AVX、AVX2和…

Go语言的命名规范是怎样的?

文章目录 Go语言的命名规范详解一、标识符命名规范示例代码 二、包名命名规范示例代码 三、变量命名规范示例代码 四、常量命名规范示例代码 五、函数命名规范示例代码 总结 Go语言的命名规范详解 在Go语言中&#xff0c;代码的命名规范对于项目的可读性、可维护性和可扩展性至…

前端菜鸡,对于35+程序员失业这个事有点麻了

“经常看到30岁程序员失业的新闻&#xff0c;说实话&#xff0c;有点麻。目前程序员供求关系并未失衡&#xff0c;哪怕是最基础的前端或者后台、甚至事务型的岗位也是足够的。 事实上&#xff0c;现在一个开出的岗位要找到一位尽职尽责能顺利完成工作的程序员并不是一件那么容…

C++_string简单源码剖析:模拟实现string

文章目录 &#x1f680;1.构造与析构函数&#x1f680;2.迭代器&#x1f680;3.获取&#x1f680; 4.内存修改&#x1f680;5. 插入&#x1f680;6. 删除&#x1f680;7. 查找&#x1f680;8. 交换swap&#x1f680;9. 截取substr&#x1f680;10. 比较符号重载&#x1f680;11…

副业树洞聊天项目/树洞倾诉/陪陪系统源码/树洞源码下载搭建

随着社会的发展和人们生活水平的提高&#xff0c;越来越多的人在面临心理压力、情感困扰或生活困境时&#xff0c;需要一个可以宣泄、倾诉和寻求支持的平台。而传统的人际交往方式往往会遇到难以排解的问题&#xff0c;比如担心被他人知晓自己的隐私等&#xff0c;这就导致了人…

package.json中peerDependencies的使用场景

文章目录 peerDependencies 的使用场景peerDependencies 的使用案例为什么使用 peerDependencies需要注意的事项主要作用 ✍创作者&#xff1a;全栈弄潮儿 &#x1f3e1; 个人主页&#xff1a; 全栈弄潮儿的个人主页 &#x1f3d9;️ 个人社区&#xff0c;欢迎你的加入&#xf…

DNS服务的部署与配置(1)

一、DNS的定义 1、域名系统&#xff08;英文&#xff1a;Domain Name System&#xff0c;缩写&#xff1a;DNS&#xff09;是互联网的一项服务。 它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便地访问互联网。 DNS使用UDP端口53。 当前&#xff0…

超简单白话文机器学习 - 回归树树剪枝(含算法介绍,公式,源代码实现以及调包实现)

1. 回归树 1.1 算法介绍 大家看到这篇文章时想必已经对树这个概念已经有基础了&#xff0c;如果不是很了解的朋友可以看看笔者的这篇文章&#xff1a; 超简单白话文机器学习-决策树算法全解&#xff08;含算法介绍&#xff0c;公式&#xff0c;源代码实现以及调包实现&#x…

BL121DT网关在智能电网分布式能源管理中的应用钡铼技术协议网关

随着全球能源结构的转型和智能电网技术的飞速发展&#xff0c;分布式能源管理系统在提高能源利用效率、促进可再生能源接入及保障电网稳定运行方面发挥着日益重要的作用。然而&#xff0c;分布式能源系统内设备种类繁多&#xff0c;通信协议各异&#xff0c;如何高效整合这些设…

如何从http免费升级到https

使用https协议开头是为了在用户访问网站时提供更安全的网络环境。相比http&#xff0c;使用https有数据加密、身份验证、保护隐私、搜索引擎优化等优势。一般获取https证书&#xff0c;则需要支付费用给证书颁发机构&#xff08;CA&#xff09;。还有一些免费的证书证书颁发机构…

解决 SpringBoot 的 Date、LocalDateTime 变成时间戳和数组的问题,创建自定义对象消息转换器

问题描述 SpringBoot 项目&#xff0c;当返回前端的数据类型为 Map 的时候&#xff0c;在 Map 中 put() 时间对象会出现以下问题&#xff1a; 传递的 Date 对象会变成时间戳传递的 LocalDateTime 对象会变成数组 问题复现 编写一个 Controller 方法&#xff0c;返回值为 Ma…

Java并发: 基于Unsafe的CAS实现无锁数据结构

在上一篇Java并发: 面临的挑战文章中说过CAS是解决原子性问题的方案之一。Unsafe提供了CAS的支持&#xff0c;支持实例化对象、访问私有属性、堆外内存访问、线程的启停等功能。 许多Java的并发类库都是基于Unsafe实现的&#xff0c;比如原子类AtomicInteger&#xff0c;并发数…

多线程(C++11)

多线程&#xff08;C&#xff09; 文章目录 多线程&#xff08;C&#xff09;前言一、std::thread类1.线程的创建1.1构造函数1.2代码演示 2.公共成员函数2.1 get_id()2.2 join()2.3 detach()2.4 joinable()2.5 operator 3.静态函数4.类的成员函数作为子线程的任务函数 二、call…

【Linux学习】深入探索进程等待与进程退出码和退出信号

文章目录 退出码return退出 进程的等待进程等待的方法 退出码 main函数的返回值&#xff1a;进程的退出码。 一般为0表示成功&#xff0c;非0表示失败。 每一个非0退出码都表示一个失败的原因&#xff1b; echo $&#xff1f;命令 作用&#xff1a;查看进程退出码。&#xf…

I.MX6ULL Linux C语言开发环境搭建(点灯实验)

系列文章目录 I.MX6ULL Linux C语言开发 I.MX6ULL Linux C语言开发 系列文章目录一、前言二、硬件原理分析三、构建步骤一、 C语言运行环境构建二、软件编写三、链接脚本 四、实验程序编写五、编译下载验证 一、前言 汇编语言编写 LED 灯实验&#xff0c;但是实际开发过程中汇…

Go语言的内存泄漏如何检测和避免?

文章目录 Go语言内存泄漏的检测与避免一、内存泄漏的检测1. 使用性能分析工具2. 使用内存泄漏检测工具3. 代码审查与测试 二、内存泄漏的避免1. 使用defer关键字2. 使用垃圾回收机制3. 避免循环引用4. 使用缓冲池 Go语言内存泄漏的检测与避免 在Go语言开发中&#xff0c;内存泄…