AVL树详解+模拟实现

1:概念

当数据有序,二叉搜索树将趋近于单叉树,查找元素相当于在顺序表中查找元素,效率低下,两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis创建了AVL树。特性如下:

  • 左右子树高度差的绝对值不超过1

  • 左右子树都为AVL树

如果一棵树是高度平衡的,搜索效率就可以保持在log2的N

2:模拟实现

2.1:结构体定义

因为map/multimap/set/multiset底层是平衡搜索树,所以可以采用键值对插入的方式。

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;
    AVLTreeNode(const pair<K,V>& kv)
        :left(nullptr)
        ,right(nullptr)
        ,parent(nullptr)
        ,_kv(kv)
        ,_bf(0)
    {}
};

_bf是平衡因子,表示左右子树高度差。

2.2:平衡因子

平衡因子表示左右高度差,通常用右子树高度-左子树高度。

2.3:插入

插入就和二叉搜索树一样,用kv的键值对的K(first)比较,但是AVL树的插入分为2个步骤,插入节点和调整平衡因子。

    bool Insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)
        {
            _root = new Node(kv);
        }
        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;

        }

先找到合适的位置,再判断自己是左节点还是右节点,将这个new的新节点和父亲链接起来。

2.3:调整平衡因子

因为是插入孩子节点,所以要调整父亲的平衡因子,因此当父亲为空的时候停止调整。

           while(parent)
            if (cur = parent->left)
            {
                parent->_bf--;
            }
            else
            {
                parent->_bf++;
            }

cur是左,平衡因子--,因为左高右低,同理反之。

因为AVL树要保持高度平衡,所以接下来要对父亲的平衡因子情况做判断。

            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)
                {
                    //右子树高度发生了变化
                    RotateR(parent);
                    break;
                }
            }
  • 如果父亲平衡因子变成0,说明原本是1或者-1,也就是从不平衡到平衡,这次插入是填充矮的一边,高度是不会发生变化的,不需要进行旋转。

  • 如果父亲平衡因子变成了1的绝对值,说明从0到1和-1,(不可能从2变成1,从-2变成-1是因为插入的每一次要保证是AVL树,如果插入前已经不平衡,那么后续怎么旋转都没用),那么此时子树高度就发生了变化,变得更高了,但是如果是1的绝对值,还能满足AVL树的特性。

但是如图,9后再插入值,9的平衡因子变成了1,8的平衡因子变成了2,不能满足AVL树的特性了,所以当父亲的平衡因子变成1的绝对值的时候,表明parent所在子树的高度变化了,要持续向上更新。

  • 当父亲的平衡因子变为2的绝对值,并且cur的平衡因子是1的时候,说明当前是在cur的右节点上插入了节点,导致高度严重不平衡。

2.3.1:左单旋

假设abc是高度为h的avl子树,因为之前说了cur的bf为1,说明是从c这里插入节点,c的高度变为h+1,此时就会发生左单旋,左单旋又分为多种情况。

2.3.1.1:h=0

abc此时为空节点,将30链接到60的左孩子。

2.3.1.2:h=1

把b作为30的右孩子,30作为60的左孩子。因为b必然大于30小于60。30和a必然小于60。

c的左右新增都会引发旋转。

2.3.1.3:h=2

abc高度为2,那么a和b必然各自有3种情况,对于c肯定是z的形状,因为c插入了新节点导致c的高度变为了h+1,才会导致树不平衡,如果c是x或者y,插入了新节点后高度不会变,不会引发旋转。而且c如果是x或者y的情形,就等同于第一种情况h=0,因为我们这里所说的不同的h情况不只是针对整棵树,也有可能是部分的子树旋转。所以这里c如果是x或者y就和第一种情况一样。

所以c是z的形状,当c的2叶子节点的4个孩子节点任意位置插入1个节点,c的高度变为h+1,60的平衡因子变成1,30的平衡因子变成2,必然引发30的旋转。

2.3.1:左单旋实现

旋转的目的:

  • 让左右子树高度差不超过1

  • 旋转过程保证他是搜索树

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

  • 保证子树的高度和插入前一致

    void RotateR(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 == nullptr)
        {
            //更新root
            _root = subR;
            _root->parent = nullptr;
        }
        //parent不是根节点,说明还要把父亲和subr链接
        else
        {
            if (ppNode->left == parent)
            {
                ppNode->left = subR;
            }
            else
            {
                ppNode->right = subR;
            }
            subR->parent = ppNode;
        }
        parent->_bf = subR->_bf = 0;
        
    }

因为结束条件是parent为空就停止调整,所以每次传parent给单旋函数。

实际上调整的是parent右节点/右节点的左节点/parent/parent的parent这4个节点的关系

思路就是

  • 定义2个指针指向parent的右和右的左

  • parent的右指向subRL

  • 当subRL不为空的时候,subRL的parent指向parent

  • subR的左指向parent

  • 定义指针指向parent的parent,因为不确定parent是否为根节点,如果不为根节点,因为parent的parent需要更改为subR,但是parent不是根节点,他还有parent,还应该把更改前的parent的parent与subR进行链接,否则找不到parent的parent。

  • parent的parent指向subR

  • 如果parent的parent是空,说明parent本来是根节点,此时就让根节点变成subR,并且让subR也就是root根节点的父亲变为空指针。不置空那么subR的父亲还仍然指向parent。

  • 如果parent的parent不为空,说明parent不是根节点,此时判断parent是左还是右,然后对应着把parent的parent的左/右链接到subR,

  • 把subR的父亲链接到ppnode

  • 调整parent和subR的平衡因子(目的之一)

为什么旋转一次就可以break了?因为到了当前子树部分(也有可能到了根,不影响),因为当前子树部分的高度变化了,导致上面的高度也会变化,所以旋转当前部分的子树,子树的高度又变为插入之前一样的高度了,上面的高度又会恢复,所以旋转一次break就行。

2.3.2:右单旋

与单左旋差不多

    void RotateL(Node* parent)
    {
        Node* subL = parent->left;
        Node* subLR = subL->right;
        parent->left = subLR;
        if (subLR)
        {
            subLR->parent = parent;
        }
        Node* ppnode = parent->parent;
        parent->parent = subL;
        subL->right = parent;
        if (ppnode == nullptr)
        {
            _root = subL;
            _root->parent = nullptr;
        }
        else
        {
            if (ppnode->left = parent)
            {
                ppnode->left = subL;
            }
            else
            {
                ppnode->right = subL;
            }
            subL->parent = ppnode;
        }
        parent->_bf = subL->_bf = 0;
    }

2.3.3:双旋转

我们前面2种情况的插入是直着插入,如果是右边这种旋转的插入,就算是用前面的办法,把2的左边给1的右边,1作为3的左边,旋转后这样仍然是旋转的样子,不能让子树保持跟插入前的高度一致(就算不能让父亲重新回到0这个平衡因子)。

因此给出了方法,二次旋转。

针对这个又可以画图来分析一下什么情况会导致需要旋转

2.3.3.1:h=0

(此时bc的父亲节点相当于不存在)

2.3.3.2:h=1
2.3.3.3:h=2

b和c必须为z才能导致插入后会发生旋转。

2.3.3.4:双旋过程

双旋情况:

parent和cur分别为-2,1或者2,-1

步骤:

  • 先以30为父亲节点,此时90相当于ppnode,会用来进行链接60

  • 然后以90为父亲节点,再进行右旋。

但是用代码复用的话,平衡因子更新有问题,左旋的话30和60的因子会变成0,右旋的话60和90的因子会变成0。

如果不从单旋分下来看这个问题,本质上这个问题就是把60的b拆分给30的右,60的c拆分给90的左,这样的话90的bf是1,30的bf才是0,更何况如果父亲的bf是-2&&cur的bf是1的时候,b和c哪个插入都不知道,这样一来b和c的平衡因子都不确定,明显平衡因子有错误。

同样的,在h=0的时候,60就是新增,先进行左旋的话(以60为轴点),就是60的左边给30的右边,30变成60的左边,再以90为轴点右旋,就是60的右边变成90的左边,90变成60的右边。

2.3.3.5:双旋的平衡因子调整和双旋LR代码

因此可以根据60的平衡因子来判断是b插入还是c插入还是60就是新增,针对3个情况写出如下的代码。

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

        RotateL(parent->right);
        RotateL(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);
        }
    }
2.3.3.6:RF双旋代码

    void RotateRL(Node* parent)
    {
        Node* subR = parent->right;
        Node* subRL = subL->left;
        int bf = subRL->_bf;

        RotateR(parent->right);
        RotateL(parent);
        if (bf == -1)
        {
            //subrl左边新增
            subR->_bf = 1;
            parent->_bf = 0;
            subRL->_bf = 0;
        }
        else if (bf == 1)
        {
            //subrl右边新增
            parent->_bf = -1;
            subR->_bf = 0;
            subRL->_bf = 0;
        }
        else if (bf == 0)
        {
            //subrl就是新增
            parent->_bf = 0;
            subR->_bf = 0;
            subRL->_bf = 0;

        }
        else
        {
            assert(false);
        }
    }

注意pair的头文件包一下<utility>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不熬夜不抽烟不喝酒

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

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

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

打赏作者

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

抵扣说明:

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

余额充值