【C++】AVL树的插入实现(详解旋转机制)

简介: 【C++】AVL树的插入实现(详解旋转机制)

AVL树的定义


一棵AVL树是其每个结点的平衡因子绝对值最多相差1的二叉查找树

平衡因子?这是什么东西,别急,继续往下看。

先给出对应平衡因子的计算,我们这里是根节点的右子树高度减去左子树高度。同时平衡因子的绝对值越大,说明树越不平衡。当平衡因子的绝对值大于1时,就需要进行旋转操作来恢复平衡


当然更好一点的定义是每个结点的左子树和右子树的高度差。如果用右子树的高度减去左子树的高度来计算平衡因子,那么需要注意平衡因子的符号和旋转方向会相反。这是因为,如果你按照定义来计算平衡因子,那么当平衡因子为正时,说明左子树比右子树高,此时需要进行右旋或左右双旋;当平衡因子为负时,说明右子树比左子树高,此时需要进行左旋或右左双旋。但是如果你用右子树的高度减去左子树的高度来计算平衡因子,那么当平衡因子为正时,说明右子树比左子树高,此时需要进行左旋或右左双旋;当平衡因子为负时,说明左子树比右子树高,此时需要进行右旋或左右双旋。也就是说,平衡因子的符号和旋转方向会相反。


根据定义这颗二叉排序树中有结点的平衡因子的绝对值超过1,就不是一颗AVL树。


4ab083738f9d4b4fa03d3d3dfba2fab5.png

例如上图:在插入90结点之前是一颗标准的AVL树,在插入90结点之后就不是了,我们查找一下最小不平衡二叉排序树,从距离90结点最近的结点开始,80结点平衡因子为1,70结点平衡因子为2,到这里就找到了,所以以50为根结点的子树就是最小不平衡二叉排序树。

明白了以上概念后我们就需要再了解一下左旋与右旋的概念了。AVL树的精华部分就是通过旋转来保持一颗二叉平衡搜索树👇👇👇


AVL树的旋转机制


首先初步认识一下AVL树的四种旋转:

AVL的四种旋转是指在插入或删除节点后,为了保持AVL树的平衡性,需要对树进行调整的操作。

AVL旋转的名称一般是根据新节点插入的位置和最先失衡的节点的位置来命名的。


这么说清楚些


左单旋(RR):当新节点插入到较高右子树的右侧时,需要对最先失去平衡的节点进行一次左旋,使其右子树成为新的根节点,原根节点成为其左子树。

右单旋(LL):当新节点插入到较高左子树的左侧时,需要对最先失去平衡的节点进行一次右旋,使其左子树成为新的根节点,原根节点成为其右子树。

左右双旋(LR):当新节点插入到较高左子树的右侧时,需要先对左子树进行一次左旋,再对最先失去平衡的节点进行一次右旋,使其左子树的右子树成为新的根节点。

右左双旋(RL):当新节点插入到较高右子树的左侧时,需要先对右子树进行一次右旋,再对最先失去平衡的节点进行一次左旋,使其右子树的左子树成为新的根节点。


然后:

旋转的原则:保持此棵树继续是二叉搜索树


旋转的目的:保持左右均衡,降低整棵树的高度


1.左旋操作 — 新节点插入较高右子树的右侧—右右:左单旋



85a3171f11c44a6f9d2f2df6fc8a1ab4.png


  1. 在较高右子树右侧插入节点后,节点30的平衡因子变为2,这时我们将30节点进行左旋,这样AV了树就重新达到平衡了

下面展示代码基本上都加上了注释,对比上面左旋流程仔细分析一下,这里需要注意一下,旋转完后结点的父节点都需要重置。

代码示例:左旋操作


void RotateL(Node* parent) // 左旋操作
{
  Node* subR = parent->_right; // subR 是 parent 的右子树根节点
  Node* subRL = subR->_left; // subRL 是 subR 的左子树根节点
  parent->_right = subRL; // 将 subRL 作为 parent 的右子树
  if (subRL)
    subRL->_parent = parent; // 如果 subRL 存在,更新其父节点为 parent
  Node* ppnode = parent->_parent; // ppnode 是 parent 的父节点
  subR->_left = parent; // 将 parent 作为 subR 的左子树
  parent->_parent = subR; // 更新 parent 的父节点为 subR
  if (ppnode == nullptr) // 如果 ppnode 不存在,说明 parent 是根节点
  {
    _root = subR; // 更新根节点为 subR
    _root->_parent = nullptr; // 根节点的父节点为空
  }
  else // 如果 ppnode 存在
  {
    if (ppnode->_left == parent) // 如果 parent 是 ppnode 的左子树
    {
      ppnode->_left = subR; // 将 subR 作为 ppnode 的左子树
    }
    else // 如果 parent 是 ppnode 的右子树
    {
      ppnode->_right = subR; // 将 subR 作为 ppnode 的右子树
    }
    subR->_parent = ppnode; // 更新 subR 的父节点为 ppnode
  }
  parent->_bf = subR->_bf = 0; // 更新 parent 和 subR 的平衡因子为 0
}

2.右旋操作 — 新节点插入较高左子树的左侧——左左:右单旋


53db204b989e41fb8d435ce801a9d308.png


  1. 新节点插入较高左子树的左侧时,节点60的平衡因子变为2,这里我们将节点60进行右旋,就变成右图的一颗AVL树了
void RotateR(Node* parent) // 右旋操作,和左旋操作对称,只需将左右互换即可
{
  Node* subL = parent->_left; // subL 是 parent 的左子树根节点
  Node* subLR = subL->_right; // subLR 是 subL 的右子树根节点
  parent->_left = subLR; // 将 subLR 作为 parent 的左子树
  if (subLR)
    subLR->_parent = parent; // 如果 subLR 存在,更新其父节点为 parent
  Node* ppnode = parent->_parent; // ppnode 是 parent 的父节点
  subL->_right = parent; // 将 parent 作为 subL 的右子树
  parent->_parent = subL; // 更新 parent 的父节点为 subL
  if (parent == _root) // 如果 parent 是根节点
  {
    _root = subL; // 更新根节点为 subL
    _root->_parent = nullptr; // 根节点的父节点为空
  }
  else // 如果 ppnode 存在
  {
    if (ppnode->_left == parent) // 如果 parent 是 ppnode 的左子树
    {
      ppnode->_left = subL; // 将 subL 作为 ppnode 的左子树
    }
    else // 如果 parent 是 ppnode 的右子树
    {
      ppnode->_right = subL; // 将 subL 作为 ppnode 的右子树
    }
        subL->_parent = ppnode; // 更新subL的父节点为ppnode;
    }
    subL->_bf = parent->_bf = 0;//更新subL和parent的平衡因子为0;
}


3.左右双旋 — 新节点插入较高左子树的右侧——左右:先左单旋再右单旋


上面就是左旋与右旋的操作,这部分搞明白之后理解双旋就容易了。


f9b89e6b9f594950b0060778c053b855.png



关于双旋的代码: 旋转的代码其实并不需要我们怎么写,我们直接复用左右单旋的代码即可,但在传参时要注意轴点的位置变化,拿右左双旋来举例,轴点先为subR后为parent。

请先看双旋操作中平衡因子的调节

void RotateLR(Node* parent)//LR型,先对parent的左子树进行左旋,再对parent进行右旋;
{
    Node*subL=parent->left;//subL是parent的左子树根节点;
    Node*subLR=subL->right;//subLR是subL的右子树根节点;
    int bf=subLR->bf;//记录subLR的平衡因子;
    RotateL(parent->left);//对parent的左子树进行左旋;
    RotateR(parent);//对parent进行右旋;
    if(bf==1)//如果subLR的平衡因子为1;
    {
        parent->bf=0;//更新parent的平衡因子为0;
        subLR->bf=0;//更新subLR的平衡因子为0;
        subL->bf=-1;//更新subL的平衡因子为-1;
    }
    else if(bf==-1)//如果subLR的平衡因子为-1;
    {
        parent->bf=1;//更新parent的平衡因子为1;
        subLR->bf=0;//更新subLR的平衡因子为0;
        subL->bf=0;//更新subL的平衡因子为0;
    }
    else if(bf==0)//如果subLR的平衡因子为0;
    {
        parent->bf=0;//更新parent的平衡因子为0;
        subLR->bf=0;//更新subLR的平衡因子为0;
        subL->bf=0;//更新subL的平衡因子为0;
    }
    else//其他情况不可能发生;
    {
        assert(false);
    }
}


4.右左双旋 — 新节点插入较高右子树的左侧——右左:先右单旋再左单旋



140c8d8d8c634693bc2d1ab953d21f28.png


void RotateRL(Node* parent)//RL型,先对parent的右子树进行右旋,再对parent进行左旋;
{
    Node*subR=parent->right;//subR是parent的右子树根节点;
    Node*subRL=subR->left;//subRL是subR的左子树根节点;
    int bf=subRL->bf;//记录subRL的平衡因子;
    RotateR(parent->right);//对parent的右子树进行右旋;
    RotateL(parent);//对parent进行左旋;
    if(bf==0)//如果subRL的平衡因子为0;
    {
        parent->bf=subR->bf=subRL->bf=0;//更新三个结点的平衡因子都为0;
    }
    else if(bf==1)//如果subRL的平衡因子为1;
    {
        parent->bf=-1;//更新parent的平衡因子为-1;
        subR->bf=subRL->bf=0;//更新subR和subRL的平衡因子都为0;
    }
    else if(bf==-1)//如果subRL的平衡因子为-1;
    {
        subR->bf=1;//更新subR的平衡因子为1;
        parent->bf=subRL->bf=0;//更新parent和subRL的平衡因子都为0;
    }
    else//其他情况不可能发生;
    {
        assert(false);
    }
}

5. 双旋操作中平衡因子的调节


关于双旋的代码实现,它主要的难点不是旋转,因为旋转我们直接复用单旋代码就可以处理了,真正的难点是在平衡因子的调节这块儿,他又分了三种情况


bc9ea629016645999d68114c949e146d.png


总结:

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


pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR

当pSubR的平衡因子为1时,执行左单旋

当pSubR的平衡因子为-1时,执行右左双旋

pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL

当pSubL的平衡因子为-1是,执行右单旋

当pSubL的平衡因子为1时,执行左右双旋

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


AVL树的插入实现


先给出AVL树的基本框架:

//AVL树的节点
template<class K, class V>//直接设置成KV模型,不搞K模型了
  class AVLTreeNode
  {
  public:
    AVLTreeNode(const std::pair<K, V>& p) :
      _data(p),
      _left(nullptr),
      _right(nullptr),
      _parent(nullptr),
      _bf(0)
    {}
    std::pair<K, V> _data;//K、V关系封装在一起
    int _bf;//平衡因子
    AVLTreeNode<K, V> *_left;//
    AVLTreeNode<K, V> *_parent;//用于记录当前节点的父节点
    AVLTreeNode<K, V> *_right;
  };
  template<class K, class V=bool>
  class AVLTree
  {
    typedef AVLTreeNode<K, V> Node;//节点
  public:
    AVLTree() :_root(nullptr)
    {}
  private:
  //在private方法这里定义旋转操作的方法
  Node* _root;
  };


步骤如下:


按照二叉搜索树的规则,从根节点开始比较,找到合适的位置插入新节点。

从插入节点向上回溯,更新每个节点的高度和平衡因子。

如果发现某个节点的平衡因子的绝对值大于1,说明该节点失去平衡,需要进行旋转操作。

根据失去平衡的节点和其子节点的平衡因子,判断旋转的类型(LL,RR,LR或RL)。

根据旋转的类型,进行相应的单旋或双旋操作,调整节点的位置和指针。

在旋转后,再次更新相关节点的高度和平衡因子。

返回新的根节点或子树的根节点。


整体参考代码:

bool insert(const std::pair<K, V>& data)
{
  Node* parent = nullptr;
  Node* cur = _root;
  if (_root == nullptr)//空树
  {
    _root = new Node(data);
    return true;
  }
  while (cur)
  {
    parent = cur;
    if (cur->_data.first == data.first)
      return false;
    else if (cur->_data.first > data.first)
      cur = cur->_left;
    else
      cur = cur->_right;
  }
  cur = new Node(data);
  cur->_parent = parent;//记住要维护父指针域
  if (parent->_data.first > cur->_data.first)
    parent->_left = cur;
  else
    parent->_right = cur;
  //插入一个节点过后,必定导致,cur这个节点的祖宗节点的平衡因子改变,我们需要跟新cur的父节点的平衡因子
  while (parent)
  {
  //cur是parent的右孩子,parent右孩子_bf++
    if (cur->_data.first > parent->_data.first)
      parent->_bf++;
    else
      parent->_bf--;
    //检查更新过后的parent的平衡因子,然后做出不同的反应
    if (parent->_bf == 0)
    {
      //更新过后parent的平衡因子为0,那么更新之前parent的平衡因子一定是 ±1 (可列方阵=程验证);
      //这说明,新插入的节点,是插在parent矮的一颗子树上的,parent这棵树的高度不变,无需在更新祖宗节点的平衡因子;
      //此次插入,非常成功
      break;
    }
    else if (abs(parent->_bf) == 1)
    {
      //更新过后parent的平衡因子是 ±1 那么说明更新之前parent的_bf一定是0
      //这说明在此次插入过后,parent这棵树的高度增加了,必须更新祖宗节点的_bf
      parent = parent->_parent;//parent、cur直接往上更新
    }
    else if (abs(parent->_bf) == 2)
    {
      //更新过后parent的平衡因子是 ±2 ,那么说明更新之前parent的_bf一定是±1;
      //这说明,此时parent这课树已经失衡了,需要旋转parent这课树
      //不平衡又有四种情况,这四种情况,对应不同的旋转方法
      //这里有个小细节,就是要先比较parent->_bf,再比较parent->_left/parent->_right,不能交换比较顺序,不然会出现错误
      if (parent->_bf == 2 && parent->_right->_bf == 1)//(RR型)
      {
        //parent左旋
        RotateL(parent);
      }
      else if (parent->_bf == -2 && parent->_left->_bf == -1)//(LL型)
      {
        //parent右旋
        RotateR(parent);
      }
      else if (parent->_bf == 2 && parent->_right->_bf == -1)//(RL型)
      {
        //先记录一下parent的右节点的左节点的_bf
        Node* SubR = parent->_right;
        Node* SubRL = SubR->_left;//放心这个节点一定存在,可证明
        int bf = SubRL->_bf;
        //1、先parent->_right右旋
        RotateR(parent->_right);
        //2、再parent左旋
        RotateL(parent);
        //这里我们需要重新更新一下平衡因子,因为单单的左旋、右旋只会将平衡因子置0,这是不正确的,需要我们手动调节
        if (bf == -1)
        {
          SubRL->_bf = 0;
          parent->_bf = 0;
          SubR->_bf = 1;
        }
        else if (bf == 1)
        {
          SubRL->_bf = 0;
          SubR->_bf = 0;
          parent->_bf = -1;
        }
        else if (bf == 0)
        {
          SubRL ->_bf= 0;
          SubR ->_bf= 0;
          parent->_bf = 0;
        }
        else
        {
          assert(false);
        }
      }
      else if (parent->_bf == -2 && parent->_left->_bf == 1)//(LR型)
      {
        Node* SubL = parent->_left;
        Node* SubLR = SubL->_right;//放心这个节点一定存在,可证明
        int bf = SubLR->_bf;
        //1、先parent->_left左旋
        RotateL(parent->_left);
        //2、再parent右旋
        RotateR(parent);
        //平衡因子调节
        if (bf == -1)
        {
          SubLR->_bf = 0;
          SubL->_bf = 0;
          parent->_bf = 1;
        }
        else if (bf == 1)
        {
          SubLR->_bf = 0;
          SubL->_bf = -1;
          parent->_bf = 0;
        }
        else if (bf == 0)
        {
          SubLR->_bf = 0;
          SubL->_bf = 0;
          parent->_bf = 0;
        }
        else
        {
          assert(false);
        }
      }
      else
      {
      //不用说了,插入之前的AVL树就已经出问题了
        assert(false);
      }
      //无论哪种旋转,旋转完毕过后,头结点平衡因子都为0了,
      //这课树已经平衡了,不需要在向上调整了
      break;
    }
    else
    {
//如果更新完过后parent->_bf等于1、-1、0、-2、2之外的数,不用说了,插入之前的AVL树就已经出问题了
      assert(false);
    }
  }
  return true;
}
void RotateR(Node* parent)
{
  Node* SubL = parent->_left;
  Node* SubLR = SubL->_right;
  Node* ppNode = parent->_parent;
  parent->_left = SubLR;
  if (SubLR)//SubLR节点存在
    SubLR->_parent = parent;
  SubL->_right = parent;
  parent->_parent = SubL;
  if (ppNode == nullptr)//parent是根节点
  {
    SubL->_parent = nullptr;
    _root = SubL;
  }
  else
  {
    SubL->_parent = ppNode;
    if (ppNode->_data.first > SubL->_data.first)
      ppNode->_left = SubL;
    else
      ppNode->_right = SubL;
  }
  SubL->_bf = 0;
  parent->_bf = 0;
}
void RotateL(Node* parent)
{
  Node* SubR = parent->_right;//这个节点一定存在,可以证明
  Node* SubRL = parent->_right->_left;//这个节点就不一定存在了
  Node* ppNode = parent->_parent;//提前记录一下parent的父亲
  //开始链接SubRL节点
  parent->_right = SubRL;
  if (SubRL)//只有当这个节点存在时,才需要维护器=其父亲节点
    SubRL->_parent = parent;
  //开始链接parent节点
  SubR->_left = parent;
  parent->_parent = SubR;
  //开始链接SubR节点
  if (ppNode == nullptr)//如果parent就是根,那么需要更新根节点
  {
    SubR->_parent = nullptr;
    _root = SubR;
  }
  else//parent不是根节点
  {
    SubR->_parent = ppNode;
    if (ppNode->_data.first > SubR->_data.first)
      ppNode->_left = SubR;
    else
      ppNode->_right = SubR;
  }
  //更新平衡因子
  SubR->_bf = 0;
  parent->_bf = 0;
}


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

2

(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

相关文章
|
4天前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
16 2
|
2月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树:它以苏联科学家Georgy Adelson-Velsky和Evgenii Landis的名字命名。
27 2
|
3月前
|
C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
34 5
|
4月前
|
C++
【C++】手撕AVL树(下)
【C++】手撕AVL树(下)
51 1
|
4月前
|
算法 测试技术 C++
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
35 2
|
4月前
|
Java C++ Python
【C++】手撕AVL树(上)
【C++】手撕AVL树(上)
52 0
|
5月前
|
C++
【c++】avl树
【c++】avl树
33 0
|
2天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
15 2
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
14天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
46 4