AVL树【图示详解+代码实现】

本文详细介绍了AVL树的概念、节点定义以及插入、删除操作,重点讨论了四种旋转操作:右单旋、左单旋、左右双旋和右左双旋,同时分析了插入和删除时的平衡因子调整。此外,还涵盖了AVL树的性能优势和验证方法,展示了其实现代码。

前言:这篇文章会对AVL树这个较复杂的数据结构进行讲解,重点讲解了对AVL树的四种旋转操作,对于这四种旋转都做了非常详细的画图分析,并且对代码进行了实现,还有对于AVL树的验证代码及AVL树的性能分析也做了介绍.

🏞️1.AVL树的概念

在前面,我们学习过二叉搜索树,虽然二叉搜索树可以缩短查找效率,但如果数据有序或接近有序二叉搜索树将退化为单边树查找元素相当于在顺序表中查找,效率低下. 因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度**.

基于上述概念,我们可以得出:一棵AVL树,要么是空树,要么是具有以下性质的二叉搜索树:

  • 它的左右子树均为AVL
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1

image-20220820010045675

上述图中计算平衡因子时采用公式:平衡因子 = 右子树高度 - 左子树高度

如果一棵二叉搜索树是高度平衡的,它就是AVL树. 如果它有n个节点,其高度可保持在 O ( l o g 2 n ) O(log_2n) O(log2n),搜索时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n).

🌁2. AVL树节点的定义

AVL树节点定义:

template<class K, class V>
struct AVLTreeNode
{
   
   
	AVLTreeNode(const pair<K, V>& val = pair<K, V>())
		: _kv(val)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{
   
   }

	pair<K, V> _kv;
	int _bf;

	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;
};

在节点定义中,我们使用kv模型来存储值.

🌠3. AVL树的操作

📖3.1 AVL树的插入

注意:对于AVL树的插入,因为它是要结合AVL树的旋转的,所以在本文中,AVL树的插入和AVL树的旋转合起来才是完整的插入过程,所以这里的3.1 主要讲一下插入的大体的一个过程,具体插入的细节及代码实现都在3.2AVL树的旋转中.

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

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

对于平衡因子的调整,在插入之前,所有节点的平衡因子分为三种情况:0,1,-1插入后,新插入节点可能会使它的父节点的平衡因子发生变化,有这么三种情况:

1.新插入节点的父节点(parent)的平衡因子变成0(一定是由1或-1变成0)

image-20220822182847133

2.新插入节点的父节点(parent)的平衡因子变成-1/1(由0变成1或-1)

image-20220822184132920

3.新插入节点的父节点的平衡因子变成2/-2,此时已经违反了平衡树的性质,需要对其进行旋转处理.

📖3.2 AVL树的旋转

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

image-20220822163908653

对于图中的右单旋,它的规则如下:

image-20220822163537693

对于图中的a,b,c均为抽象节点,可能不太好理解,所以我们也可以将它们设置成实际的节点来进行分析,会更加直观:

对于,图中的抽象模型,为了方便分析,我们可以将它替换成实际节点来看:

image-20220822164207403

image-20220822164430590

当我们通过将h设置为不同的值时,实际的AVL树就会改变,通过画出h = 0h = 1的图我们就已经可以分析清楚这种旋转的情况了,对于h=2、3、4........,由于光是h=2时这棵树的样子就可能有36种情况,所以这里便不再一一画出.如果读者感兴趣,可以试着自己画一画,但根据以上h=0和h=1的情况我们就已经可以分析清楚了

最终,根据我们图上所画的这种右单选的情况,我们可以按照上图写出右旋转的代码:

void RotateR(Node* parent)
{
   
   
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
    {
   
   
        subLR->_parent = parent;
    }

    //有可能parent是一个节点的子节点
    Node* ppNode = parent->_parent;

    subL->_right = parent;
    parent->_parent = subL;

    if (ppNode)
    {
   
   
        //parent为ppNode的子节点
        if (parent == ppNode->_left)
        {
   
   
            ppNode->_left = subL;
        }
        else
        {
   
   
            ppNode->_right = subL;
        }

        subL->_parent = ppNode;
    }
    else
    {
   
   
        //parent为根结点,旋转后将subL作新的根节点
        _root = subL;
        subL->_parent == nullptr;
    }
	
    //调整平衡因子
    subL->_bf = parent->_bf = 0;
}

在写上述的代码时,我们有一个需要注意的地方,当我们发现当前节点的平衡因子发生错误,我们就需要将当前节点传入到RotateR右旋函数进行右单选,但是当前的节点也就是parent节点,它有可能是根结点,也可能是一个节点(在代码中我们用ppNode表示)的子节点,所以我们需要分情况讨论.

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

image-20220821232744003

左单旋的旋转规则如下:

image-20220822163736428

对于左单选的图示中,将抽象节点转换为实际节点进行分析在右单选中已经演示过,两者非常类似,所以这里不再花费篇幅去讲解.

我们根据左单选的旋转规则就可以写出它的代码:

void RotateL(Node* parent)
{
   
   
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    parent->_right = subRL;

    if (subRL)
    {
   
   
        subRL->_parent = parent;
    }

    Node* ppNode = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    if (ppNode)
    {
   
   
        if (parent == ppNode->_left)
        {
   
   
            ppNode->_left = subR;
        }
        else
        {
   
   
            ppNode->_right = subR;
        }
        subR->_parent = ppNode;
    }
    else
    {
   
   
        _root = subR;
        subR->_parent = nullptr;
    }

    subR->_bf = parent->_bf = 0;
}
  1. 新插入节点在较高左子树的右侧:先左单选再右单旋

image-20220821153659206

image-20220821153727771

对于这种左右双旋,它的旋转规则如下:

image-20220821154010907

对于规则中所述的左单旋右单旋,在文章上面均已讲解,可以参考上面.

在上图所画的节点均为抽象节点,对于这种左右双旋的情况,我们也可以将抽象节点代替成成实际节点来分析一下:

image-20220821154508507

h=0时:

image-20220821154533913

h=1时:在这里要注意,当h=1时,我们在插入新节点的时候,25的左子树和右子树均可以插入,所以就有两种情况我们先来看第一种:新节点插入在25的左子树

image-20220821155221805

新节点插入在25的右子树:

image-20220821155402383

我们分别分析了h=0、h=1时的情况,对左右双旋的这种情况进一步的加深理解,对于分析过程中,我们应该还会发现一个问题,那就是最终调整完之后的平衡因子调整问题:

我们发现,对上面的情况,每一棵树在插入新节点后,它们的subLR的平衡因子都各不相同,而且对应最终平衡因子需要调整的节点(parent、subL、subLR),它们调整后的值也是分为了三种情况的:

image-20220821161113936

所以,由此,我们可以总结出最终的平衡因子调整规则

  • subLR = 0时:调整为subLR = 0,subL = 0,parent = 0
  • subLR = -1时:调整为subLR = 0,subL = 0,parent = 1
  • subLR = 1时:调整为subLR = 0,subL = -1,parent = 0

对于左右双旋的旋转规则我们已经分析完成,接下来完成它的代码:

void RotateLR(Node* parent)
{
   
   
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(parent->_left);
    RotateR(parent);

    if (bf == 0)
    {
   
   
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == -1)
    {
   
   
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == 1)
    {
   
   
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    }
    else
    {
   
   
        //如果走到这里,说明在旋转之前就已经有错,直接断言
        assert(false);
    }
}
  1. 新节点插入到较高右子树的左侧:先右单旋再左单旋

同样的,对于右左双旋的这种情况,在插入新节点的时,也会有两个插入位置,所以也要分情况来看:

新节点插入在25的左边:

image-20220821230139502

image-20220821230347277

新节点插入在25的右边:

image-20220821230951992

image-20220821231033859

对于平衡因子的调整,上述讨论已经展现出了两种调整情况,但它和左右双旋一样,也是有三种的调整情况,所以我们需要再分析一下当h=0</

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉默.@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值