详解AVL树

文章介绍了AVL树的概念,作为解决二叉搜索树退化问题的平衡树,强调了平衡因子的重要性。详细阐述了插入操作的步骤,包括如何找到插入位置、更新平衡因子以及可能需要的旋转操作。同时,讨论了不同旋转类型(左旋、右旋、左右旋、右左旋)及其适用场景,以保持树的平衡。
摘要由CSDN通过智能技术生成

代码汇总:

https://gitee.com/maka-loveacm/success-will-come-when-conditions-are-ripe.c/blob/master/Today-is/2023_02_11AVL/AVL.h

概念:

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

为什么是不超过1呢?:

因为对于4个节点这类的 不可能做到左右子树的高度绝对值相差为0 不超过1 也可以理解成一种退而求其次

什么是二叉搜索树:若它的左子树不空,则左子树上所有结点的值均小于它的 根结点 的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值

性质:

它的左右子树都是AVL树

左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

手撕AVL:

(补充)对pair结构的认识

概念:

pair是将2个数据组合成一组数据,如stl中的map就是将key和value放在一起来保存。 pair的实现是一个结构体,主要的两个成员变量是first second ,因为是使用struct不是class,所以可以直接使用pair的成员变量。

类模板:template<class T1,class T2> struct pair

参数:T1是第一个值的数据类型,T2是第二个值的数据类型。

功能:

pair将一对值(T1和T2)组合成一个值,

这一对值可以具有不同的数据类型(T1和T2),

两个值可以分别用pair的两个公有函数first和second访问。

构造函数:(make_pair在插入中很常用)

pair<T1, T2> p1;

//创建一个空的pair对象(使用默认构造),它的两个元素分别是T1和T2类型,采用值初始化。

pair<T1, T2> p1(v1, v2);

//创建一个pair,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2。

make_pair(v1, v2);

// 以v1和v2的值创建一个新的pair对象,其元素类型分别是v1和v2的类型。

p1 < p2;

// 两个pair对象间的小于运算,其定义遵循字典次序:如 p1.first < p2.first 或者 !(p2.first < p1.first) && (p1.second < p2.second) 则返回true。

p1 == p2;

// 如果两个对象的first和second依次相等,则这两个对象相等;该运算使用元素的==操作符。

p1.first; // 返回对象p1中名为first的公有数据成员

p1.second; // 返回对象p1中名为second的公有数据成员

创建和初始化:

必须提供俩个可同可不同的类型名字

pair<string, string> anon; // 创建一个空对象anon,两个元素类型都是string

pair<string, int> word_count; // 创建一个空对象 word_count, 两个元素类型分别是string和int类型

pair<string, vector<int> > line; // 创建一个空对象line,两个元素类型分别是string和vector类型

pair<string, string> author("James","Joy"); // 创建一个author对象,两个元素类型分别为string类型,并默认初始值为James和Joy。

pair<string, int> name_age("Tom", 18);

pair<string, int> name_age2(name_age); // 拷贝构造初始化

pair对象的操作:

可以直接用.first .second对pair的成员进行访问

AVL节点的初始化:(三叉链+平衡因子)

什么是平衡因子? balance factor

节点右子树高度-左子树高度

1.第一个struct 写出左指针 右指针 平衡因子 构造函数(不写的话 将来插入new会出问题)

2.第二个struct 对AVL的节点重命名成Node

3.在第二个struct的public中增添函数

#pragma once
#include<assert.h>
#include<time.h>

template<class K,class V>
struct AVLTreeNode
{
    pair<K, V>_kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf;// balance factor

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

template<class K,class V>
struct AVLTree
{
    typedef AVLTreeNode<K, V>Node;
private:
    Node* _root = nullptr;
};

AVL树的插入bool Insert(const pair<K, V>& kv):

思路:

1.当树为空的时候 直接new插入即可

//空树
        if (root == nullptr)
        {
            _root = new Node(kv);
            return true;
        }

2.用俩个指针 parent指向nullptr cur指向_root

parent指针存在的意义:令parent和cur产生迭代(parent是cur的跟屁虫)

当节点找到他应该插入的位置的时候 如果没有parent 一个cur只是一个单纯的形参变量 和这个结构产生不了联系

//准备迭代建立关系
        Node* parent = nullptr;
        Node* cur = _root;

3.while(cur) 寻找满足插入结点的位置 同时if语句中parent与cur不断迭代更新

//寻找在哪里插入
        while (cur)
        {
            if (cur->_kv.first < kv.first)//节点值小于插入值 大的往右走说明这里的不够满足胃口
            {
                parent = cur;
                cur = cur->_right;
            }
            else if (cur->_kv.first > kv.first)
            {
                parent = cur;
                cur = cur->_left;
            }
            else
            {
                return false;//值相等 不要 避免冗余
            }
        }

        cur = new Node(kv);//嘎嘎插入

4.已经插入节点 判断parent的key键值大小 从而判断cur插入左侧还是右侧

二叉树搜索树:

AVL树:

//建立联系
        if (parent->_lv.first < kv.first)
        {
            parent->_right = cur;
            cur->_parent = parent;//如果是普通的二叉搜索树没有这句
        }
        else
        {
            parent->_left = cur;
            cur->_parent = parent;
        }

5.更新平衡因子 最坏的情况就是一直向上更新到了parent 所以是while(parent)

由于插入了cur 新增在左 parent->_bf++; 新增在右 --;

是否继续更新依据:子树的高度是否变化

1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1

说明之前parent一边高一边低,这次恰好插入填上矮的那边,parent所在子树高度不变,直接break

2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,

parent所在子树高度变了,得继续往上更新(这段代码的cur和parent都在往上跑 所以叫往上更新)

3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则

只好动手术---旋转

        //更新平衡因子
        while (parent)//最多更新到根
        {
            // 新增在右,parent->bf++;
            // 新增在左,parent->bf--;
            if (cur == parent->_left)
            {
                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)
            {
                //旋转
            }
            else
            {
                assert(false);//如果这里报错 说明插入写的有问题 可以更快的判断错误位置
            }
        }
        return true;
    }

此时只差旋转手术刀没有解决 这也是一个不好突破的点

AVL树的四种旋转:

else if (parent->_bf == 2 || parent->_bf == -2)

{

//旋转

}

当走到这种地步的时候 只能通过旋转才可以保持结构的稳定

旋转条件:(也是Insert需要补充的代码)

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR

当pSubR的平衡因子为1时,执行左单旋(RR)

当pSubR的平衡因子为-1时,执行右左双旋(RL)

2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL

当pSubL的平衡因子为-1是,执行右单旋(LL)

当pSubL的平衡因子为1时,执行左右双旋(LR)

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

else if (parent->_bf == 2 || parent->_bf == -2)
            {
                if (parent->_bf == 2 && cur->_bf == 1)
                {
                    RotateL(parent);//RR 在较高右子树插入右节点
                }
                else if (parent->_bf == -2 && cur->_bf == -1)
                {
                    RotateR(parent);//LL 在较高的左子树插入左节点
                }
                else if (parent->_bf == -2 && cur->_bf == 1)
                {
                    RotateLR(parent);//LR  在较高的左子树插入右节点
                }
                else if (parent->_bf == 2 && cur->_bf == -1)
                {
                    RotateRL(parent);// RL 在较高的右子树插入左节点
                }
                else
                {
                    assert(false);
                }
            }

旋转的要求:

1.左右高度不超过一

2.旋转过程中也得保持是搜索树、

3.更新调整孩子节点的平衡因子

4.让子树高度和插入前保持一致(break)

~所谓的左旋和右旋都是以子树为原点的:如b是a的子树,那么旋转就围绕b来进行。

~如果b是a的左子树,那么就围绕b将a向右旋转,看着就像是a直接掉下来了,掉成了b的右子树。

~如果b是a的右子树,那么就围绕b将a向左旋转,看着就像是a直接掉下来了,掉成了b的左子树。

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

60是30的子树 看起来就像是将60提起来 30被旋转到了左边一样

逻辑思路:

60的左边变成30的右边

30变成60的左边

原来30是根 现在60是根

代码书写思路:(走下去还有返回头来看看_parent节点有没有书写完毕)

注意:b存在与否 树局部与否

将30 60 和 b节点分别取名为 parent subR subRL 是为了防止将来的节点丢失

如果b不存在的话 是不需要subR的_parent建立联系的

为了避免局部子树旋转带来的问题 利用ppNode记录parent的_parent;

若不是局部子树 改变根 返回空

若是局部子树 书写subR的_parent=ppNode

注意:平衡因子

最后一定要刷新平衡因子 都变成了0

void RotateL(Node* parent)
    {
        Node* subR = parent->_right;//30的右孩子60记录
        Node* subRL = subR->_left;//记录可能不存在的b

        //开始旋转
        parent->_right = subRL;//60的左调整到30的右
        if (subRL)//如果b存在的话 要是不存在 空指针也没有parent
            subR->_parent = parent;

        Node* ppNode = parent->_parent;//担心是局部子树 记录下根节点的parent

        subR->left = parent;//30到60的左边
        parent->_parent = subR;//parent节点一定存在 所以不写if语句

        //如果非局部子树 根节点的parent得置空
        if (ppNode == nullptr)
        {
            _root = subR;//subR就是60
            _root->_parent == nullptr;//必须得置空
        }
        //如果是局部子树 
        else
        {
            if (ppNode->left == parent)//不会改变 parent可是我一开始就传进来的参数
            {
                ppNode->_left = subR;
            }
            else
            {
                ppNode->_right = subR;
            }
            subR->_parent = ppNode;//相呼应 保持一致
        }
        parent->_bf = subR->_bf = 0;//只动了60 和 30的孩子 没有动abc
    }

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

60的左边变成30的右边

60变成30的右边

根变成了30

节点:parent subL subLR

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

将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新

逻辑分析:90 parent 30 subL 60 subLR

对于90左边高 对于30右边高

刚开始30为轴 左单旋 b变30右 30变60左 60变根 缕直之后

90为轴 c变90左 90变60右 60变根

注意:这里的平衡因子不可以顺利的都要变成0 而是得具体分析 得利用subLR的_bf来分析

情况一: b插入

subL 0 sublR 0 parent 1

情况二:c插入

情况三:subLR 60节点自己就是新增 所以一股脑子全是0

这般操作小来 平衡因子的值就确定完毕了

代码分析:注意记录平衡因子就好

void RotateLR(Node* parent)
    {
        Node* subL = parent->_left;//30
        Node* subLR = sub->_right;//60
        int bf = subLR->_bf;//用60衡量平衡因子
        //先对30进行左旋
        RotateL(parent->_left);
        //对根进行右旋
        RotateR(parent);
        //平衡因子的调整
        if (bf == -1) // subLR左子树新增
        {
            subL->_bf = 0;
            parent->_bf = 1;
            subLR->_bf = 0;
        }
        else if (bf == 1) // subLR右子树新增
        {
            parent->_bf = 0;
            subL->_bf = -1;
            subLR->_bf = 0;
        }
        else if (bf == 0) // subLR自己就是新增
        {
            parent->_bf = 0;
            subL->_bf = 0;
            subLR->_bf = 0;
        }
        else
        {
            assert(false);//方便调整代码
        }
    }

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

逻辑分析:30 parent 90 subR 60 subRL

将双旋变成单旋后再选转,即:先对90进行右单旋,然后再对30进行左单旋,旋转完成后再考虑平衡因子的更新 右单旋 c变30左 90变60右 60变成根

b变30右 30变60左 60变根

平衡因子三种情况:(按照30 60 90 的顺序)

情况一:

-1 0 0

情况二:

0 0 1

情况三:

0 0 0

AVL性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这

样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操

作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,

有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数

据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小羊oi!

小小的赞 会是精神百倍的重要源

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

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

打赏作者

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

抵扣说明:

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

余额充值