当前位置: 首页 > news >正文

2. AVL树

目录

1.AVL树的概念

2.AVL树的实现

2.1 AVL树节点的定义

2.2 AVL树的插入结构

2.3 AVL树的旋转

 2.3.1 左单旋

2.3.2 右单旋

2.3.3 右左双旋

2.3.4 左右双旋

2.4 总体的插入逻辑


1.AVL树的概念

        二叉搜索树虽可以增加查找的效率,但如果数据有序或接近有序,二叉搜索树将会退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因为两位俄罗斯的数学家发明了一种解决上述问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过1,即可降低树的高度,从而减少平均搜索长度。

一颗AVL树或者是空树,是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树"高度平衡二叉树"
  • 左右子树高度之差(平衡因子)的绝对值不超过1(-1、0、1)

|右子树高度-左子树高度|≤1,平衡为什么不是高度相等呢?有时候是真的做不到。

2.AVL树的实现

2.1 AVL树节点的定义

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

2.2 AVL树的插入结构

template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    bool insert(const pair<K, V>& kv)
    {
        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;
        else
            parent->right = cur;
        cur->_parent = parent;

        //控制平衡,更新平衡因子_bf
    }
private:
    Node* _root = nullptr;
};

上面是插入了结点,下面就该考虑如何平衡了:需要分情况讨论:

插入结点就会改变 cur 的 _parent 的 _bf:

新增在左:parent 的因子 --

  20(-1)
 /  
10(0)

新增在右:parent 的因子 ++

20(1)
  \
  30(0)

更新后 parent 的因子 == 0,parent的子树高度不变,不会影响祖先,不再向上更新

  20(0)
 /  \
10   30

更新后 parent 因子 == 1 or -1,说明parent的平衡因子原来是0,现在高度变了,需要沿parent逐级向上更新,因为当前树变的不平衡,一定会影响更高层的_bf,也就是根据parent的变化,更新parent->_parent的 _bf

  20 (0)
 /  \
10   30(1) parent
       \
       40 cur

 向上更新时,相当于cur新增在下面这个parent的右边,所以 parent 的 _bf --,由0变-1

  20 parent (0)->(-1)
 /  \
10   30(1) cur
       \
       40

更新后 parent 因子 == 2 or -2,说明 parent 原来就不平衡,此时更不平衡了,需要对parent所在子树进行旋转

  20 (2)
 /  \
10   30 (2) parent
       \
       40 (1) cur
         \
         50

 更新到 cur 为根结点,也就是 parent 为 nullptr 时结束,说明更新到了最祖宗的结点

        //控制平衡,更新平衡因子_bf
        while(parent)
        {
            if(cur == parent->left)
                parent->_bf--;
            else
                parent->_bf++;
            //检查更新后的_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//万一不小心更新出了3,4,5...说明2的时候就出错了
                assert(false);
        }

2.3 AVL树的旋转

        插入函数的结构我们很清楚了,下面来分析一下旋转的具体过程:

旋转的时候要注意两个问题:

  1. 保持树是搜索树
  2. 变成平衡树的同时降低树的高度
 2.3.1 左单旋

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

  20 
 /  \
10   30 (2) parent
       \
       40 (1) cur
         \
         50

核心操作:

parent->right = cur->left;
cur->left = parent;
     30 (2) parent
      \
       40 (1) cur
        \
         50

parent->right = cur->left;


(parent)30   40 cur
         \  /  \
         null   50

cur->left = parent;

             40 cur
            /  \
   (parent)30  50
            \
            null

但是这时是有问题的,每个结点有三个方向,left, right, parent。

我们断开的是parent→right; cur→left, 但是只有这两步是不行的,他们之间复杂的指向关系还需要更新。

我们可以画个图:

所以我们的两个核心操作改变了三个结点之间的指向,cur、parent、cur→left

    void rotateL(Node* parent)
    {
        Node* cur = parent->_right;
        Node* curleft = cur->_left;
        
        parent->_right = curleft;
        if(curleft)
            curleft->_parent = parent;
        cur->_left = parent;

        Node* ppnode = parent->_parent;
        parent->_parent = cur;

        if(parent == _root)//如果旋转放下去的是根结点,那么cur就是新根,它的父结点就是nullptr
        {
            _root = cur;
            cur->_parent = nullptr;
        }
        else//如果parent之上还有结点,那么需要在前面修改parent的_parent之前保存ppnode,现在cur->_parent=ppnode
        {
            cur->_parent = ppnode;
            //然后我们还不知道ppnode的left还是right链接的parent,所以也不知道是left还是right链接cur
            if(ppnode->_left == parent)
                ppnode->_left = cur;
            else
                ppnode->_right = cur;
        }
        parent->_bf = cur->_bf = 0;
    }
  • parent为根结点,放到cur的left了,cur成为新的根节点,cur→_parent = nullptr
  • parent之上还有父节点ppnode,ppnode需要连接子树新的根节点cur,就需要判断ppnode的左还是右去连接cur。
  • 最后更新parent 和 cur 的_bf
2.3.2 右单旋

新结点添加在较高左子树的左侧-左左:右单旋

和左单旋类似,基本就是翻转过来,核心操作为:

parent->_left = cur->_right;
cur->_right = parent;
void rotateR(Node* parent)
{
    Node* cur = parent->_left;
    Node* curright = cur->_right;

    parent->_left = curright;
    if(curright)
        curright->_parent = parent;
    cur->_right = parent;

    Node* ppnode = parent->_parent;
    parent->_parent = cur;
    if(_root == parent)
    {
        _root = cur;
        cur->_parent = nullptr;
    }
    else
    {
        cur->_parent = ppnode;
        if(ppnode->_left == parent)
            ppnode->_left = cur;
        else
            ppnode->_right = cur;
    }
    parent->_bf = cur->_bf = 0;
}
2.3.3 右左双旋

新增结点在较高右子树的左边:右左

  20 (1) 
 /  \
10   30 
    /  \
   25   40 
    
  20 (2) 
 /  \
10   30 (-1)
    /  \
   25   40 
   /
  24

若像原来,右子树高,parent 的 _bf = 2,走左单旋:

  20 (2) parent
 /  \
10   30 (-1) cur
    /  \
   25   40 
   /
  24

     20   30 cur
    /  \  /  \
   10   25   40 
       /
      24

       30
      /  \
     20   40
    /  \   
   10   25  
       /
      24

还是不平衡,所以要先对cur子树进行右单旋,再对parent进行左单旋,即为双旋:

我们看黄线就可以明显的观察出双旋的本质

第一个旋转是将 折线 变 直线,第二个旋转是将 直线 变 平衡。

void RotateRL(Node* parent)
{
    RotateR(parent->right);
    RotateL(parent);
}

但是在双旋中,因为我们调用的单旋最后都是平衡的,所以cur,curleft,parent 的 _bf 最后都修改为了0,在上图中我们可以看出这是不对的。

我们需要分情况讨论_bf的情况。

上例中我们在 60 的左边插入了新结点 40 导致的双旋,所以 60 的_bf 为-1,这种情况下,双旋完成后

cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;

如果在 60 的右插入新结点 50 导致双旋,那么 60 的_bf 为 1,这种情况下:

cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;

最后就是 60自己 引发的双旋,60 的_bf 是0,这种情况比较简单,旋转之后三个结点的平衡因子都是0

30       30
  \       \
  90  →   60  →   60
  /        \     /  \
 60        90   30  90
    void rotateRL(Node* parent)
    {
        Node* cur = parent->_right;
        Node* curleft = cur->_left;
        int bf = curleft->_bf;

        rotateR(parent->_right);
        rotateL(parent);
        if(bf == 1)
        {
            curleft->_bf = 0;
            parent->_bf = -1;
            cur->_bf = 0;
        }
        else if(bf == -1)
        {
            curleft->_bf = 0;
            parent->_bf = 0;
            cur->_bf = 1;
        }
        else
        {
            curleft->_bf = parent->_bf = cur->_bf = 0;
        }
    }
2.3.4 左右双旋

新增结点在较高左子树的右边,与有左旋是大致相同的,基本镜像

    void rotateLR(Node* parent)
    {
        Node* cur = parent->_left;
        Node* curright = cur->_right;
        int bf = curright->_bf;
        rotateL(parent->_left);
        rotateR(parent);
        if(bf == 1)
        {
            curright->_bf = 0;
            parent->_bf = 0;
            cur->_bf = -1;
        }
        else if(bf == -1)
        {
            curright->_bf = 0;
            parent->_bf = 1;
            cur->_bf = 0;
        }
        else
        {
            curright->_bf = parent->_bf = cur->_bf = 0;
        }
    }

2.4 总体的插入逻辑

    bool insert(const pair<K, V>& kv)
    {
        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;
        else
            parent->right = cur;
        cur->_parent = parent;

        //控制平衡,更新平衡因子_bf
        while(parent)
        {
            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)
            {
                //旋转
                if(parent->_bf == 2)
                {
                    if(cur->_bf == 1)
                        rotateL(parent);
                    else
                        rotateRL(parent);
                }
                else
                {
                    if(cur->_bf == -1)
                        rotateR(parent);
                    else
                        rotateLR(parent);
                }
                break;
            }
            else
                assert(false);
        }
    }

相关文章:

  • 查询、插入、更新、删除数据的SQL语句(SQLite)
  • Ceph集群部署步骤
  • Python---数据分析(Pandas十一:二维数组DataFrame统计计算二)
  • 《AI大模型趣味实战 》第8集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 2
  • 开源AI大模型、AI智能名片与S2B2C商城小程序源码:实体店引流的破局之道
  • 指针,数组 易混题解析(一)
  • 【STM32】SPI通信外设硬件SPI读写W25Q64
  • 手动离线安装NextCloud插件
  • ElementUI表格展开属性
  • AI与自媒体的深度融合路径、场景与挑战
  • 从 Java 的 Spring Boot MVC 转向 Go 语言开发的差异变化
  • 华为 SD-WAN 内联隧道原理
  • 使用Python构建去中心化预测市场:从概念到实现
  • 高频面试题(含笔试高频算法整理)基本总结回顾67
  • Unity中MonoBehaviour的生命周期详解
  • PyTorch核心基础知识点(一)
  • numpy学习笔记9:numpy的广播机制详细解释
  • 吴恩达机器学习笔记复盘(九)逻辑回归模型概述
  • 人工智能 - 在 Spring Boot 中调用 AnythingLLM+DeepSeek 的知识库获取消息接口
  • 大模型开发(六):LoRA项目——新媒体评论智能分类与信息抽取系统
  • 经彩申城!上海网络大V沙龙活动走进闵行
  • 美联储主席:不打算先发制人地降息,将继续观望
  • 巴基斯坦外交部:印度侵略行径侵犯巴主权
  • 中演协:五一假期全国营业性演出票房收入同比增长3.6%
  • 景点变回监狱,特朗普下令重新启用“恶魔岛”
  • 特朗普要征100%关税,好莱坞这批境外摄制新片有麻烦了