AVL树(平衡二叉搜索树)

简介: nt _bf;, _bf(0), _kv(kv){}某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1或小于-1的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。


AVL的产生
二叉搜索树在一定程度上可以提高搜索效率,但是当序列是有序时:如果所示

此时二叉搜索树退化成单链表,搜索效率退化为O(N)。为了解决这个问题科学家引入了AVL树,又称平衡搜索二叉树。

AVL树的概念
AVL简称平衡二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。它具有如下几个性质:

可以是空树。
假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝值不超过 1。
AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(balanced binary search tree)。

现在尝试判断一下,下面那个树不是平衡二叉树

图一:

图二:

图三:

在平衡二叉树的基础上添加上二叉搜索树的性质就成为了AVL树,

即:左子树全部小于根,右子树全部大于根。

AVL树的节点定义
我们这里直接实现KV模型的AVL树。

AVL树与二叉搜索树与众不同之处就是多了旋转这一步骤,然而为了方便后序的旋转操作,将AVL树的结点定义为三叉链结构,(还存在平衡因子,后面解释作用与存在原因)

template<class k, class v>
struct AVLTreeNode
{
    //三叉链
    AVLTreeNode* _left;
    AVLTreeNode* _right;
    AVLTreeNode* _parent;

    //平衡因子
    int _bf;
    pair<k, v> _kv;

    AVLTreeNode(const pair<k, v>& kv)
        : _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _bf(0)
        , _kv(kv)
    {}
};

AVL树的插入
AVL树的插入也是最为麻烦的,就比如说一个AVL树在插入前符合要求,然而插入后破坏了平衡,但如果我们知道这时会破坏平衡还好,但是坏就坏在不知道,在插入前不知道是左子树深度高还是右子树深度高,这就是麻烦之处。

然而,前面已经提到了AVL树是一种特别的平衡二叉树,平衡二叉树里面就有个概念就是:平衡因子,解决了这一问题。

平衡因子
定义
某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1或小于-1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。

有了平衡因子这一说,那么在插入前我都知道是左子树深度高还是右子树深度高了,又因为AVL树又有二叉搜索树的性质,所以我在插入前就可以知道其要被插入的位置,所以说对于插入还是有了一点思路,知道了什么时候会失衡,失衡的大致情况是什么。

此时平衡就会被打破,那么如果想要调节,从而达到平衡,那么就要想办法使其左子树的深度减少,

对于此又会引入另一个新的概念:

最小失衡树
在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1或小于-1 的结点为根的子树称为最小不平衡子树。也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。

AVL树种解决失衡问题通过旋转最小失衡树来使整棵树达到平衡,旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。

对于此类的调节方法共可以分为四种旋转情况

插入的四种旋转方式

单旋
右旋

当插入0后,就会破坏破坏

此时树的平衡性被破坏此时从插入节点往上查到最小失衡树为3,3这个节点不平衡原因是它的左树的高度太高,因此我们只需要对3这个节点进行右旋。

下面的操作用抽象图代替

大致操作为如下:

在一个AVL树种插入一个值在a子树,从而使得平衡被破坏,使得60变为最小失衡树的根节点,我们令60为parent(下面为了简便用P代替)60的左子树为subPL(下面为了简便代替为PL),PL的右子树为subPLR(PLR)。

然后进行如图的操作。

图中的操作共可以分为3步走:

将b变为60的左
60变为30的右
30为新的根

代码如下:

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

parent->_left = subLR;
if (subLR)
{
    subLR->_parent = parent;
}
subL->_right = parent;
Node* parent_parent = parent->_parent;
parent->_parent = subL;
if (_root == parent)
{
    _root = subL;
    subL->_parent = nullptr;
}
else
{
    if (parent_parent->_left == parent)
    {
        parent_parent->_left = subL;
    }
    else
    {
        parent_parent->_right = subL;
    }
    subL->_parent = parent_parent;
}
parent->_bf = subL->_bf = 0;

}

左旋

同样我们用抽象图代替,在c子树下插入,从而破坏平衡,令30为P,30的右为PR,PR的左子树为PRL,大致操作可以分为3步骤:

b变为30的右
30变为60的左
60为新根

代码如下:

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

        parent->_right = subRL;
        subR->_left = parent;
        if (subRL)
        {
            subRL->_parent = parent;
        }
        Node* parent_parent = parent->_parent;
        parent->_parent = subR;
        if (_root == parent)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            if (parent_parent->_left == parent)
            {
                parent_parent->_left = subR;
            }
            else
            {
                parent_parent->_right = subR;
            }
            subR->_parent = parent_parent;
        }
        subR->_bf = parent->_bf = 0;
    }

双旋
在单旋我们的插入都是在图一的a(进行右旋),或者在图二的c(进行左旋)进行插入后破坏平衡,然后进行单旋从而恢复平衡,他的插入可以说是直线的插入,因此经过一次旋转就可以使其达到平衡。但是如果是折线过来又改怎么旋转呢。这时候就要用双旋了。

就比如说在图一的b插入(进行左右双旋),在图二的b插入(进行右左双旋)。

右左双旋

右左双旋就是在如图的b上插入节点

但是这个插入又分为两种,是在b的左子树与b的右子树部分插入,也就是分为在b1上插入,或在b2上插入,但实际上写代码的时候是不需要这样考虑的,写代码的时候还是一种情况,这里分两种情况是为了方便讲解详细操作步骤,从而使得过程更为好理解。

双旋重要的是要回更新平衡因子,两次的旋转还是很好解决的,下面我们以情况一来假设模拟一下。在双旋中,我们只需要先对PR进行右单旋,后对P进行左单旋便可以。总的来说还是很简单。

对于平衡因子的调整,我们就需要分情况了,这时候分为三种情况

subRL自己就是新增
插入的节点在b1上
插入的节点在b2上
这三种情况来说还是很好解决的

对于第一种就不多说了只需要将P,PR,PRL的平衡因子全等于0即可。

我们把图给出,对于此时

PRL的平衡因子为0,

P的为0

PR为1

我们把图给出,对于此时

PRL的平衡因子为0,

P的为-1

PR为0

最后代码展示:

    void RotateRL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        int bf = subRL->_bf;

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

        //调整平衡因子
        if (bf == 0)
        {
            // subRL自己就是新增
            parent->_bf = subR->_bf = subRL->_bf = 0;
        }
        else if (bf == -1)
        {
            //在b上增加
            parent->_bf = 0;
            subRL -> _bf = 0;
            subR->_bf = 1;
        }
        else if (bf == 1)
        {
            subR->_bf = subRL->_bf = 0;
            parent->_bf = -1;
        }
        else
        {
            assert(false);
        }
    }

左右双旋

同样左右双旋也是要分两种情况来定,最后调整平衡因子,这一部分代码,就留给你来写了,就不在一步一步来说明了

直接给出代码:

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

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

//调整平衡因子
if (bf == 0)
{
    // subLR自己就是新增
    parent->_bf = subL->_bf = subLR->_bf = 0;
}
else if (bf == -1)
{
    parent->_bf = 1;
    subL->_bf = subLR->_bf = 0;
}
else if (bf == 1)
{
    parent->_bf = subLR->_bf = 0;
    subL->_bf = -1;
}
else
{
    assert(false);
}

}

还有图解:

先对30左旋,后对40右旋。

旋转方式的选择
四种的旋转方式说完了,那么我们就要写代码,判断什么时候要进行那种旋转了,总的来说还是靠平衡因子来选择的。

注意:

插入后的调整要寻找最小失衡树!!!,而不是说插入后的那一小部分树来进行调整。

bool Insert(const pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node parent = nullptr;
Node
cur = _root;
while (cur)
{
if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}

cur = new Node(kv);
if (parent->_kv.first > kv.first)
{                
    parent->_left = cur;
    cur->_parent = parent;
}
else
{                
    parent->_right = cur;
    cur->_parent = parent;
}
//调整
while (parent)
{
    if (parent->_left == cur)
    {
        parent->_bf--;
    }
    else
    {
        parent->_bf++;
    }
    if (parent->_bf == 0)
    {
        break;
    }
    else if (parent->_bf == -1 || parent->_bf == 1)
    {
        cur = parent;
        parent = parent->_parent;
    }
    else if (parent->_bf == -2 || parent->_bf == 2)
    {
        if (parent->_bf == 2 && cur->_bf == 1)
        {
            RotateL(parent);
        }
        else if (parent->_bf == -2 && cur->_bf == -1)
        {
            RotateR(parent);
        }
        else if (parent->_bf == 2 && cur->_bf == -1)
        {
            RotateRL(parent);
        }
        else if (parent->_bf == -2 && cur->_bf == 1)
        {
            RotateLR(parent);
        }
        // 1、旋转让这颗子树平衡了
        // 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,
        // 所以对上一层没有影响,不用继续更新
        break;
    }
    else
    {
        break;
    }
}
return true;

}

AVL树的删除
AVL的删除是比插入多了平衡调整,比较麻烦,真正实现起来要200-300行的代码,总的来说是插入的代码量的二倍左右,我也不会,这就不多说什么了,但推荐一个文章,里面就有删除:

【数据结构】AVL树的删除(解析有点东西哦)_avl树删除节点-CSDN博客

其余函数
find与中序遍历打印
总的来说AVL树到此就结束了,剩下的就是find函数与中序遍历函数,这两个函数与二叉搜索树的实现完完全全相同

void InOrder()
{
_InOrder(_root);
cout << endl;
}

void _InOrder(Node* root)
{
if (root == nullptr)
return;

_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);

}
Node* findLastIndex(K key)
{

    Node* pre = _root;//记录前一个节点
    Node* cur = _root;
    while (cur) {
        pre = cur;
        if (cur->_kv.first == key) {
            break;
        }
        else if (cur->_kv.first > key) {
            cur = cur->_left;
        }
        else {
            cur = cur->_right;
        }
    }
    return pre;

}

检测是否为AVL树
当然还有检查目前的AVL树是不是符合要求

    int _Height(Node* root)
    {
        if (root == nullptr)
            return 0;

        int leftHeight = _Height(root->_left);
        int rightHeight = _Height(root->_right);

        return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
    }

    bool _IsBalance(Node* root)
    {
        if (root == nullptr)
        {
            return true;
        }

        int leftHeight = _Height(root->_left);
        int rightHeight = _Height(root->_right);
        if (rightHeight - leftHeight != root->_bf)
        {
            cout << root->_kv.first << "平衡因子异常" << endl;
            return false;
        }
        return abs(rightHeight - leftHeight) < 2
            && _IsBalance(root->_left)
            && _IsBalance(root->_right);
    }

到这就完了,拜拜

目录
相关文章
|
监控 安全 测试技术
研发中如何保证产品质量的稳定性
研发中如何保证产品质量的稳定性
|
弹性计算 安全 Linux
SSL-VPN和客户端配置|学习笔记
快速学习SSL-VPN和客户端配置
SSL-VPN和客户端配置|学习笔记
|
4月前
|
JSON 自然语言处理 Kubernetes
MindIE PD分离部署Q&A
使用mindie进行PD分离部署
137 28
|
8月前
|
机器学习/深度学习 人工智能 自然语言处理
《多语言+多文化,自然语言处理的全球通关秘籍》
在全球化背景下,信息快速流动,多语言交流频繁。自然语言处理(NLP)面临语法、词汇、语义差异及数据获取标注等挑战。为应对这些难题,多语言预训练模型(如XLM-RoBERTa)、迁移学习与零样本学习、融合多模态信息等技术应运而生,提升跨语言处理能力。同时,文化适应至关重要,需融入文化背景知识,确保准确传达含义,增强跨文化交流效果。NLP正逐步成为跨越语言与文化鸿沟的桥梁,促进全球信息交流与合作。
1228 17
|
安全 Java 数据安全/隐私保护
深入理解java中Unsafe类及其实现原理
深入理解java中Unsafe类及其实现原理
216 0
|
存储 JSON 安全
Docker 的 overlay2 扩容教程
Docker 的 overlay2 扩容教程
928 4
|
缓存 JavaScript 测试技术
如何创建一个VUE3项目并使用Element UI插件
如何创建一个VUE3项目并使用Element UI插件
220 1
|
Linux
如何检查CentOS版本:5种方法
这个文件包含了CentOS的详细版本信息,包括版本号、架构等。
3046 0
|
弹性计算
阿里云服务器多少钱一年?2024年5月云服务器价格表曝光!
2024年5月,阿里云服务器价格曝光,ECS云服务器2核2G3M带宽低至99元/年,2核4G5M优惠价199元/年。香港轻量服务器24元/月,4核8G服务器700元/年。其他配置如8核32G也有不同优惠。详细价格表及活动信息见阿里云服务器ECS页面
501 6