【数据结构】AVL树——平衡二叉搜索树

简介: 【数据结构】AVL树——平衡二叉搜索树

AVL树概述

平衡树:左子树的高度等于右子树的高度

不平衡树:左子树的高度不等于等于右子树的高度

二叉搜索树很难是一颗平衡树。

对二叉树进行插入和删除的操作,或插入大量的数据不够随机,都会是使二叉搜索树不够平衡。

极端情况下,二叉树会退化成类似链表的结构,那么二叉搜索树查询数据的效率荡然无存。

在二叉树的基础上加入平衡的概念就是平衡二叉搜索树,也叫AVL树

AVL树也不是一颗绝对的平衡树,AVL树的平衡是相对的,它允许左子树和右子树的高度为 1 ,但不能超过 1

平衡是相对的很好理解,因为一个父亲节点最多只能有两个孩子节点,而数据又是一个一个插入的,所以一定会出现左子树和右子树高度差为 1 的情况。

B树可达到绝对平衡,因为B树是多叉结构——一个父亲节点有多个孩子节点

如果左子树和右子树的高度差为 2 就视为打破平衡

如果打破平衡,就需要通过旋转这一操作让左右子树的高度差小于等于 1 。

AVL树是保持一种相对平衡的状态,而不是绝对平衡。那么AVL树搜索数据的效率只能是接近

AVL树只是保证了搜索效率的下限,而不是提高了上限

平衡因子

平衡因子这一概念并不是AVL树所必备的——从代码实现的角度来说,如果不加入平衡因子的概念理解起来会比较抽象。

平衡因子:让每个节点存一个整型,该整形值的大小等于右子树的高度减左子树的高度

平衡因子等于 0 左右子树平衡

平衡因子等于 1左右子树相对平衡,右树偏高

平衡因子等于 -1左右子树相对平衡,左树树偏高

平衡因子等于 2 -2左右子树不平衡

平衡因子的更新:

插入父亲节点的右边平衡因子加加,插入父亲节点的右边平衡因子减减

父亲节点更新后的平衡因子等于 1 或 -1 ,需要不断往上(溯源)更新,直到父亲节点的平衡因子为 0 或 更新至整棵树的根节点就停止更新

如果父亲节点的平衡因子为 2 或 -2 时,需要对这棵子树旋转,旋转后更新平衡因子

示例

旋转情况分类

旋转分为:

左单旋 右单旋 左右双旋  右左双旋

左单旋

:新节点插入较高右子树的右侧

具象图:

抽象图:

那么左单旋是怎么旋的呢?核心步骤为:

设父亲节点为:fathernode 孩子节点为:cur

cur的左孩子成为fathernode的右孩子,

再让fathernode成为cur的左孩子。

如下示意图

右单旋

:新节点插入较高左子树的左侧

具象图:

抽象图:

那么右单旋是怎么旋的呢?核心步骤为:

设父亲节点为:fathernode 孩子节点为:cur

cur的右孩子成为fathernode的左孩子,

再让fathernode成为cur的右孩子

如下示意图:

左右双旋

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

左右双旋的核心步骤为:

设父亲节点为:fathernode

父亲的左孩子节点为:fathernodeL

父亲的左孩子节点的右孩子节点的为fathernodeLR

先让fathernodeL左单旋,再让fathernodeLR进行右单旋

这里小编直接上抽象图:

右左双旋

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

设父亲节点为:fathernode

父亲的 右孩子节点为:fathernodeR

父亲的右孩子节点的左孩子节点的为fathernodeRL

先对fathernodeR进行右单旋,再对fathernode进行左单旋。

示意图:

AVL树节点设计

AVL树的节点需要三个指针,分别指向左孩子节点,右孩子节点,父亲节点。指向父亲节点的指针是为了能溯源更新平衡因子。

需要一个整型存储平衡因子,平衡因子在构造函数的初始化列表中初始化为 0,因为新节点左右孩子都为空。

template <class K>
class AVLTreeNode
{
public:
 
  AVLTreeNode(const K& key) //构造函数
    :_key(key)
    , _left(nullptr)
    , _right(nullptr)
    , _FatherNode(nullptr)
    , _bf(0)
  {
 
  }
 
  K _key; //键值   
 
  AVLTreeNode<K>* _left;//左孩子
  AVLTreeNode<K>* _right;//右孩子
  AVLTreeNode<K>* _FatherNode;//父亲  
 
  int _bf;//平衡因子
 
};

AVL树设计

template <class K>
class AVLTree
{
  typedef AVLTreeNode<K> node; 
 
  node* _root;
 
public:
 
  AVLTree()  //构造函数,初始化为空树
    :_root(nullptr)
  {
 
  }
 
 
 
 
  bool Insert(const K& key)//插入元素
  {
//
    if (nullptr == _root) //是否是空树
    {
      _root = new node(key);  
      return true;
    }
//
    node* cur = _root;
    node* fathernode = nullptr;
 
    while (cur)  //查找插入的位置,如果树中已经有要插入的值,则插入失败,
    {
      if (cur->_key < key)
      {
        fathernode = cur;
        cur = cur->_right;
      }
      else if (cur->_key > key)
      {
        fathernode = cur;
        cur = cur->_left;
      }
      else
      {
        return false;
      }
 
    }
 
 
      cur = new node(key); //新插入节点 
 
      if (fathernode->_key < cur->_key) //判断新节点应该是左孩子还是右孩子
      {
        fathernode->_right = cur;
        cur->_FatherNode = fathernode;
 
      }
      else
      {
        fathernode->_left = cur;
        cur->_FatherNode = fathernode;
      }
      //
      
      while (fathernode)//更新平衡因子
      {
 
      if (cur == fathernode->_left)
      {
        fathernode->_bf--;
      }
      else  if (cur == fathernode->_right)
      {
        fathernode->_bf++;
      }
 
 
      //
      if (fathernode->_bf == 0)
      {
        // 更新结束
        break;
      }
 
      else if (fathernode->_bf == 1 || fathernode->_bf == -1)
      {
        // 继续往上更新
        cur = fathernode;
        fathernode = fathernode->_FatherNode;
      }
      else if (fathernode->_bf == 2 || fathernode->_bf == -2)
      {
        // 子树不平衡了,需要旋转
        if (fathernode->_bf == 2 && cur->_bf == 1)
        {
          RevolveLeft(fathernode);//左单旋
        }
        else if (fathernode->_bf == -2 && cur->_bf == -1)
        {
          RevolveRight(fathernode);//右单旋
        }
        else if (fathernode->_bf == 2 && cur->_bf == -1)
        {
          RevolveRightLeft(fathernode); //右左双旋   
          
        }
        else if (fathernode->_bf == -2 && cur->_bf == 1)
        {
          RevolveLeftRight(fathernode);//左右双旋
        }
        else
        {
          assert(false);   //平衡因子出问题了
        }
        
        break;
      }
    
 
  }
 
  return true;
  }
 
}

下面通过代码的细节来深入理解旋转

详解单旋

左单旋

完整代码如下

void RevolveLeft(node *& fathernode)//左单旋      
{
  node* cur = fathernode->_right; //父亲节点的右孩子
 
  fathernode->_right = cur->_left; //更改指向关系
 
  if (cur->_left != nullptr) //特殊情况
    cur->_left->_FatherNode = fathernode;//更改指向关系
 
  cur->_FatherNode = fathernode->_FatherNode;//更改指向关系
 
  if (fathernode->_FatherNode != nullptr) //为空是特殊情况,
  {
 
    if (fathernode->_FatherNode->_right == fathernode)
    {
      fathernode->_FatherNode->_right = cur;//更改指向关系
    }
    else
    {
      fathernode->_FatherNode->_left = cur;//更改指向关系
    }
 
  }
 
  cur->_left = fathernode;//更改指向关系
 
  fathernode->_FatherNode = cur;//更改指向关系
 
  fathernode->_bf = 0; //更新平衡因子
  cur->_bf = 0;
 
}

处理指向关系时,一定不要忘了更改父亲的指向关系

经过左单旋之后,父亲节点和右孩子节点的平衡因子都为 0 ,可参考上文图示。

特殊情况如下,如果不处理特殊情况,程序很容易崩溃

右单旋

void RevolveRight(node *& fathernode) //右单旋
{
  node* cur = fathernode->_left; //父亲节点的左节点
 
  fathernode->_left = cur->_right;//更新指向关系
 
  if (cur->_right != nullptr) //特殊情况
    cur->_right->_FatherNode = fathernode;//更新指向关系
 
  cur->_FatherNode = fathernode->_FatherNode;//更新指向关系
 
  if (fathernode->_FatherNode != nullptr)//特殊情况
  {
 
    if (fathernode->_FatherNode->_right == fathernode)
    {
      fathernode->_FatherNode->_right = cur;//更新指向关系
    }
    else
    {
      fathernode->_FatherNode->_left = cur;//更新指向关系
    }
 
  }
 
 
  cur->_right = fathernode;//更新指向关系
 
  fathernode->_FatherNode = cur;//更新指向关系
 
  fathernode->_bf = 0;//更新平衡因子
  cur->_bf = 0;
}

详解双旋

左右双旋

左右双旋只需复用左单旋和右单旋即可,但平衡因子的更新却比较麻烦

完整代码如下

  void RevolveLeftRight(node *& fathernode)//左右双旋    
  {
    node* fathernodeL = fathernode->_left; //父亲节点的左孩子节点
    node* fathernodeLR = fathernodeL->_right;//父亲节点的左孩子节点的右孩子节点
 
    int bf = fathernodeLR->_bf; //保存平衡因子,实际是为了判断是插入了fathernodeLR左边还是右边还是fathernodeLR本身插入
 
    RevolveLeft(fathernodeL);
    RevolveRight(fathernode);
 
//更新平衡因子
    if (bf == 0)
    {
      fathernode->_bf = 0;
      fathernodeL->_bf = 0;
      fathernodeLR->_bf = 0;
    }
    else if (bf == -1)
    {
      fathernode->_bf = 1;
      fathernodeL->_bf = 0;
      fathernodeLR->_bf = 0;
    }
    else if (bf == 1)
    {
      fathernodeL->_bf = -1;
      fathernode = 0;
      fathernodeLR = 0;
    }
    else
    {
      assert(false);
    }
 
 
  }

平衡因子情况如下

右左双旋

完整代码如下

  void RevolveRightLeft(node *& fathernode) //右左双旋 
  {
    node* fathernodeR = fathernode->_right; 
    node* fathernodeRL = fathernodeR->_left;
 
    int bf = fathernodeRL->_bf;
 
    RevolveRight(fathernodeR);
    RevolveLeft(fathernode);
    if (bf == 0)
    {
      fathernode->_bf = 0;
      fathernodeR->_bf = 0;
      fathernodeRL->_bf = 0;
    }
    else if (bf == 1)
    {
      fathernode->_bf = -1;
      fathernodeR->_bf = 0;
      fathernodeRL->_bf = 0;
    }
    else if (bf == -1)
    {
      fathernodeR->_bf = 1;
      fathernode->_bf = 0;
      fathernodeRL->_bf = 0;
    }
    else
    {
      assert(false); 
    }
  }

平衡因子情况如下

相关文章
|
3月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
99 3
 算法系列之数据结构-Huffman树
|
3月前
|
存储 自然语言处理 数据库
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
210 19
|
3月前
|
算法 Java
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
101 22
|
3月前
|
C语言 C++ 容器
【数据结构】二叉搜索树(二叉排序树)
本文介绍了二叉搜索树(Binary Search Tree, BST)的定义、实现及其性能分析。二叉搜索树是一种特殊的二叉树,其特点是左子树所有节点值小于根节点值,右子树所有节点值大于根节点值,且每个子树也满足此特性。文中详细讲解了BST的节点结构、插入、查找、删除等操作的实现,并通过C++代码展示了这些功能。此外,还讨论了BST的性能:在理想情况下,时间复杂度接近O(logN),但在最坏情况下可能退化为O(N)。为了提高效率,后续将学习自平衡二叉搜索树如AVL树和红黑树。掌握BST有助于理解STL中的set和map容器。感谢阅读,欢迎点赞支持。
229 9
|
5月前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
151 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
5月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
136 12
|
5月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
136 10
|
5月前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
188 2
|
7天前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
栈区的非法访问导致的死循环(x64)
|
7月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
170 58