【C++练级之路】【Lv.15】AVL树(双子旋转,领略绝对平衡之美)

简介: 【C++练级之路】【Lv.15】AVL树(双子旋转,领略绝对平衡之美)

引言

之前讲解了二叉搜索树,最优情况下它具有非常好的搜索性能,但是在极端场景下,它可能退化为单支树,可以形象地称为歪脖子树~

这样的话,它搜索的时间复杂度会从O(log2N)退化为O(N2),完全丧失了其优良的搜索性能。那么AVL树就可以登场了,它就是为解决这类问题而生的!

一、AVL树的概念

两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了AVL树,AVL树满足两条性质:

  • 它的左右子树都是AVL树
  • 任意结点的左右子树高度差的绝对值不超过1

这样通过控制子树高度差,让AVL树几乎完美接近于平衡,便不会出现单支树的情况,保证了优良的搜索性能,因此AVL树又称为高度平衡二叉搜索树

二、AVL树的模拟实现

2.1 结点

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)
  {}
};

细节:

  1. 使用三叉链,增加了指向parent的指针
  2. 使用KV模型,数据存储键值对pair
  3. 结点存储平衡因子,用来记录左右子树高度差

注:平衡因子计算高度差,是 右子树高度 - 左子树高度

2.2 成员变量

template<class K, class V>
class AVLTree
{
protected:
  typedef AVLTreeNode<K, V> Node;
public:
protected:
  Node* _root = nullptr;
};

2.3 插入

因为AVL树也是二叉搜索树,所以默认成员函数和遍历与之前写的没什么不同,这里重点讲解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->_right = cur;
  }
  else
  {
    parent->_left = 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)
    {
      //旋转
      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. 讨论平衡因子,以及调整结构

这里的重点在于如何讨论和调整平衡因子(bf)。

  1. 首先,插入cur结点,调整parent结点的bf,左减右加
  2. 讨论parent的bf
  • bf为0
  • bf为1或-1
  • bf为2或-2

bf为0时:

分析:此时没有增加高度,而是补上缺口,整棵树是平衡的,直接break即可


bf为1或-1时:

分析:此时增加了局部子树的高度,不确定有没有影响整体的高度差,所以要继续向上调整

parent = parent->_parent;
cur = cur->_parent;

bf为2或-2时:

此时bf已经超出平衡限制区间,需要进行结构调整,我们称之为旋转

2.4 旋转

旋转分为两大类:单旋和双旋。而单旋分为左单旋和右单旋,双旋分为左右旋和右左旋。

2.4.1 左单旋

场景:右部外侧插入

过程:

  1. parent接过subR的左子树subRL
  2. subR左边再链接parent

void RotateL(Node* parent)//左单旋
{
  Node* grandparent = parent->_parent;
  Node* subR = parent->_right;
  Node* subRL = subR->_left;

  parent->_right = subRL;
  if (subRL)
  {
    subRL->_parent = parent;
  }

  subR->_left = parent;
  parent->_parent = subR;

  if (grandparent)
  {
    if (grandparent->_right == parent)
    {
      grandparent->_right = subR;
    }
    else
    {
      grandparent->_left = subR;
    }
  }
  else
  {
    _root = subR;
  }
  subR->_parent = grandparent;

  parent->_bf = subR->_bf = 0;
}

细节:

  1. 大体是三步链接,注意双向链接
  2. 注意判空(subRL,grandparent)
  3. 如果判空,注意_root的传递
  4. 最后调整平衡因子_bf

2.4.2 右单旋

场景:左部外侧插入

过程:

  1. parent接过subL的右子树subLR
  2. subL右边再链接parent

void RotateR(Node* parent)//右单旋
{
  Node* grandparent = parent->_parent;
  Node* subL = parent->_left;
  Node* subLR = subL->_right;

  parent->_left = subLR;
  if (subLR)
  {
    subLR->_parent = parent;
  }

  subL->_right = parent;
  parent->_parent = subL;

  if (grandparent)
  {
    if (grandparent->_right == parent)
    {
      grandparent->_right = subL;
    }
    else
    {
      grandparent->_left = subL;
    }
  }
  else
  {
    _root = subL;
  }
  subL->_parent = grandparent;

  parent->_bf = subL->_bf = 0;
}

细节:同左单旋

2.4.3 左右旋

场景:左部内侧插入

过程:

  1. 先对subL进行左单旋
  2. 再对parent进行右单旋

void RotateLR(Node* parent)//左右旋
{
  Node* subL = parent->_left;
  Node* subLR = subL->_right;
  int bf = subLR->_bf;

  RotateL(subL);
  RotateR(parent);

  if (bf == 1)
  {
    subL->_bf = -1;
    subLR->_bf = 0;
    parent->_bf = 0;
  }
  else if (bf == -1)
  {
    subL->_bf = 0;
    subLR->_bf = 0;
    parent->_bf = 1;
  }
  else if (bf == 0)
  {
    subL->_bf = 0;
    subLR->_bf = 0;
    parent->_bf = 0;
  }
  else
  {
    assert(false);
  }
}

细节:

  1. 这里旋转直接复用前面单旋的代码
  2. 主要的重点,在于平衡因子bf的讨论
  • bf为1,在subLR的右侧插入
  • bf为-1,在subLR的左侧插入
  • bf为0,插入subLR(之前为空)

2.4.4 右左旋

场景:右部内侧插入

过程:

  1. 先对subR进行右单旋
  2. 再对parent进行左单旋

void RotateRL(Node* parent)//右左旋
{
  Node* subR = parent->_right;
  Node* subRL = subR->_left;
  int bf = subRL->_bf;

  RotateR(subR);
  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);
  }
}

细节:同左右旋


综上所述,旋转的目的保证平衡,同时降低树的高度

三、AVL树的验证

我们主要验证左右子树高度是否平衡,即高度差是否小于等于1

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

int Height(Node* root)
{
  if (root == nullptr)
  {
    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(rightH - leftH) <= 1
    && _IsBalance(root->_left)
    && _IsBalance(root->_right);
}

细节:

  1. 为了方便计算高度,写一个Height函数
  2. 在子函数递归中,计算高度差是否小于等于1
  3. 与此同时,还要检查平衡因子是否正常

四、AVL树的性能

4.1 优势

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)

4.2 缺陷

但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

4.3 适用场景

因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。


真诚点赞,手有余香


相关文章
|
3月前
|
算法 C语言 C++
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
80 0
|
1月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
81 11
|
1月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
79 2
|
2月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树:它以苏联科学家Georgy Adelson-Velsky和Evgenii Landis的名字命名。
27 2
|
3月前
|
C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
33 5
|
3月前
|
C++
C++代码来计算一个点围绕另一个点旋转45度后的坐标
C++代码来计算一个点围绕另一个点旋转45度后的坐标
77 0
|
4月前
|
C++
【C++】手撕AVL树(下)
【C++】手撕AVL树(下)
51 1
|
4月前
|
Java C++ Python
【C++】手撕AVL树(上)
【C++】手撕AVL树(上)
52 0
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4