【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术

简介: 【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术

前言: 在数据结构的浩瀚海洋中,AVL树(Adelson-Velsky和Landis发明的树)以其独特的平衡机制和高效的搜索性能,成为了一颗璀璨的明星。它不仅解决了二叉搜索树在数据插入和删除时可能产生的失衡问题,更通过旋转操作,使得树的高度始终保持在一个相对较低的水平,从而保证了搜索的高效性

AVL树的学习并非一蹴而就。它需要我们深入理解其背后的数学原理和算法思想,掌握其插入、和旋转等操作的具体实现,并在实践中不断摸索和优化。只有经过这样的过程,我们才能真正掌握AVL树的精髓,并在实际项目中灵活运用

本篇我们将详细介绍AVL树的基本概念、性质、插入操作的具体实现、旋转操作的原理和技巧等内容!

让我们一起踏上学习 AVL树 的旅程,探索它带来的无尽可能!


📒1. AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

注意: 如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O ( l o g 2 n ) O(log_2 n)O(log2n),搜索时间复杂度O(l o g 2 n log_2 nlog2n)


📙2. AVL树节点的定义

AVL树节点的定义通常包含以下几个关键部分:

基本元素:

  • _left:指向节点的左子节点的指针
  • _right:指向节点的右子节点的指针
  • _parent:指向节点的父节点的指针
  • _kv:一个结构体或配对(pair),包含节点的键值(key)和值(value)。这取决于AVL树的具体用途,可能只包含键或包含键值对。

平衡因子(_bf):

  • 一个整数,表示节点左子树和右子树的高度差。AVL树的性质要求任何节点的平衡因子的绝对值不超过1(-1, 0, 1)

构造函数:

  • 初始化一个新节点时,通常需要一个构造函数,它接受一个键值对(或仅键),并设置节点的左子节点、右子节点、父节点和平衡因子(初始化为0)

节点定义示例(C++):

template<class K,class V>
struct AVLTreeNode
{
  AVLTreeNode<K, V>* _left; // 该节点的左孩子
  AVLTreeNode<K, V>* _right; // 该节点的右孩子
  AVLTreeNode<K, V>* _parent; // 该节点的父亲
  
  pair<K, V> _kv;  // pair
  int _bf; // balance factor 该节点的平衡因子
  AVLTreeNode(const pair<K, V>& kv)
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _kv(kv)
    , _bf(0)
  {}
};

📜3. AVL树的插入

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

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

在我们进行插入操作之前,我们先定义一个AVL树的类

AVL树定义示例(C++):

template<class K,class V>
class AVLTree
{
  typedef AVLTreeNode<K, V> Node;
public:
  // 其他未实现的成员函数
private:
  Node* _root = nullptr;
};

cur插入后,parent的平衡因子一定需要调整

在插入之前,parent的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  • 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
  • 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可

插入后,parent的平衡因子可能有三种情况:0,正负1, 正负2

  • 如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整
    成0,此时满足AVL树的性质,插入成功
  • 如果parent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
    新成正负1,此时以parent为根的树的高度增加,需要继续向上更新
  • 如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进
    行旋转处理

AVL树的插入操作类似于我们之前二叉搜索树的插入,只不过AVL树的插入操作涉及到旋转操作,我们先演示一下它的全部代码

AVL树插入示例(C++):

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->_left;
    }
    else if (cur->_kv.first < kv.first)
    {
      parent = cur;
      cur = cur->_right;
    }
    else
    {
    return false;
    }
  }
  // 链接新节点
  cur = new Node(kv);
  if (parent->_kv.first > kv.first)
  {
    parent->_left = cur;
    cur->_parent = parent;
  }
  else
  {
    parent->_right = cur;
    cur->_parent = parent;
  }
  // 更改平衡因子
  while (parent)
  {
    if (cur == parent->_left)
    {
      parent->_bf--;
    }
    else
    {
      parent->_bf++;
    } 
    if (parent->_bf == 0)
    {
      break;
    }
    // 向上遍历,判断祖先是否受到影响
    else if (parent->_bf == 1 || parent->_bf == -1)
    {
      cur = parent;
      parent = parent->_parent;
    }
    // 平衡出现问题,需要进行修理
    else if(parent->_bf == 2 || parent->_bf == -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);
      }
      break;
    }
    else
    {
      assert(false);
    }
  }
  return true;
}

📚4. AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,
使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:


🌈右单旋

新节点插入较高左子树的左侧—左左:

此处旋转是将30的右子树变成60的左子树,然后让60成为30的右子树

在旋转中有几点要注意:

  • 30这个节点的右孩子可能不存在
  • 60这个节点可能是根节点,也可能是子树

如果是根节点,旋转完成后,要更新根节点

如果是子树,可能是某个节点的左子树,也可能是右子树

AVL树右单旋示例(C++):

void RotateR(Node* parent)
{
  // 定义parent的左孩子 和 parent的左孩子的右孩子
  Node* subL = parent->_left;
  Node* subLR = subL->_right;
  parent->_left = subLR;
  if (subLR)
  {
    subLR->_parent = parent;
  }
  // 当旋转点是子树时,保留父亲节点
  Node* Parentparent = parent->_parent;
  subL->_right = parent;
  parent->_parent = subL;
  // 当旋转点是根时,跟新根节点
  if (_root == parent)
  {
    _root = subL;
    subL->_parent = nullptr;
  }
  // 当旋转点是子树时,更新链接
  else
  {
    if (parent == Parentparent->_left)
    {
      Parentparent->_left = subL;
    }
    else
    {
      Parentparent->_right = subL;
    }
    subL->_parent = Parentparent;
  }
  // 更新平衡因子为0
  parent->_bf = subL->_bf = 0;
}

🌞左单旋

新节点插入较高右子树的右侧—右右:

左单旋与单旋类似,所以我们直接来看代码

AVL树左单旋示例(C++):

void RotateL(Node* parent)
{
  Node* subR = parent->_right;
  Node* subRL = subR->_left;
  parent->_right = subRL;
  subR->_left = parent;
  Node* Parentparent = parent->_parent;
  parent->_parent = subR;
  if (subRL)
  {
    subRL->_parent = parent;
  }
  // 判断parent是不是根节点
  if (_root == parent)
  {
    _root = subR;
    subR->_parent = nullptr;
  }
  else
  {
    if (parent == Parentparent->_left)
    {
      Parentparent->_left = subR;
    }
    else
    {
      Parentparent->_right = subR;
    }
      subR->_parent = Parentparent;
  }
  parent->_bf = subR->_bf = 0;
}

🌙左右双旋

新节点插入较高左子树的右侧—左右:

这里是将双旋变成单旋后再旋转,先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新

这里单旋可以复用上面讲的

AVL树左右双旋示例(C++):

void RotateLR(Node* parent)
{
  Node* subL = parent->_left;
  Node* subLR = subL->_right;
  int bf = subLR->_bf; // 根据右子树的右孩子的平衡因子,判断旋转后的平衡因子情况
  // 复用单旋
  RotateL(parent->_left);
  RotateR(parent);
  // subLR就是新插入节点
  if (bf == 0)
  {
    parent->_bf = subL->_bf = subLR->_bf = 0;
  }
  else if (bf == 1)
  {
    subLR->_bf = 0;
    parent->_bf = 0;
    subL->_bf = -1;
  }
  else if (bf == -1)
  {
    subLR->_bf = 0;
    parent->_bf = 1;
    subL->_bf = 0;
  }
  else
  {
    assert(false);
  }
}

⭐右左双旋

新节点插入较高右子树的左侧—右左:

右左双旋和左右双旋类似,我们直接看代码

AVL树右左双旋示例(C++):

void RotateRL(Node* parent)
  {
  Node* subR = parent->_right;
  Node* subRL = subR->_left;
  int bf = subRL->_bf;
  
  // 复用单旋
  RotateR(parent->_right);
  RotateL(parent);
  if (bf == 0)
  {
    parent->_bf = subR->_bf = subRL->_bf = 0;
  }
  else if (bf == 1)
  {
    subRL->_bf = 0;
    parent->_bf = -1;
    subR->_bf = 0;
  }
  else if (bf == -1)
  {
    subRL->_bf = 0;
    parent->_bf = 0;
    subR->_bf = 1;
  }
  else
  {
    assert(false);
  }
}

总结:

假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑

  • parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
    当subR的平衡因子为1时,执行左单旋
    当subR的平衡因子为-1时,执行右左双旋
  • parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL
    当subL的平衡因子为-1是,执行右单旋
    当subL的平衡因子为1时,执行左右双旋

旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新


📝5. AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

验证其为二叉搜索树

如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

代码演示示例(C++):

// 中序遍历
void InOrder()
{
  _InOrder(_root);
  cout << endl;
}
void _InOrder(Node* root)
{
  if (root == nullptr)
  {
    return;
  }
  _InOrder(root->_left);
  cout << root->_kv.first << " ";
  _InOrder(root->_right);
}

验证其为平衡树

  • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
  • 节点的平衡因子是否计算正确

代码演示示例(C++):

bool IsBalance()
{
  return _IsBalance(_root);
}
int _Height(Node* root)
{
  if (root == nullptr)
  {
    return;
  }
  int rightHeight = _Height(root->_right);
  int leftHeight = _Height(root->_left);
  return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}
bool _IsBalance(Node* root)
{
  if (root == nullptr)
  {
    return true;
  }
  int rightHeight = _Height(root->_right);
  int leftHeight = _Height(root->_left);
  if (rightHeight - leftHeight != root->_bf)
  {
    cout << root->_kv.first << "平衡因子有误" << endl;
    return false;
  }
  return abs(rightHeight - leftHeight) < 2
    && _IsBalance(root->_left)
    && _IsBalance(root->_right);
}

验证用例

int main()
{
  // 这里会进行双旋
  int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
  AVLTree<int, int> t;
  
  for (auto e : a)
  {
    t.Insert(make_pair(e, e));
  }
  t.InOrder();
  cout << t.IsBalance() << endl;
  return 0;
}


📘6. AVL树的缺陷

缺陷 原因
插入操作复杂 为了保持树的平衡,每次插入或删除节点时,AVL树可能需要进行多次旋转操作。具体来说,插入一个节点可能需要单旋转或双旋转来重新平衡树结构,而删除节点后可能需要从被删除节点到根节点这条路径上所有节点的平衡,旋转的量级最坏情况下为O(logN)。这增加了操作的复杂性并可能影响性能。
维护成本高 由于AVL树要求每个节点的左右子树高度差不超过1,因此需要频繁地检查和调整树的结构。这种严格的平衡要求导致了相对较高的维护成本,特别是在频繁进行插入和删除操作的情况下。
空间开销较大 虽然AVL树在查找效率上具有优势,但由于其需要频繁地进行旋转操作以维持平衡,这可能导致额外的空间开销。尤其是在处理大量数据时,这种开销可能会更加明显。
不适用于所有场景 AVL树适用于查找操作远多于插入和删除操作的场景。如果在一个应用中插入和删除操作也非常频繁,那么AVL树可能不是最优选择,因为每次插入和删除都需要进行平衡调整,这会影响性能。

📖7. 总结

在深入探讨AVL树的旅程即将结束时,我们不禁为这种精妙的数据结构所折服。AVL树不仅以其高度的平衡性保证了高效的搜索、插入操作,而且它所蕴含的平衡维护机制也体现了计算机科学中的智慧与美

学习AVL树的过程,不仅是一次对数据结构知识的积累,更是一次对问题分析和解决能力的锻炼。我们学会了如何在插入和删除操作中通过旋转操作来保持树的平衡,这种动态调整的思想在软件开发中同样具有广泛的应用

AVL树的学习之旅虽然告一段落,但我们对数据结构和算法的探索永无止境。在未来的学习和工作中,我们将会遇到更多复杂的问题和挑战,但只要我们保持对知识的渴望和对问题的深入思考,就一定能够找到解决问题的钥匙

让我们以AVL树为起点,继续在数据结构和算法的海洋中遨游,不断挖掘计算机科学的奥秘,为未来的技术创新和进步贡献自己的力量。愿每一位学习者都能在求知的道路上不断前行,收获满满的智慧与快乐

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!

目录
相关文章
|
20天前
|
监控 算法 数据处理
基于 C++ 的 KD 树算法在监控局域网屏幕中的理论剖析与工程实践研究
本文探讨了KD树在局域网屏幕监控中的应用,通过C++实现其构建与查询功能,显著提升多维数据处理效率。KD树作为一种二叉空间划分结构,适用于屏幕图像特征匹配、异常画面检测及数据压缩传输优化等场景。相比传统方法,基于KD树的方案检索效率提升2-3个数量级,但高维数据退化和动态更新等问题仍需进一步研究。未来可通过融合其他数据结构、引入深度学习及开发增量式更新算法等方式优化性能。
55 17
|
13天前
|
Java C++
c++ 红黑树(自平衡二叉搜索树)(k,v型)
因为红黑树是一种特殊的AVL树(但少了平衡因子的存在),所以其结点的定义是在AVL树上加上新的成员变量,用于表示结点的颜色。RED,BLACK,//三叉链, _kv(kv){}首先我们在默认构造上,默认构造结点的颜色默认情况下为红色所以为什么构造结点时,默认将结点的颜色设置为红色?这是因为:当我们向红黑树插入结点时,若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质4,此时我们就需要对红黑树进行调整。
|
4月前
|
C++ 容器
c++中的二叉搜索树
c++中的二叉搜索树
|
5月前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
152 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
5月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
144 12
|
5月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
136 10
|
5月前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
192 2
|
6月前
|
机器学习/深度学习 人工智能 自然语言处理
C++构建 GAN 模型:生成器与判别器平衡训练的关键秘籍
生成对抗网络(GAN)是AI领域的明星,尤其在C++中构建时,平衡生成器与判别器的训练尤为关键。本文探讨了GAN的基本架构、训练原理及平衡训练的重要性,提出了包括合理初始化、精心设计损失函数、动态调整学习率、引入正则化技术和监测训练过程在内的五大策略,旨在确保GAN模型在C++环境下的高效、稳定训练,以生成高质量的结果,推动AI技术的发展。
207 10
|
7月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
155 2
|
4月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。

热门文章

最新文章

下一篇
oss创建bucket