C++之AVL树(上)

简介: C++之AVL树(上)

前言

前面我们介绍了STL中的关联式容器map/set/multimap/mutiset等,我们可以发现它们的底层都是按照二叉搜索树来实现的,但是二叉搜索树自身有一些缺陷,当往二叉搜索树中插入的元素有序或者接近有序二叉搜索树就会退化为单支,其检索的时间复杂度就会退化为O(n)。因此map、set等关联式容器的底层结构是对搜索二叉树进行平衡处理的平衡二叉搜索树。

本节我们就来了解平衡搜索二叉树AVL树的相关概念。


一、概念

普通的搜索二叉树一旦数据有序或者接近有序就会造成二叉搜索树退化为单支,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

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

一棵AVL树要具有以下性质:

  1. 该树如果是空树,那么它是AVL树;
  2. 它的左右子树是AVL树;
  3. 左右子树的高度差(命名为平衡因子)的绝对值不超过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)。

二、AVL树结点的定义

template<class K,class V>
  struct AVLnode//三叉链
  {
    pair<K, V> _kv;
    AVLnode(const pair<K,V>& kv)
    :_kv(kv)
    , _bf(0)
    , _pleft(nullptr)
    , _pright(nullptr)
    , _pparent(nullptr)
    {}
    AVLnode<K, V>* _pleft;//左孩子
    AVLnode<K, V>* _pright;//右孩子
    AVLnode<K, V>* _pparent;//双亲结点
    int _bf;//平衡因子
  };

三、AVL树的插入

实际上,AVL树就是在二叉搜索树的基础上增加了平衡因子,因此AVL树的插入可以分为两步:

  1. 按照二叉搜索树的插入方式插入新结点
  2. 调整结点的平衡因子
bool insert(const pair<K,V>& kv)
    {
      //1.按照二叉搜索树的规则将节点插入到AVL树中
      node* newnode = new node(kv);
      node* cur = _root;
      if (_root == nullptr)//如果该树为空树,直接插入新节点,此时新节点是该树的根节点
      {
        _root = newnode;
        return true;
      }
      node* prev = nullptr;
      while (cur)
      {
        prev = cur;
        if (cur->_kv.first > data)
        {
          cur = cur->_pleft;
        }
        else if (cur->_kv.first < data)
        {
          cur = cur->_pright;
        }
        else return false;//树中已经存在该元素,插入失败
      }
      if (prev->_kv.first > data)
      {
        prev->_pleft = newnode;
        newnode->_pparent = prev;
      }
      else
      {
        prev->_pright = newnode;
        newnode->_pparent = prev;
      }
      node* pParent = prev;
      cur =newnode;
      //2.对结点的平衡因子进行更新,并检测新插入的结点是否破坏了AVL树的平衡性
      //调节平衡因子,在插入新结点前pparent的平衡因子有以下三种情况:-1, 0, 1
      //如果cur插在pparent的左边,给pparent的结点的平衡因子-1;
      //如果cur插在pparent的右边,给pparent的结点的平衡因子+1;
      //此时,pparent的平衡因子有以下三种情况,0,±1,±2
      //pparent平衡因子为0,说明新插入结点不影响该子树的高度,满足AVL树的性质,不再进行调整
      //pparent平衡因子为±1,说明插入前pparent的平衡因子为0,此时以pparent为根的子树的高度加1,需要继续向上更新平衡因子
      //pparent平衡因子为±2,说明插入前pparent的平衡因子为±1,此时pparent的平衡因子违反了AVL树的性质需要进行旋转操作
      while (pParent)//当pParent为空时,pParent就是根节点的父节点,就不需要再进行调整了
      {
        //更新父节点的平衡因子
        if (cur == pParent->_pleft)
        {
          pParent->_bf--;
        }
        else if (cur == pParent->_pright)
        {
          pParent->_bf++;
        }
        //检测更新后的平衡因子
        if (0 == pParent->_bf)//该子树的高度没变化,不用调整
        {
          break;
        }
        else if (1 == pParent->_bf || -1 == pParent->_bf)//该子树的高度增加了1,因此要继续向上调整平衡因子
        {
          cur = pParent;
          pParent = pParent->_pparent;
        }
        //3.破坏了AVL树的平衡性,我们就要对以pparent为根的子树就地旋转处理
        //旋转的目的:
        //1)让这棵子树的左右高度差不超过1
        //2)旋转过程中保持它是搜索树
        //3)更新旋转后的平衡因子
        //4)让这棵树的高度与插入前保持一致(不会继续影响上层,不用继续向上调整)
        else if (2 == pParent->_bf || -2 == pParent->_bf)
        {
          //右单旋
          //我的平衡因子为-1;父节点的平衡因子为-2.
          if (-2 == pParent->_bf && -1 == cur->_bf)
          {
            RotateR(pParent);
            //更新平衡因子
            cur->_bf = 0;
            pParent->_bf = 0;
          }
          //左单旋
          else if (2 == pParent->_bf && 1 == cur->_bf)
          {
            RotateL(pParent);
            //更新平衡因子
            cur->_bf = 0;
            pParent->_bf = 0;
          }
          //左右双旋
          else if (-2 == pParent->_bf && 1 == cur->_bf)
          {
            node* SubR = cur->_pright;
            RotateL(cur);
            RotateR(pParent);
            //更新平衡因子
            //新增结点就是SubR
            if (SubR ->_bf == 0)
            {
              cur->_bf = 0;
              pParent->_bf = 0;
            }
            //新增结点在SubR结点的左子树
            else if (SubR->_bf == -1)
            {
              SubR->_bf = cur->_bf = 0;
              pParent->_bf = 1;
            }
            //新增结点在SubR结点的右子树
            else if (SubR->_bf == 1)
            {
              SubR->_bf = pParent->_bf = 0;
              cur->_bf = -1;
            }
            else
            {
              assert(false);
            }
          }
          //右左双旋
          else if (2 == pParent->_bf && -1 == cur->_bf)
          {
            node* SubL = cur->_pleft;
            RotateR(cur);
            RotateL(pParent);
            //更新平衡因子
            //新增结点就是SubL
            if (SubL->_bf == 0)
            {
              cur->_bf = 0;
              pParent->_bf = 0;
            }
            //新增结点在SubL结点的左子树
            else if (SubL ->_bf == -1)
            {
              SubL->_bf = pParent->_bf = 0;
              cur->_bf = 1;
            }
            //新增结点在SubL结点的右子树
            else if (SubL->_bf == 1)
            {
              SubL->_bf = cur->_bf = 0;
              pParent->_bf = -1;
            }
            else
            {
              assert(false);
            }
          }
          return true;
        }
        else//如果以上的程序哪里出现问题就会直接报错
        {
          assert(false);
        }
      }
      return true;
    }
  private:
    AVLnode<T>* _root;
  };

四、AVL树的旋转

1.右单旋的情况以及具体操作

抽象图

先看如下的抽象图:

图中a,b,c是高度为h的子树,红色数字是插入前该节点的平衡因子。

新增结点导致要向上更新平衡因子,如果父节点的平衡因子为-2,且当前结点的平衡因子为-1时,我们就要进行右单旋。

那么如何进行右单旋呢?旋转处理之后平衡因子又是如何更新的呢?

要由具体的解决方法推出抽象的解决方法,因此下面我们具体分析当h分别为0/1/2时,我们的解决方法:

h = 0

如图,当h = 0时,在a处新增节点,按照图中步骤使用右单旋对该子树进行调整,最后更新平衡因子即可。

h = 1

如图,当h = 1时,在a结点的左右子结点任意一个位置新增节点,按照图中步骤使用右单旋对该子树进行调整,最后更新平衡因子即可。

h = 2

如图,当h = 2时,在a子树的i/j/m/n等四个位置的任意一个位置新增节点,按照图中步骤使用右单旋对该子树进行调整,最后更新平衡因子即可。

总结:根据以上三种情况我们可以得出,新增节点向上调整平衡因子的过程中,如果出现父节点的平衡因子为-2,当前结点的平衡因子为-1的情况,就以父节点为轴进行右单旋,之后更新父节点和当前结点的平衡因子为0即可。

代码实现

//左单旋(父节点平衡因子为2,右孩子平衡因子为1)
    void RotateL(node* parent)
    {
      node* SubR = parent->_pright;
      node* SubRL = SubR->_pleft;
      node* Grandpa = parent->_pparent;//祖父
      parent->_pright = SubRL;
      if (SubRL)
      {
        SubRL->_pparent = parent;
      }
      SubR->_pleft = parent;
      parent->_pparent = SubR;
      SubR->_pparent = Grandpa;
      //更新祖父的孩子结点
      if (Grandpa == nullptr)//pParent此时是根节点
      {
        _root = SubR;//更新cur为根节点
        _root ->_pparent = nullptr;
      }
      else
      {
        if (parent == Grandpa->_pleft)
        {
          Grandpa->_pleft = SubR;
        }
        else
        {
          Grandpa->_pright = SubR;
        }
      }
    }

2.左单旋的情况以及具体操作

抽象图

图中a,b,c是高度为h的子树,红色数字是插入前该节点的平衡因子。

新增结点导致要向上更新平衡因子,如果父节点的平衡因子为2,且当前结点的平衡因子为1时,我们就要进行左单旋。

左单旋与右单旋的方法类似,没有特殊情况,因此这里只介绍当h = 0时的情况:

当h = 0时,在c位置新增结点

可以看出,当父节点为2且当前结点为1时,需要以父节点为轴进行左单旋,最后更新平衡因子。

代码实现

//右单旋(父节点平衡因子为-2,左孩子平衡因子为-1)
    void RotateR(node* parent)
    {
      node* SubL = parent->_pleft;
      node* SubLR = SubL->_pright;
      node* Grandpa = parent->_pparent;//祖父
      parent->_pparent = SubL;
      SubL->_pright = parent;
      parent->_pleft = SubLR;
      if (SubLR)
        SubLR->_pparent = parent;
      SubL->_pparent = Grandpa;
      //更新祖父的孩子结点
      if (Grandpa == nullptr)//pParent此时是根节点
      {
        _root = SubL;
        SubL->_pparent = nullptr;
      }
      else
      {
        if (parent == Grandpa->_pleft)
        {
          Grandpa->_pleft = SubL;
        }
        else
        {
          Grandpa->_pright = SubL;
        }
      }
    }


相关文章
|
11天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
49 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
11天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
36 12
|
11天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
36 10
|
11天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
34 2
|
2月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
74 2
|
4月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树:它以苏联科学家Georgy Adelson-Velsky和Evgenii Landis的名字命名。
45 2
|
5月前
|
C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
55 5
|
6月前
|
C++
【C++】手撕AVL树(下)
【C++】手撕AVL树(下)
66 1
|
6月前
|
算法 测试技术 C++
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
46 2
|
6月前
|
Java C++ Python
【C++】手撕AVL树(上)
【C++】手撕AVL树(上)
67 0