【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(一)

简介: 【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解

前文

关于上篇二叉搜索树性能那块,探讨了二叉搜索树虽然可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度,按照这种规则形成的二叉搜索树为AVL树,保持着严格的平衡。

一、AVL树概念

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  • 平衡因子并不是必须,只是他的一种实现方式

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

平衡因子计算:

  • 平衡因子 = 右子树高度 - 左子树高度

二、AVL树实现

2.1 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;
        //主要类型
        AVLTreeNode(const pair<K, V> & kv)
            :_left(nullptr)
                ,_right(nullptr)
                ,_parent(nullptr)
                ,_kv(kv)
                ,_bf(0)
            {}
    };

AVL树每个结构存储一个pair对象,其中需要parent指针目的是为了方便访问当前节点的上一个节点。

考虑到AVLTreeNode书写麻烦,使用typedef进行类型重定义typedef AVLTreeNode Node;

2.2 AVL树查找逻辑

Node* Find(const K& key)
  {
    //按照搜索树
    Node* cur = _root;
    while (cur)
    {
      if (key > cur->_kv.first)
      {
        cur = cur->_right;
      }
      else if (key < cur->_kv.first)
      {
        cur = cur->_left;
      }
      else
      {
        //返回当前节点的指针
        return cur;
      }
    }
        //防止意外发生。
    return nullptr;
  }

本质上来说AVL树属于二叉搜索树,关于cur->_kv.first是cur通过指针去访问Node类中类型为pair对象的_kv。通过set与map简单学习。可以得知pair内部实现逻辑,_这里的kv.first中first是K类型的对象。同时AVL也是二叉树搜索树,查找逻辑当然是使用二叉搜索树的逻辑。

2.3 AVL树插入逻辑

既然AVL也是二叉搜索树,那么插入逻辑也是一样的,不同在于需要进行平衡因子的调正。(平衡因子 = 右子树高度 - 左子树高度)

当插入节点结束后,对插入节点的父亲节点进行平衡因子的修改。当对于父亲节点达到特定值会对应不同的情况,不断利用parent向上调整父亲节点的平衡因子。

通过例图去分析,当父亲平衡因子到达特定值对应不同情况:

第一张:当parent平衡因子从1或-1变到0

第二张:这里出现两种情况,parent平衡因子从0变到1或-1,parent平衡因子从1或-1变到2或-2。只有从0变到1或-1,没有从2或-2变成1或-1,因为当parent平衡因子绝对值超过1时,已经出现问题,需要进行调正

具体说明:

  1. 按照搜索树规则插入
  2. 某个父亲节点的平衡因子    
  • 插入父亲的左孩子,父亲平衡因子–
  • 插入父亲的右孩子,父亲平衡因子++
while (parent)
{
    //规矩平衡因子的规则,是根据特性和下面代码得来的
    if (parent->_left == cur)
    {
        parent->_bf--;
    }
    else if (parent->_right == cur)
    {
        parent->_bf++;
    }
    //判断是否回到父亲节点改动
    if (parent->_bf == 0)
    {
        //没有啥问题,可以跳出程序
        break;
    }
    else if (parent->_bf == 1 || parent->_bf == -1)
    {
        cur = parent;
        parent = parent->_parent;
    }
    //出现问题,这种情况是由情况二变化过来的,那么就是说,cur和parent向上移动了
    else if (parent->_bf == 2 || parent->_bf == -2
             {//.............}
}

理解:

在节点插入后,需要进行平衡因子的更新。更新顺序是从下到上,结合着父亲节点平衡因子进行判断是否需要继续向上更新。

根据判断更新父亲的平衡因子状况进行处理(该平衡因子更新完后):

  • 父亲平衡因子 == 0,父亲所在子树高度不变,不再继续往上更新,插入结束
  • 父亲平衡因子 == 1 or -1, 父亲所在子树高度变了,继续往上更新上面节点的平衡因子
  • 父亲平衡因子 == 2 or -2, 父亲所在的子树已经不平衡了,需要旋转处理

三、AVL的旋转(重点)

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

这里可以将前面两种归为单旋进行处理,而后面两种需要通过双旋进行处理。

提前说明:一个AVL树有多个AVL树或者说一个复杂的AVL树是由许多个简单的AVL树进行组合而成的。那么如果插入一个节点导致AVL树失去平衡,这里AVL树指以插入节点的父亲节点为根节点的子AVL树。遵守一个有问题解决问题,如果只是这一课AVL树有问题,就解决这颗AVL树,正常的AVL树我们是不要去过多的进行处理。

根据说明,如果将一整棵AVL树全部拆分进行研究,不同节点插入的位置很多选择,情况有很多种,难以全部罗列出来,而且没有必要,如果将一颗有问题AVL树治好,就可以将任何一颗AVL树治好,所以这边采用使用抽象图进行分析。

3.1 选用抽象图探讨AVL旋转

其中使用a、b、c子AVL树及其高度设置为h

抽象图中的抽象的AVL树平衡因子这里不用看的,目前罗列的情况是可以包含这里的,这里小部分就是大部分调正平衡因子的过程,意味着这部分也可以是抽象AVL树中的情况按照当前处理已经进行了调整。

3.2 单旋问题

使用单旋场景:parent->_bf == 2 && cur->_bf == 1或者parent->_bf == -2 && cur->_bf == -1

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

场景:parent->_ bf == -2 && cur->_bf == -1

具体解析旋转步骤:

目前AVL树是平衡的,当插入新节点30到左子树中,左子树高度加一层,导致以60为根的子AVL树失去平衡。为了使得平衡,意味着节点60的右子树也需要增加一层,那么将60节点成为30节点的左孩子,同时原本30节点的左孩子和60节点成为30节点的左孩子的高度相同,那么将原本30节点的左孩子成为60节点有孩子。这里保证了30节点原本左孩子一定是小于60节点的。更新节点的平衡因子即可,简单概况就是30 < b子树 < 60 < c子树

旋转过程中,有以下几点情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树    
  • 如果是根节点,旋转完成后,要更新根节点
  • 如果是子树,可能是某个节点的左子树,也可能是右子树
void RotateR(Node* parent)
{
    Node* SubL = parent->_left;
    Node* SubLR = SubL->_right;
    parent->_left = SubLR;
    if (SubLR)
        SubLR->_parent = parent;
    SubL->_right = parent;
    Node* ppNode = parent->_parent;
    parent->_parent = subL;
    if (parent == _root)
    {
        SubL = _root;
        SubL->_parent = nullptr;
    }
    else
    {
        if (ppNode->_left == parent)
        {
            ppNode->_left = SubL;
        }
        else if(ppNode->_right == parent)
        {
            ppNode->_right = SubL;
        }
        SubL->_parant = ppNode;
    }
    SubL->_bf = 0;
    parent->_bf = 0;
}

通过旋转使得不平衡AVL树得到调整,这里不需要考虑向上更新平衡因子。从抽象图中可以得出旋转后结论,直接使用结论修改平衡因子就行。

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

场景:parent->_ bf == 2 && cur->_bf == 1

这里和右单旋是差不多的,具体流程可以去看上述和代码分析

void RotateL(Node* parent)
{
    Node* SubR = parent->_right;
    Node* SubRL = SubR->_left;
    parent->_right = SubRL;
    if (SubRL)
        SubR->_parent = parent;
    SubR->_left = parent;
    Node* ppNode = parent->_parent;
    parent->_parent = subR;
    if (parent == _root)
    {
        SubR = _root;
        SubR->_parent = nullptr;
    }
    else
    {
        if (ppNode->_left == parent)
        {
            ppNode->_left = SubR;
        }
        else if (ppNode->_right == parent)
        {
            ppNode->_right = SubR;
        }
        SubR->_parant = ppNode;
    }
    SubR->_bf = 0;
    parent->_bf = 0;
}


【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(二)https://developer.aliyun.com/article/1617412

相关文章
|
21天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
56 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
21天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
40 12
|
21天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
39 10
|
22天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
41 2
|
1月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
92 5
|
2月前
|
机器学习/深度学习 存储 算法
数据结构实验之二叉树实验基础
本实验旨在掌握二叉树的基本特性和遍历算法,包括先序、中序、后序的递归与非递归遍历方法。通过编程实践,加深对二叉树结构的理解,学习如何计算二叉树的深度、叶子节点数等属性。实验内容涉及创建二叉树、实现各种遍历算法及求解特定节点数量。
119 4
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
306 9
|
2月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
48 1
|
21天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
134 77

热门文章

最新文章