【数据结构】AVLTree的实现

一、AVLTree的概念

1.AVLTree的定义

AVLTree:高度平衡二叉搜索树

①首先要满足是一棵搜索二叉树  ②每个节点左右子树的高度差的绝对值不超过1

2.平衡因子

为了方便AVLTree的实现,这里引入节点平衡因子的概念(这只是其中一种实现方式)

节点平衡因子计算公式:平衡因子 = 右子树高度 - 左子树高度 

3.AVLTree节点的定义

template<class K, class V>
struct AVLTreeNode
{
public:
    AVLTreeNode(const pair<K, V>& kv)
        :_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_bf(0)
        ,_kv(kv)
    {}
public:
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf;//balance factor 平衡因子
    std::pair<K, V> _kv;//键值对
};

二、AVLTree的操作

(一)AVLTree的插入

AVLTree的插入比二叉搜索树的插入要复杂,AVLTree的插入大体上分成:插入和旋转。

下面对每一步进行详细的介绍,在最后会有插入函数整体的代码

1.先按搜索树的规则进行插入(和二叉搜索树相比就是多一步连接父亲节点)

//1.先按搜索树的规则进行插入
//空树插入
if(_root == nullptr)
{
    _root = new Node(kv);
    return true;
}
//非空树插入
Node* cur = _root;
Node* parent = nullptr;
//找位置
while (cur)
{
    //要插入的小,走左边
    if(kv.first < cur->_kv.first)
    {
        parent = cur;
        cur = cur->_left;
    }
    //要插入的大,走右边
    else if(kv.first > cur->_kv.first)
    {
        parent = cur;
        cur = cur->_right;
    }
    //不可以插入一样的
    else
        return false;
}
//找到位置了,插入
cur = new Node(kv);
//连接
if(kv.first < parent->_kv.first)
{
    parent->_left = cur;//连孩子
    cur->_parent = parent;//连父亲
}
else
{
    parent->_right = cur;//连孩子
    cur->_parent = parent;//连父亲
}

2.更新平衡因子(只会影响插入节点的祖先)

如果更新完平衡因子,如果平衡,则插入结束;parent指向的节点出现不平衡(平衡因子是-1、

0、1就平衡,否则不平衡),需要旋转处理(在第三点介绍)

现在有一个问题:插入后哪些节点会变得不平衡?答:只会影响插入节点的祖先。

再进一步分析:也不是所有的祖先都会被影响,下面四点就是更新平衡因子的过程

(1)首先:若cur是parent的左孩子,parent->bf--;若cur是parent的右孩子,parent->bf++

(2)更新完parent的bf(即完成第一步),如果parent->bf == 0,更新结束,插入完成

parent->bf == 0,说明parent高度不变。解释:说明更新前,parent的bf是1或-1,现在变成0,

说明把矮的那边填上了。说明以parent为根的树的高度不变,对上层没有影响。

(3)更新完parent的bf(即完成第一步),如果parent->bf==1/-1,继续往上更新

parent->bf==1/-1,说明parent的高度变了。解释:说明更新前,parent的bf是0,现在变成1

或-1,说明以parent为根的树的高度变高了,对上层有影响。

(4)更新完parent的bf,如果parent->bf == 2/-2,就需要旋转处理了

parent->bf == 2/-2,说明以parent为根的树出现了不平衡,需要调整成平衡的。

下面展示更新平衡因子的代码

//2.更新平衡因子
while(parent)
{
    if(cur == parent->_left)
        parent->_bf--;
    else
        parent->_bf++;
    
    if(parent->_bf == 0)//说明parent所在的子树的高度不变,更新结束
        break;
    
    else if(parent->_bf == 1 || parent->_bf == -1)
    {
        //说明parent所在的子树的高度变了,继续往上更新
        cur = parent;
        parent = parent->_parent;
    }
    else if(parent->_bf == 2 || parent->_bf == -2)
    {
        //parent所在的子树出现不平衡了,需要旋转处理。
    }
}

3.旋转处理

如果出现了不平衡,那么就需要旋转处理。旋转完成之后,得到的新树要满足

①新树还是搜索树  ②新树从不平衡变成平衡的

旋转处理一共分成四种情况:两种单旋、两种双旋。单旋的旋转路径是直线,单旋代码更关注旋

转过程。双旋的旋转路径是折线,可以通过两个单旋来完成一个双旋,但是由于插入的位置不 

同,最终平衡因子也会随着插入位置的改变而改变,所以双旋代码的重点是调整平衡因子。

下面用图来解释什么是:旋转路径是直线、旋转路径是折线

(1)单旋:旋转路径是直线

①左单旋

什么情况下要进行左单旋?parent->_bf == 2 并且 cur->_bf == 1 的情况下

可以这样理解:新插入的节点在右边,导致右边多出来了,所以需要往左边旋转。

如何左单旋?(a)subR的左边放在parent的右边 (b)parent变成subR的左边

左旋函数代码:因为AVLTree是用三叉链来表示的,所以不仅需要连接孩子,还需要连接父亲。

连接父亲注意三点:

(a)要更新subRL的父亲,特别注意当subRL为空时,就不需要连接父亲了。

(b)要更新parent的父亲,将parent的父亲更新为subR。

(c)旋转后,新树的树根变成了subR,所以需要更新subR的父亲。

        第一种情况:原来parent是这棵树的根,现在subR成了这棵树的根。

        第二种情况:parent为根的树只是整棵树的子树。

                原来parent为根的树是左子树。

                原来parent为根的树是右子树。

//左单旋
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* ppNode = parent->_parent;//指向parent节点的父亲
    
    //1.subR的左边放在parent的右边
    //!!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
    parent->_right = subRL;//连儿子
    if(subRL)//连父亲
        subRL->_parent = parent;
    
    //2.parent变成subR的左边
    //同样 !!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
    subR->_left = parent;
    parent->_parent = subR;
    //找subR的父亲
    //(1)原来parent是这棵树的根,现在subR成了这棵树的根
    if(_root == parent)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    //(2)parent为根的树只是整棵树的子树
    else
    {
        //连孩子
        //parent是一个左子树
        if(ppNode->_left == parent)
            ppNode->_left = subR;
        //parent是一个右子树
        else
            ppNode->_right = subR;
        
        //连父亲
        subR->_parent = ppNode;
    }
    //更新平衡因子
    parent->_bf = subR->_bf = 0;
}

②右单旋

什么情况下要进行右单旋?parent->_bf == -2 并且 cur->_bf == -1 的情况下

可以这样理解:新插入的节点在左边,导致左边多出来了,所以需要往右边旋转。

如何右单旋?(a)subL的右边放在parent的左边 (b)parent变成subL的右边

右旋函数代码:因为AVLTree是用三叉链来表示的,所以不仅需要连接孩子,还需要连接父亲。

连接父亲注意的三点和左旋函数的一样,在这里就不再提了,直接看代码。

//右单旋
void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    Node* ppNode = parent->_parent;//指向parent节点的父亲
    
    //1.subL的右边放到parent的左边
    parent->_left = subLR;
    if(subLR)
        subLR->_parent = parent;
    
    //2.parent变成subL的右边
    subL->_right = parent;
    parent->_parent = subL;
    //找subL的父亲
    //(1)原来parent是这棵树的根,现在subL成了这棵树的根
    if(_root == parent)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    //(2)parent为根的树只是整棵树的子树
    else
    {
        //连孩子
        //parent是一个左子树
        if(ppNode->_left == parent)
            ppNode->_left = subL;
        //parent是一个右子树
        else
            ppNode->_right = subL;
        //连父亲
        subL->_parent = ppNode;
    }
    //更新平衡因子
    parent->_bf = subL->_bf = 0;
}

(2)双旋:旋转路径是折线​​​​​​​

①左右双旋

什么情况下要进行左右双旋?parent->_bf == -2 并且 cur->_bf == 1 的情况下。

在b和c上插入都是左右双旋,但是这会导致两种插入的subLR->_bf不一样(后面重点说明)

如何左右双旋?先对30的位置进行左单旋,在对90的位置进行右单旋。

左右双旋函数代码:⚠️因为插入的位置不一样导致 更新平衡因子 会有分类讨论。

//左右双旋
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;
    
    RotateL(subL);
    RotateR(parent);
    
    if(bf == 1)
    {
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    }
    else if(bf == -1)
    {
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if(bf == 0)
    {
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
}

②右左双旋

什么情况下要进行右左双旋?parent->_bf == 2 并且 cur->_bf == -1 的情况下。

在b和c上插入都是右左双旋,但是这会导致两种插入的subRL->_bf不一样(后面重点说明)

​​​​​​​​​​​​​​

如何右左双旋?先对90的位置进行右单旋,在对30的位置进行左单旋。

右左双旋函数代码:⚠️因为插入的位置不一样导致 更新平衡因子 会有分类讨论。

//右左双旋
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;
    
    RotateR(subR);
    RotateL(parent);
    
    if(bf == -1)
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else if(bf == 1)
    {
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if(bf == 0)
    {
        parent->_bf = 0;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
}

下面展示旋转处理的代码

注意⚠️:进行了旋转处理以后,整棵树就达到了平衡状态,所以直接跳出循环即可。

else if(parent->_bf == 2 || parent->_bf == -2)
{
    //parent所在的子树出现不平衡了,需要旋转处理。
    if(parent->_bf == 2)
    {
        if(cur->_bf == 1)
            RotateL(parent);
        else if (cur->_bf == -1)
            RotateRL(parent);
    }
    else if(parent->_bf == -2)
    {
        if(cur->_bf == -1)
            RotateR(parent);
        else if(cur->_bf == 1)
            RotateLR(parent);
    }
    //完成旋转之后,整棵树就达到了平衡状态,所以直接跳出循环即可
    break;
}

下面展示插入的全部代码

bool Insert(const pair<K, V>& kv)
{
    //1.先按搜索树的规则进行插入
    //空树插入
    if(_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }
    //非空树插入
    Node* cur = _root;
    Node* parent = nullptr;
    //找位置
    while (cur)
    {
        //要插入的小,走左边
        if(kv.first < cur->_kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        //要插入的大,走右边
        else if(kv.first > cur->_kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        //不可以插入一样的
        else
            return false;
    }
    //找到位置了,插入
    cur = new Node(kv);
    //连接
    if(kv.first < parent->_kv.first)
    {
        parent->_left = cur;//连孩子
        cur->_parent = parent;//连父亲
    }
    else
    {
        parent->_right = cur;//连孩子
        cur->_parent = parent;//连父亲
    }
    
    //2.更新平衡因子
    while (parent) {
        if(cur == parent->_left)
            parent->_bf--;
        else
            parent->_bf++;
        
        if(parent->_bf == 0)//说明parent所在的子树的高度不变,更新结束
            break;
        
        else if(parent->_bf == 1 || parent->_bf == -1)
        {
            //说明parent所在的子树的高度变了,继续往上更新
            cur = parent;
            parent = parent->_parent;
        }
        else if(parent->_bf == 2 || parent->_bf == -2)
        {
            //parent所在的子树出现不平衡了,需要旋转处理。
            if(parent->_bf == 2)
            {
                if(cur->_bf == 1)
                    RotateL(parent);
                else if (cur->_bf == -1)
                    RotateRL(parent);
            }
            else if(parent->_bf == -2)
            {
                if(cur->_bf == -1)
                    RotateR(parent);
                else if(cur->_bf == 1)
                    RotateLR(parent);
            }
            //完成旋转之后,整棵树就达到了平衡状态,所以直接跳出循环即可
            break;
        }
    }
    return true;
}

(二)AVLTree​​​​​​​的删除

AVLTree​​​​​​​的删除和插入的过程相似,所以我们对比插入来学习删除。

增【插入】:a、按搜索树的规则插入 b、更新平衡因子(下面对比两者更新平衡因子的不同)

c、更新过程中出现平衡因子为2/-2,则根据情况判断是那种旋转,进行旋转处理。

删【删除】:a、按搜索树的规则删除 b、更新平衡因子(下面对比两者更新平衡因子的不同)

c、更新过程中出现平衡因子为2/-2,则根据情況判断是哪种旋转,进行旋转处理。

介绍删除中平衡因子的更新:首先要明确删除的更新和插入的更新过程基本相反,依然对比介绍

1.右边插入,父亲平衡因子++;左边插入,父亲平衡因子--。

   右边删除,父亲平衡因子--;左边删除,父亲平衡因子++。

2.插入后,父亲的平衡因子变成0,说明父亲所在的树高度不变,更新结束。

   删除后,父亲的平衡因子变成0,说明父亲所在的树高度变了,继续往上更新。

3.插入后,父亲的平衡因子变成1/-1,说明父亲所在的树高度变了,继续往上更新。

   删除后,父亲的平衡因子变成1/-1,说明父亲所在的树高度不变,更新结束。

4.插入/删除后,父亲的平衡因子变成2/-2,说明不平衡,旋转处理。

(三)AVLTree的查改

1.搜索树中key是不允许修改的,因为如果修改了整棵树可能就破坏了。

2.key/value的场景下可以修改value,但是不能修改key。

查和改和二叉树搜索树是一样的,所以这里不在展示代码,在二叉树搜索树的博客中有展示。

(四)AVLTree​​​​​​​的验证

验证AVLTree​​​​​​可以分成两步:

1.验证其为二叉搜索树 <==> 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树。

2.验证其为平衡树 <==> 每个节点子树高度差的绝对值不超过1。

三、AVLTree的性能

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

样可以保证查询时高效的时间复杂度o(logN)。但是如果要对AVL树做一些结构修改的操作,性能

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

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

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

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值