【C++】AVL树模拟实现

简介: 当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,达到高度平衡,即可降低树的高度,从而减少平均搜索长度。即如果一棵二叉搜索树的任意节点左右子树高度差绝对值都<=1,它就是AVL树。空树也算AVL树

一. 什么是AVL树?


当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,达到高度平衡,即可降低树的高度,从而减少平均搜索长度。即如果一棵二叉搜索树的任意节点左右子树高度差绝对值都<=1,它就是AVL树。

空树也算AVL树,AVL树一般具有一下性质:

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

bdaff968da244dbd930899f4b5bfef28.png


二. 为什么要有AVL树?


AVL树是在搜索树的基础上进行平衡优化后的结果。对一颗搜索树而言,如果数据有序或接近有序搜索树将退化为单支树,这时查询的时间复杂度将变为线性。二AVL树通过旋转操作使得树总是保持平衡,保证查询时高效的时间复杂度。类比AVL树的作用就像快排的三数取中一样解决了快排在数据有序或接近有序时的一趟遍历才能排好一个数的问题,一样是保证了效率。


三. AVL树的实现


1. 基本框架


AVL树主要包括节点类AVLNode和树的本体AVLTree,AVLNode主要存放存储的数据和指向自己亲属的指针,而AVLTree里面主要是实现AVL树的功能:包括插入节点、删除节点等等。

节点类框架

这里我们存储的数据类型是pair,所以要传两个模板参数k和v分别对应键值对中key和value的类型

template<class k,class v>    
struct AVLNode    
{    
  // 构造函数
  AVLNode(const pair<k,v>& kv)    
    :_parent(nullptr)    
    , _left(nullptr)    
    , _right(nullptr)    
    , _kv(kv)    
    , _bf(0)    
  {}    
  AVLNode<k,v>* _parent;// 指向父亲节点
  AVLNode<k,v>* _left;  // 指向左孩子
  AVLNode<k,v>* _right; // 指向右孩子
  pair<k,v> _kv;        // 数据域,即键值对
  int _bf;              // 平衡因子:右子树高度减左子树高低
};  

树的本体框架

成员变量只有一个就是根节点

template                                                               
class AVLTree                                                                            
{                                                                                        
  public:                                                                                
    typedef AVLNode Node;                                                           
  private:      
    Node* _root=nullptr;//根节点      
};

PS:该类只有一个成员变量,没必要特意写构造函数,可以直接在声明_root处赋值为nullptr。这是有必要的,不然会因为后面代码逻辑的原因而造成野指针的访问进而导致结果出问题。

比如我们在插入一个节点时,首先要判断该树是否为空,即_root是否等于nullptr,如果一开始我们没有给根节点初始化的话它就是个非空的随机值,

代码逻辑就会以为不是空树进而导致后面结果出错。

f5ae010eb4504b7fa2b5540b03e2f6fd.png


2. 插入节点


AVL树的插入大体分为两步:

  1. 按照搜索树的方式插入新节点
  2. 调整节点的平衡因子

插入成功返回true,失败返回false。传入的参数为键值对,调用时我们可以make_pair创造一个临时的键值对传入。


2.1 第一步:按搜索树的性质插入节点


分两种情况处理:

1.如果是空树的话,直接让_root指向一个动态开辟的节点,作为整棵树的跟节点,插入完成。

2.树不空,按搜索树性质寻找插入的位置并记录该位置的父亲,最后开辟节点作为父亲的孩子。

c741f53eda5348faae523ffd9d27670e.png

bool Insert(const pair<k,v>& kv)
{
  // 插入节点
  // 1、空树的话,就让插入的那个节点作为根
  if(!_root)
  {
    _root=new Node(kv);
    return true;
  }
  // 2、不是空树,就按照搜索树的性质找到插入的位置和它的父亲
  Node* cur=_root;
  Node* parent=nullptr;
  while(cur)
  {
    parent=cur;
    if(cur->_kv.first==kv.first)
    {
      return false;
    }
    else if(cur->_kv.first > kv.first)
    {
      cur=cur->_left;
    }
    else 
    {
      cur=cur->_right;
    }
  }
  // 创建要插入的节点
  Node* newNode=new Node(kv);
  // 更新关系,插入节点
  newNode->_parent=parent;
  if(parent->_kv.first < newNode->_kv.first)
  {
    parent->_right=newNode;
  }
  else 
  {
    parent->_left=newNode;
  }
  // 未完待续....
  return true;
}

补充说明

3a3e6678a7104ca48e0ed91c39ab688e.png

因为节点的数据是键值对所以我们传的参数是pair,为了节省空间我们传引用,传入后我们会依照传入的pair完成深拷贝,整个过程不涉及到修改原pair所以我们用const修饰。

这里用const修饰还有另外一个原因,我们在调用传参时通常用make_pair(),这里会生成一个临时的pair对象,临时变量具有常性,想要引用必须加const

下面我们看看这个临时的pair对象从调用时的传入到最后通过深拷贝生成另外一个pair对象所经历的整个过程:

7514cc303f9b47df97aa0fe6f521381d.png


2.2 穿插补充:树的旋转


旋转的前因后果

根据:平衡因子 = 右子树高度 - 左子树高度 我们可以得到以下结论:

  • 当插入一个节点后,该节点的平衡因子一定是0,如果该节点是父亲的左孩子的话父亲的平衡因子减一,右孩子的话父亲的平衡因子加一。
  • 调整后如果父亲的平衡因子为0,说明父亲的平衡因子原来是1或-1,那么以父亲为根的整棵树高度依然不变,只是把原来低的那边补上了,父亲往上的节点,它们的平衡因子不会受到任何影响。
  • 调整后如果父亲的平衡因子为1或-1,说明父亲原来的平衡因子为0,即原来左右子树的高度相同。现在如果为1是右子树高了,-1是左子树高了。虽然高了但是以父亲为根的整棵树依然平衡(因为左右子树高度差的绝对值并没有大于1),但是整棵树的高度确实是增加了,这时需要继续往上检查,看看祖父及其他祖先有没有平衡。
  • 调整后如果父亲的平衡因子为2或-2,此时已经不平衡了。我们以2为例,为2说明右边高了,那么我们再来看看右孩子的平衡因子(一定是1或-1,不可能是0,为0的话到右孩子哪里就调整结束了,不会在往上调整到它的父亲),如果为1,看孩子这棵树就是右边高了,合起来从父亲的角度来看就是右边的右边高了,该情况通过左单旋可以解决;反之如果为-1,对孩子而言是左边高了,父亲而言是右边的左边高了,这种情况要通过右左双旋来解决。旋转之后整棵树就平衡了且高度不变,这个时候就可以结束了,不用再往上调整。
  • 注意平衡因子不会出现3或-3的情况,因为出现2或-2时就已经通过旋转使其平衡了。

左单旋

插入后parent的平衡因子为2,说明右边高,右孩子的平衡因子为1也是右边高,需要对parent进行左单选,使左边高度增加,右边高度降低。设parent的右子树的根为subR,右子树的左子树的根为subRL

操作:把subRL作为parent的右孩子,parent作为subR的左孩子。

注意:subRL可能为空,parent可能为整棵树的根节点,这时它的父亲为空。

结果:不仅平衡了而且依然满足搜索树的性质。只有parent和subR这两个节点的连接关系发生改变,其他都没变。旋转完成后它们两个的平衡因子变为0

动画演示

c46bdd8b8e8142abb8d8f02916c25803.gif

图片演示

4d10cbfeebb644f2aaeb3bf22b98f377.png

代码实现

void RotateL(Node* parent)
{
  Node* subR=parent->_right;
  Node* subRL=subR->_left;
  // 更新节点之间的连接关系
  parent->_right=subRL;
  if(subRL)// subRL不为空才需要更新它的父亲
  {
    subRL->_parent=parent;
  }
  subR->_left=parent;
  Node* pparent=parent->_parent;
  parent->_parent=subR;
  if(!pparent)// parent为根时的处理
  {
    _root=subR;
    subR->_parent=nullptr;
  }
  else 
  {
    if(pparent->_left==parent)
    {
      pparent->_left=subR;
    }
    else 
    {
      pparent->_right=subR;
    }
    subR->_parent=pparent;
  }
  // 更新平衡因子
  parent->_bf=subR->_bf=0;
}

右单旋

parent的平衡因子为-2,说明parent的左子树高,左孩子的平衡因子为-1,也是左边高。设parent的左子树的根为subL,左子树的右子树的根为subLR

操作:把subLR给parent作为左孩子,然后parent作为subL的右孩子

注意:代码实现时要注意subLR可能为空,并且最后subL要连接上原来parent的父亲,当parent为根节点时它的父亲为nullptr

结果:不仅平衡了而且依然满足搜索树的性质。只有parent和subL的连接关系方式过改变,最终他们的平衡因子都为0

动画演示

ffe7e8de1ba74779a33e2e8416de3034.gif

图片演示

4f9e52d8d42a44c5800859a7383128b6.png

代码实现

// 右单旋
void RotateR(Node* parent)
{
  Node* subL=parent->_left;
  Node* subLR=subL->_right;
  // 更新节点之间的连接关系
  parent->_left=subLR;
  if(subLR)
  {
    subLR->_parent=parent;
  }
  subL->_right=parent;
  Node* pparent=parent->_parent;
  parent->_parent=subL;
  if(!pparent)
  {
    _root=subL;
    subL->_parent=nullptr;
  }
  else 
  {
    if(pparent->_left==parent)
    {
      pparent->_left=subL;
    }
    else if(pparent->_right==parent)
    {
      pparent->_right=subL;
    }
    subL->_parent=pparent;
  }
  // 更新平衡因子
  parent->_bf=subL->_bf=0;
}

左右双旋 和 右左双旋

两者都差不多,从最终结果看都是把subLR或subRL的两个子树分别分给它的父亲和爷爷,然后自己作为根节点。subLR或subRL的平衡因子决定了最后它父亲和爷爷的平衡因子的值,因为它要把自己的两个子树交给父亲和爷爷,它的平衡因子决定了它两个子树的高度,间接影响了它父亲和它爷爷的高度。

以左右双旋为例,它的情景包括三种,对应插入新节点后subLR的平衡因子可能为 -1、0、1这三种情况。

情况一:插入后subLR的平衡因子变为-1

  • 先左旋subL,把subLR的左孩子分给它父亲作为右孩子
  • 再右旋parent,把subLR的右孩子分给它爷爷作为左孩子
  • 最后subLR变为根,平衡因子为0;插入后subLR的平衡因子为-1,说明他的左子树高(节点插在了左子树),这个高的左子树后面作为它父亲的右子树所以它父亲的平衡因子也为0,而较矮的右子树作为了它爷爷的左孩子,所以最后爷爷的平衡因子变为1

91aa7cb469ac487780441c5417fb36cb.png

情景二:插入后subLR的平衡因子变为1

  • 先左旋subL,把subLR的左孩子作为它父亲的右孩子
  • 再右旋parent,把subLR的右孩子作为它爷爷的左孩子
  • 最后subLR变为根,平衡因子为0,由于插入后subLR的平衡因子为1,说明它的右子树高,这个高的右子树作为爷爷的左孩子使得爷爷的平衡因子也为0,较矮的左子树作为了父亲的右孩子,所以父亲的平衡因子为-1

c95b4b87e037463e9aa920f652b54770.png

情况三:subLR就是新插入的那个节点,它的平衡因子为0

  • 先左旋subL
  • 再右旋parent
  • 最后它们祖孙三个的平衡因子都为0

c052129bdbef4608886222bcbf9b8e52.png

PS:右左双旋的操作也大同小异,就不在演示了,只是平衡因子的处理结果相反:如果subRL的平衡因子为1,它爷爷的平衡因子为-1;如果subRL的平衡因子为-1,那么他父亲的平衡因子为1,;如果为0,那么它们祖孙三个也都是0

代码实现

注意:因为里面复用了单旋的接口,经过两次单旋后,祖孙三个节点的平衡因子都在被单旋操作里置为0了,所以最后我们只需特殊处理父亲或爷爷节点的平衡因子就行。

// 左右双旋
void RotateLR(Node* parent)
{
  Node* subL=parent->_left;
  Node* subLR=subL->_right;
  int flag=subLR->_bf;// 记录subLR的平衡因子,最后要依据它来更新其他节点的平衡因子
  // 依次旋转
  RotateL(subL);
  RotateR(parent);
  // 根据subLR平衡因子的值更新不同插入情况下的平衡因子
  if(flag==1)// 说明是在subLR的右子树插入的,那么subLR的左子树变为subL的右子树,subL平衡因子变为-1,subLR和parent的为0
  {
    subL->_bf==-1;
  }
  else if(flag==-1)// 说明是在subLR的左子树插入的,subLR的右子树最后会被分给parent作为左子树,parent的平衡因子变为-1,subL和subLR的平衡因子变为0
  {
    parent->_bf==1;
  }
}
// 右左双旋
void RotateRL(Node* parent)
{
  Node* subR=parent->_right;
  Node* subRL=subR->_left;
  int flag=subRL->_bf;
  // 依次旋转
  RotateR(subR);
  RotateL(parent);
  // 更新平衡因子
  if(flag==1)
  {
    parent->_bf==-1;
  }
  else if(flag==-1) 
  {
    subR->_bf==1;
  }
}

总结

当以parent为根的子树不平衡,也就是parent的平衡因子为2或者-2时,会根据不同的平衡原因来进行不同的旋转操作:

①:parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR,右子树的左子树的根为subRL

  • 当subR的平衡因子为1时,表示的是右子树的右边高,在parent结点进行左单旋操作
  • 当subR的平衡因子为-1时,表示右子树的左边高,此时需要进行右左双旋,也就是以subR结点先进行右单旋,再以parent结点进行左单旋。当subRL的平衡因子为1时,表示在subRL的右边插入新的结点,此时需要更新parent的平衡因子为-1,subR的平衡因子为0;当subRL的平衡因子为-1时,表示在subRL的左边插入新的结点,此时需要更新parent的平衡因子为0,subR的平衡因子为1;当subRL的平衡因子为0时表示 subRL 就是新插入的那个节点,旋转后祖孙三个的平衡因子都为0

②:parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL,左子树的右子树的根为subLR

  • 当subL的平衡因子为-1时,表示左子树的左边高,此时需要进行右单旋
  • 当subL的平衡因子为1时,表示左子树的右边高,此时需要进行左右双旋,也就是以subL结点先进行左单旋,再以parent结点进行右单旋。当subLR的平衡因子为1时,表示在subLR的右边插入新的结点,此时需要更新parent的平衡因子为0,subL的平衡因子为-1;当subLR的平衡因子为-1时,表示在subLR的左边插入新的结点,此时需要更新parent的平衡因子为1,subR的平衡因子为0;当subRL的平衡因子为0时表示 subRL 就是新插入的那个节点,旋转后祖孙三个的平衡因子都为0

2.2 第二步:更新节点的平衡因子

检查树是否平衡,对不平衡的树进行旋转处理,使其平衡并更新节点的平衡因子。下面的代码需要连接到第一步操作的代码的后面:



2.3 完整代码


为了方便浏览我们把前面的框架、插入和旋转的代码整合起来

// 树节点的定义
template<class k, class v>
struct AVLNode
{
  // 构造一个节点
  AVLNode(const pair<k,v>& kv)
    :_left(nullptr)
    ,_right(nullptr)
    ,_parent(nullptr)
    ,_kv(kv)
    ,_bf(0)
  {}
  AVLNode<k,v>* _left; 
  AVLNode<k,v>* _right; 
  AVLNode<k,v>* _parent;
  pair<k,v> _kv;
  int _bf;
};
// 树本体
template<class k, class v>
class AVLTree
{
  public:
    typedef AVLNode<k,v> Node;
    // 插入节点
    bool Insert(const pair<k,v>& kv)
    {
      // 空树的话,就让插入的那个节点作为根
      if(!_root)
      {
        _root=new Node(kv);
        return true;
      }
      // 不是空树,就按照搜索树的性质找到插入的位置和它的父亲
      Node* cur=_root;
      Node* parent=nullptr;
      while(cur)
      {
        parent=cur;
        if(cur->_kv.first==kv.first)
        {
          return false;
        }
        else if(cur->_kv.first > kv.first)
        {
          cur=cur->_left;
        }
        else 
        {
          cur=cur->_right;
        }
      }
      // 创建要插入的节点
      Node* newNode=new Node(kv);
      // 更新关系,插入节点
      newNode->_parent=parent;
      if(parent->_kv.first < newNode->_kv.first)
      {
        parent->_right=newNode;
      }
      else 
      {
        parent->_left=newNode;
      }
      cur=newNode;
      parent=cur->_parent;
      while(parent)
      {
        // 向上更新平衡因子
        if(cur==parent->_left)
        {
          --(parent->_bf);
        }
        else 
        {
          ++(parent->_bf);
        }
        // 检查是否需要调整
        // 0的话就平衡了
        // -1或1的话还要向上更新
        // -2或2的话需要旋转处理
        if(parent->_bf==0)// 平衡因子为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)
          {
            if(cur->_bf==1)// 右子树的右子树也高 -->  左单旋
            {
              RotateL(parent);
            }
            else if(cur->_bf==-1)// 右子树的左子树也高  -->  右左双旋
            {
              RotateRL(parent);
            }
          }
          else if(parent->_bf==-2)// 左子树高
          {
            if(cur->_bf==-1)// 左子树的左子树也高  -->  右单旋
            {
              RotateR(parent);
            }
            else if(cur->_bf==1)// 左子树的右子树也高  -->  左右双旋
            {
              RotateLR(parent);
            }
          }
          break;
        }
      }
      return true;
    }
  private:
    // 左单旋
    void RotateL(Node* parent)
    {
      Node* subR=parent->_right;
      Node* subRL=subR->_left;
      // 更新节点之间的连接关系
      parent->_right=subRL;
      if(subRL)// subRL不为空才需要更新它的父亲
      {
        subRL->_parent=parent;
      }
      subR->_left=parent;
      Node* pparent=parent->_parent;
      parent->_parent=subR;
      if(!pparent)// parent为根时的处理
      {
        _root=subR;
        subR->_parent=nullptr;
      }
      else 
      {
        if(pparent->_left==parent)
        {
          pparent->_left=subR;
        }
        else 
        {
          pparent->_right=subR;
        }
        subR->_parent=pparent;
      }
      // 更新平衡因子
      parent->_bf=subR->_bf=0;
    }
    // 右单旋
    void RotateR(Node* parent)
    {
      Node* subL=parent->_left;
      Node* subLR=subL->_right;
      // 更新节点之间的连接关系
      parent->_left=subLR;
      if(subLR)
      {
        subLR->_parent=parent;
      }
      subL->_right=parent;
      Node* pparent=parent->_parent;
      parent->_parent=subL;
      if(!pparent)
      {
        _root=subL;
        subL->_parent=nullptr;
      }
      else 
      {
        if(pparent->_left==parent)
        {
          pparent->_left=subL;
        }
        else if(pparent->_right==parent)
        {
          pparent->_right=subL;
        }
        subL->_parent=pparent;
      }
      // 更新平衡因子
      parent->_bf=subL->_bf=0;
    }
    // 左右双旋
    void RotateLR(Node* parent)
    {
      Node* subL=parent->_left;
      Node* subLR=subL->_right;
      int flag=subLR->_bf;// 记录subLR的平衡因子,最后要依据它来更新其他节点的平衡因子
      // 依次旋转
      RotateL(subL);
      RotateR(parent);
      // 根据subLR平衡因子的值更新不同插入情况下的平衡因子
      if(flag==1)// 说明是在subLR的右子树插入的,那么subLR的左子树变为subL的右子树,subL平衡因子变为-1,subLR和parent的为0
      {
        subL->_bf==-1;
      }
      else if(flag==-1)// 说明是在subLR的左子树插入的,subLR的右子树最后会被分给parent作为左子树,parent的平衡因子变为-1,subL和subLR的平衡因子变为0
      {
        parent->_bf==1;
      }
    }
    // 右左双旋
    void RotateRL(Node* parent)
    {
      Node* subR=parent->_right;
      Node* subRL=subR->_left;
      int flag=subRL->_bf;
      // 依次旋转
      RotateR(subR);
      RotateL(parent);
      // 更新平衡因子
      if(flag==1)
      {
        parent->_bf==-1;
      }
      else if(flag==-1) 
      {
        subR->_bf==1;
      }
    }
    Node* _root=nullptr;//根节点
};


相关文章
|
1天前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
12 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
|
4天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
24 5
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4