【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析}

这篇具有很好参考价值的文章主要介绍了【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析}。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

AVL树

一、AVL树的概念

  • 二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

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

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

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

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

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


二、AVL树的核心结构

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

template <class K, class V>
class AVLTree{
  typedef AVLTreeNode<K,V> Node;
  Node *_root = nullptr;
  
public:
  bool Insert(const pair<K,V> &kv); //插入并保持AVL树的平衡
  void Inorder(){ //中序遍历
    _Inorder(_root);
    cout << endl;
  }
  int Height(){ //返回二叉树的高度
    return _Height(_root);
  }
  bool Isbalance(){ //检查二叉树是否平衡
    return _Isbalance(_root);
  }
  
private:
  void RotateL(Node *parent); //左单旋
  void RotateR(Node *parent); //右单旋
  void RotateLR(Node *parent); //左右双旋
  void RotateRL(Node *parent); //右左双旋
  void _Inorder(Node *root);
  int _Height(Node *root);
  bool _Isbalance(Node *root);
};                                                                           

三、AVL树的插入

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

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
  3. 如果节点所在的二叉树不再平衡,通过旋转恢复平衡。

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

template <class K, class V>
bool AVLTree<K,V>::Insert(const pair<K,V> &kv)
{
  //1.按照二叉搜索树的方式插入新节点
  if(_root == nullptr)
  {
    _root = new Node(kv);
    return true;
  }

  Node *cur = _root;
  Node *parent = nullptr; //cur要向下一直遍历到null,所以要记录父节点的指针
  while(cur != nullptr)
  {
    if(kv.first > cur->_kv.first)                    
    {
      parent = cur;
      cur = cur->_right;
    }
    else if(kv.first < cur->_kv.first)
    {
      parent  = cur;
      cur = cur->_left;
    }
    else{
      return false;
    }
  }
  cur = new Node(kv);
  if(kv.first > parent->_kv.first)
  {                          
    parent->_right = cur;
  }
  else{
    parent->_left = cur;
  }
  cur->_parent = parent; //不要忘了修改父节点指针
    
  //2.调整节点的平衡因子
  while(parent!=nullptr) //只影响插入节点的所有祖先节点的平衡因子,parent不断向上一直遍历到根节点
  {
    //更新父节点的平衡因子
    //平衡因子=右树的高度-左树的高度
    if(cur == parent->_right)
    {
      ++parent->_bf; //插入右节点,bf++;      
    }
    else{
      --parent->_bf; //插入左节点,bf--;
    }
	//更新后检测双亲的平衡因子
    if(parent->_bf == 0)
    {
      //由1/-1更新为0,说明以父节点为根的二叉树高度不变,无需继续向上调整。 
      break; 
    }
    else if(abs(parent->_bf) == 1)
    {
      //由0更新为1/-1,说明以父节点为根的二叉树高度增加了一层,需要继续向上调整。 
      parent = parent->_parent;
      cur = cur->_parent;
    }
    else if(abs(parent->_bf) == 2)
    {
      //3.更新后为2/-2,说明parent所在的子树已经不平衡了,需要通过旋转恢复平衡。
      //......
      //下面的内容会有讲解↓↓↓
    }
    else{
      //除非代码有错,否则不可能有其他情况。
      assert(false);
    }
  }
  return true;
}

四、AVL树的旋转

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

4.1 左单旋

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

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

解释:

  1. 上图在插入前,AVL树是平衡的。a,b,c是高度为h的AVL子树(h>=0)。新节点插入到60的右子树c使c树增加了一层,最终导致以30为根的二叉树不平衡。
  2. 要让30平衡,就需要将30向左旋转,将60提上去。让30的左子树增加一层,右子树减少一层。也就是让30做60的左子树。
  3. 如果60有左子树b,b树的根一定大于30小于60,刚好做30的右子树。旋转完成后,更新节点的平衡因子即可。
  4. 在旋转过程中,有以下几种情况需要考虑:
    1. 60节点的左子树b可能存在,也可能为空。
    2. 30可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,要更新根节点指针_root。
    • 如果是子树,可能是某个节点的左子树,也可能是右子树。要更新父节点的指针。
template <class K, class V>
void AVLTree<K,V>::RotateL(Node *parent){ //parent对应30
  Node *subR = parent->_right; //subR对应60
  Node *subRL = subR->_left; //subRL对应b树的根
  Node *ppNode = parent->_parent; //记录30的父节点,便于旋转后进行连接。
  //30和b树进行连接
  parent->_right = subRL;         
  if(subRL != nullptr) //b树可能为空
  {
    subRL->_parent = parent;
  }
    
  //30和60重新连接
  subR->_left = parent;
  parent->_parent = subR;
  
  //60和30的父节点进行连接
  //如果30是根节点,更新根节点指针_root指向60
  //if(_root == parent)
  if(ppNode == nullptr)
  {
    _root = subR;
  }
  else{
    //60和30的父节点进行连接,先要确定30是父节点的左子树还是右子树
    if(ppNode->_left == parent)
    {
      ppNode->_left = subR;
    }
    else{                  
      ppNode->_right = subR;
    }
  }
  subR->_parent = ppNode;
  //更新平衡因子,进过旋转60和30的平衡因子变为0
  subR->_bf = parent->_bf = 0;
}

旋转的作用:1. 平衡二叉树 2. 降低二叉树高度(恢复到插入之前的高度h+2)


4.2 右单旋

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

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

详细解释参考左单旋。

template <class K, class V>
void AVLTree<K,V>::RotateR(Node *parent){
  Node *subL = parent->_left;
  Node *subLR = subL->_right;
  Node *ppNode = parent->_parent;

  subL->_right = parent; 
  parent->_parent = subL;
                                                                                                        
  parent->_left = subLR;
  if(subLR != nullptr)
  subLR->_parent = parent;

  //if(_root == parent)
  if(ppNode == nullptr)
  {
    _root = subL;
  }
  else{
    if(ppNode->_left == parent)
    {
      ppNode->_left = subL;
    }
    else{
      ppNode->_right = subL;
    }
  }
  subL->_parent = ppNode;
    
  subL->_bf = parent->_bf = 0;
}

旋转的作用:1. 平衡二叉树 2. 降低二叉树高度(恢复到插入之前的高度h+2)


4.3 左右双旋

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

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

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

左右双旋又能细分为3种情况:

  1. a,b,c,d是空树60是新增,引发双旋。
  2. 在b树插入新增,引发双旋。
  3. 在c树插入新增,引发双旋。

三种情况的双旋过程不变,只是平衡因子的更新需要分别处理:

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

双旋的关键在于更新平衡因子,30,60,90三个节点的平衡因子都在两次单旋过程中被错误的置为0(因为并没要满足单旋的条件)。要根据以上三种不同的情况重新调整三个节点的平衡因子。如何区分三种不同的情况?根据旋转之前60的平衡因子确认。

template <class K, class V>
void AVLTree<K,V>::RotateLR(Node *parent){ //parent对应90
  Node *subL = parent->_left; //subL对应30
  Node *subLR = subL->_right; //subLR对应60
  int bf = subLR->_bf; //记录旋转之前60的平衡因子
  RotateL(subL); //30左单旋
  RotateR(parent); //90右单旋
    
  //更新平衡因子
  subLR->_bf = 0; //60的平衡因子一定为0     
  switch(bf) //根据旋转之前60的平衡因子确认属于那种情况
  {
    case 1:
      subL->_bf = -1;
      parent->_bf = 0;
      break;
    case -1:
      subL->_bf = 0;
      parent->_bf = 1;
      break;
    case 0:
      subL->_bf = 0;
      parent->_bf = 0;
      break;
    default:
      //除非代码有错,否则不可能有其他情况。
      assert(false);
      break;
  }
}

左右双旋最终的结果是:

  1. subLR作为二叉树的根,subL作了subLR的左子树,parent作了subLR的右子树。
  2. subLR的左右子树分别作了subL和parent的右左子树。

4.4 右左双旋

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

【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析},数据结构和算法,c++,AVL树,数据结构,算法

详细解释参考左右双旋。

template <class K, class V>
void AVLTree<K,V>::RotateRL(Node *parent){
  Node *subR = parent->_right;
  Node *subRL = subR->_left;
  int bf = subRL->_bf;
  RotateR(subR);
  RotateL(parent);  
  //更新平衡因子                  
  subRL->_bf = 0;
  switch(bf)
  {
    case 1:
      subR->_bf = 0;
      parent->_bf = -1;
      break;
    case -1:
      subR->_bf = 1;
      parent->_bf = 0;
      break;
    case 0:
      subR->_bf = 0;
      parent->_bf = 0;
      break;
    default:
      //除非代码有错,否则不可能有其他情况。
      assert(false);
      break;
  }
}

右左双旋最终的结果是:

  1. 将subRL作为二叉树的根,parent作了subRL的左子树,subR作了subRL的右子树。
  2. subRL的左右子树分别作了parent和subR的右左子树。

4.5 分情况旋转

    else if(abs(parent->_bf) == 2)
    { 
	  //3.更新后为2/-2,说明parent所在的子树已经不平衡了,需要通过旋转恢复平衡。
      if(parent->_bf == 2 && cur->_bf == 1) //右右,左单旋
      {
        RotateL(parent);
      }
	  else if(parent->_bf == 2 && cur->_bf == -1) //右左,右左双旋
      {
        RotateRL(parent);
      }
      else if(parent->_bf == -2 && cur->_bf == -1) //左左,右单旋
      {
        RotateR(parent);                              
      }
      else if(parent->_bf == -2 && cur->_bf == 1) //左右,左右双旋
      {
        RotateLR(parent);
      }
      else
      {
        //除非代码有错,否则不可能有其他情况。
        assert(false);
      }
      break; //注意:旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。
    }

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

  1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR

    • 当subR的平衡因子为1时(右右),执行左单旋

    • 当subR的平衡因子为-1时(右左),执行右左双旋

  2. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL

    • 当subL的平衡因子为-1是(左左),执行右单旋

    • 当subL的平衡因子为1时(左右),执行左右双旋

经过旋转后可以直接break;因为经过旋转,插入元素前后子树的高度未发生变化都是h+2,不需要再调整上层节点的平衡因子。一次插入最多一次旋转。

所以,AVL树插入元素的时间复杂度:找插入位置O(log_2N) + 更新平衡因子O(log_2N) + 旋转O(1) = O(log_2N)。


五、AVL树的验证

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

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

  2. 验证其为平衡树

    • 每个节点子树高度差的绝对值不超过1
    • 节点的平衡因子是否计算正确
    template <class K, class V>
    bool AVLTree<K,V>::_Isbalance(Node *root){
      if(root == nullptr) return true; //注意,空树也是AVL树
      int lh = _Height(root->_left); //_Height返回二叉树的高度
      int rh = _Height(root->_right);
      int diff = rh-lh; //计算得到平衡因子                                                                                                              
      if(diff != root->_bf)
      {
        cout << "Key: " << root->_kv.first << "bf: " << root->_bf << " 平衡因子异常" << endl;
        return false;
      }
      return abs(diff) < 2 && _Isbalance(root->_left) && _Isbalance(root->_right);
    }
    
  3. 测试用例

    //插入一两组序列测试
    void Test1(){
      //int arr[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
      int arr[] = {1,2,3,4,5,6,7,8,9,10};
      AVLTree<int, int> avl;
      for(auto e : arr)
      {
        avl.Insert(make_pair(e, e));          
      }
      avl.Inorder();
      cout << "Isbalance: " << avl.Isbalance() << endl;
    }
    
    //插入10000个随机值测试
    void Test2(){
      srand(time(NULL));
      AVLTree<int, int> avl;
      const int N = 10000;
      for(int i = 0; i<N; ++i)
      {
        int x = rand();
        avl.Insert(make_pair(x, i));
      }
      cout << "Isbalance: " << avl.Isbalance() << endl;
    }
    

六、AVL树的删除(了解)

AVL树节点的删除步骤如下:

  1. 在AVL树中找到要删除的节点。
  2. 如果要删除的节点是叶子节点,直接删除即可。
  3. 如果要删除的节点只有一个子节点,先使前驱节点指向该节点的子节点,然后删除该节点。
  4. 如果要删除的节点有两个子节点,需要找到该节点的替换节点(即该节点右子树中最小的节点或左子树中最大的节点),然后交换与替换节点的值,最后删除替换节点。
  5. 在删除节点后,需要更新从该节点到根节点路径上所有节点的平衡因子,并进行平衡调整,使得整棵树重新满足AVL树的性质。

删除操作的平衡调整方法和AVL树的插入操作相似,但在实现时需要注意一些细节上的差异。需要注意的是,删除操作可能会导致多个节点的平衡因子发生变化,因此需要一直向上循环更新和平衡调整,直到根节点。具体实现大家可以参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。


七、AVL树的性能

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

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

  • 因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树。但一个结构经常修改,就不太适合。此时我们需要一种性能更优的数据结构——红黑树。文章来源地址https://www.toymoban.com/news/detail-687013.html

到了这里,关于【高阶数据结构】AVL树 {概念及实现;节点的定义;插入并调整平衡因子;旋转操作:左单旋,右单旋,左右双旋,右左双旋;AVL树的验证及性能分析}的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构(C++) : AVL树 实现篇

    目录 1.AVL树引入   (1)二叉搜索树缺点   (2)AVL树简介     [1]问题的解决     [2]AVL树的性质 2.AVL树的插入旋转操作   (1)术语解释   (2)左单旋     [1]插入到右侧的左边     [2]插入到右侧的右边   (3)右单旋     [1]插入到左侧的左边     [2]插入到左侧的右边   (4)左右双旋    

    2024年02月05日
    浏览(34)
  • [数据结构 C++] AVL树的模拟实现

    问题引入: 在上一篇文章中,我们提到了二叉搜索树在插入时,可能会形成单边树,会降低二叉搜索的性能。因此我们需要平衡二叉搜索树,降低二叉搜索树的高度,使得二叉搜索树趋于一颗完全二叉树的样子,这样就可以提高二叉搜索树的性能。本篇文章就来介绍一种平衡

    2024年02月03日
    浏览(52)
  • Java 数据结构篇-实现 AVL 树的核心方法

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍   文章目录         1.0 AVL 树的说明         2.0 AVL 树的成员变量及其构造方法         3.0 实现 AVL 树的核心方法         3.1 获取当前节点的高度 height(AVLNode node)         3.2 更新当前节点的高度 updateHe

    2024年01月23日
    浏览(47)
  • 【数据结构】链表C语言编写的,它定义了一个链表,并实现了一些基本的链表操作,如创建新节点、插入节点、清空链表、输出链表以及查找节点

    这段代码是用C语言编写的,它定义了一个链表,并实现了一些基本的链表操作,如创建新节点、插入节点、清空链表、输出链表以及查找节点。以下是每段代码的详细解释: 文件注释: 这段代码是一个文件注释 包含头文件: #include stdio.h  和  #include stdlib.h :这两个头文件

    2024年02月09日
    浏览(46)
  • 数据结构 栈的概念及栈的实现

    目录 1.栈的概念及结构 2.栈的实现  2.1  初始化栈 2.2 入栈  2.3 出栈  2.4 获取栈顶元素 2.5 获取栈中有效元素个数   2.6  检测栈是否为空,如果为空返回非零结果,如果不为空返回0 2.7 销毁栈  3. 完整代码 test.c  Stack.h Stack.c   栈(后进先出,先进后出) : 一种 特殊

    2024年01月21日
    浏览(49)
  • 数据结构:栈的概念及栈的实现

    目录 1.栈的概念及结构 2.栈的实现  2.1  初始化栈 2.2 入栈  2.3 出栈  2.4 获取栈顶元素 2.5 获取栈中有效元素个数   2.6  检测栈是否为空,如果为空返回非零结果,如果不为空返回0 2.7 销毁栈  3. 完整代码 test.c  Stack.h Stack.c   栈(后进先出,先进后出) : 一种 特殊

    2024年02月01日
    浏览(41)
  • 【数据结构】平衡二叉搜索树(AVL树)——AVL树的概念和介绍、AVL树的简单实现、AVL树的增删查改

      为什么要引入平衡二叉搜索树?   在之前我们学习了二叉搜索树,二叉搜索树的结构类似于一个倒置的树,而左子树的值小于根节点的值,右节点的值大于根节点的值,这种结构使得二叉搜索树在处理有序数据时非常高效。但是如果 在传入的数据为有序或接近有序,

    2024年02月07日
    浏览(49)
  • 数据结构入门(C语言版)二叉树的顺序结构及堆的概念及结构实现应用

    普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用 顺序结构的数组来存储 ,需要注意的是 这里的堆和操作系统虚拟进程地址空间中的堆是两回事 ,一个是 数据结构 ,一

    2023年04月19日
    浏览(53)
  • 一起学数据结构(3)——万字解析:链表的概念及单链表的实现

    上篇文章介绍了数据结构的一些基本概念,以及顺序表的概念和实现,本文来介绍链表的概念和单链表的实现,在此之前,首先来回顾以下顺序表的特点: 1. 顺序表是一组地址连续的存储单元依次存储的线性表的数据结构,逻辑上:顺序表中相邻的数据元素,其物理次序也是

    2024年02月13日
    浏览(54)
  • 【数据结构】手撕排序(排序的概念及意义、直接插入和希尔排序的实现及分析)

    目录 一、排序的概念及其运用  1.1排序的概念  1.2排序运用 1.3 常见的排序算法  二、插入排序 2.1基本思想:  2.2直接插入排序:  2.3步骤: 2.4直接插入排序的实现 三、希尔排序( 缩小增量排序 )  3.1希尔排序的发展历史 3.2 希尔排序的思路 ​编辑 gap = 3的思路讲解 3.3 如何

    2024年02月03日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包