AVL数实现

1. AVL的概念

  • AVL 树的定义:AVL 树是最先发明的自平衡二叉查找树。它要么是一棵空树,要么是具备下列性质的二叉搜索树:它的左右子树都是 AVL 树,且左右子树的高度差的绝对值不超过 1 。换句话说,AVL 树是一棵高度平衡的搜索二叉树,它通过严格控制每棵子树的高度差来控制整棵树的平衡,从而避免普通二叉搜索树在有序插入时退化成单支链表的问题。
  • 平衡因子(balance factor) :为了更方便地观察和控制树是否平衡,这里引入平衡因子的概念。每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度。由 AVL 树的性质可知,任何结点的平衡因子只可能是 0、1、-1 这三个值。需要强调的是,AVL 树并不是必须要存储平衡因子,平衡因子只是一种辅助手段,有了它可以更直观、更高效地判断某棵子树是否失衡,以及失衡的方向。
  • 效率分析:AVL 树整体的结点数量和分布与完全二叉树类似,因此树的高度可以控制在 logN 级别。高度被控制住了,增、删、查、改这些操作的时间复杂度自然也就能稳定在 O(logN),这正是 AVL 树相对于普通二叉搜索树的核心优势所在。

2.AVL树的实现

2.1AVL树的结构

本节先给出 AVL 树的结点定义与整体框架,这是后续所有插入、旋转、查找操作的基础。重点有两个:

  • 三叉链结构 :AVLTreeNode 中除了 _left、_right 两个左右孩子指针外,还额外保存了 _parent 父指针。之所以需要父指针,是因为插入新结点后要沿着父指针一路向上回溯,更新祖先结点的平衡因子,并在失衡时定位到需要旋转的子树根。没有父指针,向上回溯将非常困难。
  • 平衡因子 _bf :每个结点用一个 int 记录平衡因子,初始值为 0,表示左右子树等高。它让"是否失衡、往哪边失衡"的判断变成一次简单的整数比较,是 AVL 树高效维护平衡的关键。

AVLTree 类则用 _root 保存整棵树的根,对外只暴露增删查改接口,内部细节全部封装。

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
        // 
需要
parent
指针,后续更新平衡因⼦可以看到
 
        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>
class AVLTree
{
        typedef AVLTreeNode<K, V> Node;
public:
        //...
private:
Node* _root = nullptr;
};

2.2 AVL树的插⼊

2.2.1 AVL树插⼊⼀个值的⼤概过程

  1. 按二叉搜索树规则插入:先像普通二叉搜索树一样,从根结点开始比较 key 值,一路向下找到合适的空位置,把新结点挂上去。
  2. 向上更新平衡因子:新增结点以后,只会影响它祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子。因此需要从新增结点出发,沿着到根结点的路径自底向上更新平衡因子。实际中最坏情况下要一直更新到根,但有些情况更新到中间就可以提前停止,具体情况在下一小节详细分析。
  3. 更新过程无异常则插入结束:如果在更新平衡因子的过程中,所有祖先结点都仍然满足平衡条件,说明这次插入没有破坏 AVL 树的性质,插入直接结束。
  4. 出现不平衡则旋转:如果更新平衡因子的过程中出现了不平衡(平衡因子变成 2 或 -2),就对这棵不平衡的子树进行旋转。旋转在调整平衡的同时,本质上还把这棵子树的高度降回了插入之前的高度,因此不会再影响上一层结点,旋转完成后插入即可结束。

2.2.2 平衡因⼦更新

更新原则:

  • 平衡因子的定义:平衡因子 = 右子树高度 - 左子树高度,这是整个更新逻辑的基准。

  • 只有子树高度变化才会影响平衡因子:如果某棵子树的高度没有变化,那么它父结点的平衡因子也不会改变,这是判断是否需要继续向上更新的根本依据。

  • 插入方向决定加减:插入结点会增加所在子树的高度。所以新增结点在 parent 的右子树时,parent 的平衡因子 ++;新增结点在 parent 的左子树时,parent 的平衡因子 --。

  • 高度是否变化决定是否继续 :parent 所在子树的高度是否发生变化,决定了是否需要继续向上更新祖先结点的平衡因子。

    更新停⽌条件:

  • 情况一:更新后 parent 的平衡因子等于 0。更新过程中 parent 的平衡因子变化为 -1 -> 0 或者 1 -> 0,说明更新前 parent 子树一边高一边低,而新增的结点恰好插入在低的那一边,插入后 parent 所在的子树两边变得一样高,高度没有变化,因此不会影响 parent 的父亲结点的平衡因子,更新到此结束。

  • 情况二:更新后 parent 的平衡因子等于 1 或 -1。更新过程中 parent 的平衡因子变化为 0 -> 1 或者 0 -> -1,说明更新前 parent 子树两边一样高,新增结点插入后,parent 所在的子树变成一边高一边低。此时 parent 子树仍然符合平衡要求,但它的高度增加了 1,会影响 parent 的父亲结点的平衡因子,所以必须继续向上更新。

  • 情况三:更新后 parent 的平衡因子等于 2 或 -2。更新过程中 parent 的平衡因子变化为 1 -> 2 或者 -1 -> -2,说明更新前 parent 子树就已经一边高一边低,而新增的结点又插在了高的那一边,使得高的那边更高,彻底破坏了平衡。此时 parent 所在的子树不符合平衡要求,必须进行旋转处理。旋转的目标有两个:第一,把 parent 子树旋转平衡;第二,降低 parent 子树的高度,使其恢复到插入结点以前的高度。正因为高度被恢复了,所以旋转后不需要继续往上更新,插入直接结束。

  • 情况四:一路更新到根。如果不断向上更新,一直更新到根结点,此时根的平衡因子是 1 或 -1,也已经没有更上层的结点需要更新,更新同样停止。

2.2.3插⼊结点及更新平衡因⼦的代码实现

cpp 复制代码
bool Insert(const pair<K, V>& 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->_right;
                }
                else if (cur->_kv.first > kv.first)
                {
                        parent = cur;
                        cur = cur->_left;
                }
                else
                {
                        return false;
                }
        }
        cur = new Node(kv);
        if (parent->_kv.first < kv.first)
        {
                parent->_right = cur;
        }
        else
        {
                parent->_left = cur;
        }
        cur->_parent = parent;
        // 更新平衡因⼦
        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)
                {
                        // 不平衡了,旋转处理
                        break;
                }
                else
                {
                        assert(false);
                }
        }
        return true;
}

2.3 旋转

2.3.1 旋转的原则

  1. 保持搜索树的规则:旋转只是调整结点的层次关系,不能破坏二叉搜索树中序遍历有序的性质,即左子树所有值 < 根 < 右子树所有值。
  2. 让旋转的树从不平衡变平衡,并降低旋转树的高度:旋转不仅要消除失衡,还要把这棵子树的高度降回插入之前的高度,从而保证旋转后不再影响上层结点。

按照失衡的方向和插入位置的不同,旋转总共分为四种:左单旋、右单旋、左右双旋、右左双旋。其中单旋用于处理"直线型"失衡,双旋用于处理"折线型"失衡。

2.3.2 右单旋

  • 本图1展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种。
  • 在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要往右边旋转,控制两棵树的平衡。
  • 旋转核⼼步骤,因为5<b⼦树的值<10,将b变成10的左⼦树,10变成5的右⼦树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

右单旋的代码实现要点如下,理解这几步就掌握了单旋的通用套路:

  • 先接好"内子树" :subL 是 parent 的左孩子,subLR 是 subL 的右孩子。旋转后 subLR 要挂到 parent 的左边,所以先执行 parent->_left = subLR,并注意 subLR 可能为空,必须判空后再改它的父指针。
  • 再完成主体旋转 :让 subL->_right = parent、parent->_parent = subL,此时 subL 成为新的子树根。
  • 最后处理上层链接 :用 parentParent 暂存 parent 原来的父亲。如果 parentParent 为空,说明 parent 原本就是整棵树的根,需要把 _root 更新为 subL;否则要判断 parent 原本是父结点的左孩子还是右孩子,把对应指针改指向 subL,并维护好 subL 的父指针。
  • 更新平衡因子 :旋转完成后,parent 与 subL 的平衡因子都归零,因为它们各自的左右子树已经等高。
cpp 复制代码
void  RotateR(Node* parent)
{
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        // 
需要注意除了要修改孩⼦指针指向,还是修改⽗亲
 
        parent->_left = subLR;
        if (subLR)
                subLR->_parent = parent;
        Node* parentParent = parent->_parent;
        subL->_right = parent;
        parent->_parent = subL;
        
        // parent
有可能是整棵树的根,也可能是局部的⼦树
 
        // 
如果是整棵树的根,要修改
_root 
        // 
如果是局部的指针要跟上⼀层链接
 
        if (parentParent == nullptr)
        {
                _root = subL;
                subL->_parent = nullptr;
        }
        else
        {
                if (parent == parentParent->_left)
                {
                        parentParent->_left = subL;
                }
                else
                {
                        parentParent->_right = subL;
                }
                subL->_parent = parentParent;
        }
        parent->_bf = subL->_bf = 0;
}

2.3.4左单旋

  • 本图展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。
  • 在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从1变成2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树右边太⾼了,需要往左边旋转,控制两棵树的平衡。
  • 旋转核⼼步骤,因为10<b⼦树的值<15,将b变成10的右⼦树,10变成15的左⼦树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

2.3.5左单旋代码实现

左单旋与右单旋完全对称,只是方向相反,实现要点同样分四步:

  • 接好内子树 :subR 是 parent 的右孩子,subRL 是 subR 的左孩子。旋转后 subRL 挂到 parent 的右边,即 parent->_right = subRL,同样要判空后再改父指针。
  • 完成主体旋转 :subR->_left = parent、parent->_parent = subR,subR 成为新的子树根。
  • 处理上层链接 :若 parentParent 为空,则 _root = subR;否则根据 parent 是左孩子还是右孩子,把父结点对应指针改指向 subR,并维护 subR 的父指针。
  • 更新平衡因子 :parent 与 subR 的平衡因子都置 0。

对比右单旋可以看出,两者代码结构一一对应,只是把 _left 与 _right 互换,掌握一个即可推出另一个。

cpp 复制代码
void RotateL(Node* parent)
{
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        parent->_right = subRL;
        if(subRL)
                subRL->_parent = parent;
        Node* parentParent = parent->_parent;
        subR->_left = parent;
        parent->_parent = subR;
        
        if (parentParent == nullptr)
        {
                _root = subR;
                subR->_parent = nullptr;
        }
        else
        {
                if (parent == parentParent->_left)
                {
                        parentParent->_left = subR;
                }
                else
                {
                        parentParent->_right = subR;
                }
                subR->_parent = parentParent;
        }
        parent->_bf = subR->_bf = 0;
}

2.3.6左右双旋

左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的⼦树不再是单纯的左边⾼,对于10是左边⾼,但是对于5是右边⾼,需要⽤两次旋转才能解决,以5为旋转点进⾏⼀个左单旋,以10为旋转点进⾏⼀个右单旋,这棵树这棵树就平衡了。

2.3.7 左右双旋代码实现

左右双旋的代码实现有一个关键技巧:先记录 subLR 的平衡因子 bf,再执行两次单旋,最后根据 bf 的取值修正三个结点的平衡因子。

  • 旋转动作本身很简单:先对 parent->_left 做一次左单旋,再对 parent 做一次右单旋,即 RotateL(parent->_left); RotateR(parent);。
  • 难点在于旋转后平衡因子的修正,它取决于插入发生在 subLR 的哪一侧:
    • bf == 0:说明 subLR 就是新插入的结点,三个结点平衡因子全为 0。
    • bf == -1:新结点插在 subLR 的左子树,旋转后 parent->_bf = 1,其余为 0。
    • bf == 1:新结点插在 subLR 的右子树,旋转后 subL->_bf = -1,其余为 0。
  • 最后的 assert(false) 用于兜底,正常情况下 bf 只可能是 -1、0、1。
cpp 复制代码
void RotateLR(Node* parent)
{
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        int bf = subLR->_bf;
        RotateL(parent->_left);
        RotateR(parent);
        if (bf == 0)
        {
                subL->_bf = 0;
                subLR->_bf = 0;
                parent->_bf = 0;
        }
        else if (bf == -1)
        {
                subL->_bf = 0;
                subLR->_bf = 0;
                parent->_bf = 1;
        }
        else if(bf == 1)
        {
                subL->_bf = -1;
                subLR->_bf = 0;
                parent->_bf = 0;
        }
        else
        {
                assert(false);
        }
}

2.3.8 右左双旋

右左双旋是左右双旋的镜像,思路完全一致:先记录 subRL 的平衡因子 bf,再对 parent->_right 做右单旋、对 parent 做左单旋,最后按 bf 修正平衡因子。

  • 旋转动作:RotateR(parent->_right); RotateL(parent);。
  • 平衡因子修正按 bf 分三种情况:
    • bf == 0:subRL 即新插入结点,三个结点平衡因子全为 0。
    • bf == 1:新结点插在 subRL 的右子树,旋转后 parent->_bf = -1,其余为 0。
    • bf == -1:新结点插在 subRL 的左子树,旋转后 subR->_bf = 1,其余为 0。
  • 同样用 assert(false) 兜底非法取值。

至此四种旋转全部介绍完毕:单旋处理直线型失衡,双旋处理折线型失衡,而双旋的平衡因子修正正是整个 AVL 插入中最容易出错、也最需要重点掌握的地方。

cpp 复制代码
void RotateRL(Node* parent)
{
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        int bf = subRL->_bf;
        RotateR(parent->_right);
        RotateL(parent);
        if (bf == 0)
        {
                subR->_bf = 0;
                subRL->_bf = 0;
                parent->_bf = 0;
        }
        else if (bf == 1)
        {
                subR->_bf = 0;
                subRL->_bf = 0;
                parent->_bf = -1;
        }
        else if (bf == -1)
        {
                subR->_bf = 1;
                subRL->_bf = 0;
                parent->_bf = 0;
        }
        else
        {
                assert(false);
        }
}

2.4AVL树的查找

cpp 复制代码
Node* Find(const K& key)
{
        Node* cur = _root;
        while (cur)
        {
                if (cur->_kv.first < key)
                {
                        cur = cur->_right;
                }
                else if (cur->_kv.first > key)
                {
                        cur = cur->_left;
                }
                else
                {
                        return cur;
                }
        }
        return nullptr;
}

2.5AVL树平衡检测

我们实现的AVL树是否合格,我们通过检查左右⼦树⾼度差的的程序进⾏反向验证,同时检查⼀下结点的平衡因⼦更新是否出现了问题。

cpp 复制代码
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 _IsBalanceTree(Node* root)
{
    // 空树也是AVL树
 
    if (nullptr == root)
            return true;
    // 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
 
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;
    // 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
    // pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
 
    if (abs(diff) >= 2)
    {
            cout << root->_kv.first << "⾼度差异常" << endl;
            return false;
    }
    if (root->_bf != diff)
    {
            cout << root->_kv.first << "平衡因⼦异常" << endl;
            return false;
    }
    // pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
 
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
相关推荐
摇滚侠1 小时前
《大模型是怎样跑起来的(轻科技)》硬件篇 阅读笔记
人工智能·笔记·科技
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(116):Hybrid Memory——用互补策展与离线巩固构建长期记忆
论文阅读·人工智能·学习·开源·github
思想觉悟1 小时前
SEAndroid实战
android·linux·c++
驭渊的小故事1 小时前
LeetCode 算法题解:滑动窗口与数组合并的实战应用
算法·leetcode·职场和发展
一条泥憨鱼1 小时前
【Kafka】零基础学习教程
java·分布式·学习·kafka
.YM.Z1 小时前
优选算法——【双指针算法2】C++实现
开发语言·c++·算法
思想觉悟1 小时前
Aosp开发磨刀石之ADB速查
android·c++
小此方1 小时前
C++ 工程组件与基础设施(三)详解RPC服务的概念与安装,图片介绍Brpc服务器和客户端的搭建过程
服务器·c++·rpc
qq_406981032 小时前
什么是顶层 const什么是底层const
开发语言·c++