上节我们介绍了map / multimap / set / multiset这几个容器,它们有一个共同特点底层都是用二叉搜索树实现的

不过,二叉搜索树有个明显的缺陷——当插入的数据有序或接近有序时,树结构会退化成单支树(看起来就像链表一样),这样一来,查找效率就从 O(logN) 降到了 O(N)

所以,实际底层并没有直接使用普通的二叉搜索树,而是采用了平衡处理后的红黑树,确保各项操作都能保持较高的效率。


一.AVL树(高度平衡二叉搜索树)

1.概念:

我们都知道,二叉搜索树它虽然可以缩短查找的效率,但在这种如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率变得很低

分两种的结果:

  • 最优情况下,有 n 个结点的二叉搜索树为完全二叉树,查找效率为:O(log₂N)
  • 最坏情况下,有 n 个结点的二叉搜索树退化为单支树,查找效率为:O(N)

因此,AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,他们在1962年的论⽂《An algorithm for the organization of information》中发表了它。他们提出了一个概念平衡因子,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡,就像⼀个⻛向标⼀样

总结:AVL树是最早发明的自平衡二叉搜索树,其特点是任意节点的左右子树高度差(平衡因子)的绝对值不超过1,从而保证了树的高度始终维持在 O(logN),避免了二叉搜索树退化为链表的情况。


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

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

假如一棵二叉搜索树是高度平衡的,那么它就是 AVL 树。如果它有 n 个结点,对应的它其高度可保持在 O(log₂n),搜索时间复杂度 O(log₂n)。


思考⼀下为什么AVL树是⾼度平衡搜索二叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗?

画画图分析我们发现,不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。比如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法做到⾼度差是0

同时:AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 logN ,那么增删查改的效率也可以控制在 O(logN) ,相⽐⼆叉搜索树有了本质的提升。


2.AVL 树节点的定义

AVL树的节点是一个三叉链结构,除了包含指向左右孩子的指针外,还多了一个指向其父节点的指针。数据域存储的是键值对,即 pair 对象。此外,每个节点还引入了一个平衡因子bf,即 balance factor),用来记录当前节点的平衡状态,判断是否需要进行旋转调整。

// AVL树节点的定义(KV模型)
template<class K, class V>
struct AVLTreeNode
{
    AVLTreeNode<T>* _left;   // 该节点的左孩子
    AVLTreeNode<T>* _right;  // 该节点的右孩子
    AVLTreeNode<T>* _parent; // 该节点的双亲指针
 
    pair<K, V> _kv;          // 键值对
    int _bf;                 // 平衡因子= 右子树高度-左子树高度
 
    // 构造函数
    AVLTreeNode(const pari<K, V>& kv)
        : _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _kv(kv)
        , _bf(0)
    {}
};
 
// AVL树的定义(KV模型)
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	// 成员函数
     ......
private:
	Node* _root;
}

3.AVL树的插入

AVL树插⼊⼀个值的大概过程:

1. 插入⼀个值按二叉搜索树规则进行插入。
2. 新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦所以更新从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可以停⽌了,具体情况我们下⾯再详细分析
3. 更新平衡因子过程中没有出现问题,则插⼊结束
4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上⼀层,所以插入结束

// 插入节点
bool Insert(const pair<K, V>& kv)
{
    // 第一步:如果树为空,直接插入根节点
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    //第二步:寻找适合插入的空位置
    Node* parent = nullptr; // 记录当前节点的父亲(插入后需要连接)
    Node* cur = _root;      // 记录当前节点

    while (cur) // cur 为空时,说明找到了插入位置
    {
        if (kv.first > cur->_kv.first) // 插入的 key 大于当前节点就往右走
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (kv.first < cur->_kv.first) // 插入的 key 小于当前节点就往左走
        {
            parent = cur;
            cur = cur->_left;
        }
        else // 说明key已存在AVL树不允许重复键
        {
            return false;
        }
    }

    // 第三步:插入新节点
    cur = new Node(kv); // 申请新节点

    // 判断新节点是 parent 的左孩子还是右孩子
    if (cur->_kv.first > parent->_kv.first)
    {
        parent->_right = cur; // 插入到右子树
    }
    else
    {
        parent->_left = cur;  // 插入到左子树
    }
    cur->_parent = parent;    // 设置新节点的父指针(三叉链需要)

    //第四步:控制平衡(更新平衡因子 + 旋转调整)

    // 1、更新平衡因子(沿插入路径向上更新)
    // 2、根据平衡因子判断是否需要旋转
    // 3、旋转后调整相关节点的平衡因子

  ......//后面我们详细介绍

    return true;
}

更新平衡因子:

(1)插入新节点cur后,其父节点parent的平衡因子一定会发生改变,因此必须进行更新。在插入之前,parent的平衡因子只能是-101这三种情况之一。

具体更新规则如下:

  • 如果cur插入在新节点父亲(parent) 的左侧,说明左子树高度增加了,父亲(parent) 的平衡因子减1

  • 如果cur插入在新节点父亲(parent) 的右侧,说明右子树高度增加了,父亲(parent) 的平衡因子加1


(2)新节点父亲的平衡因子更新以后,又会分为 3 种情况:parent的平衡因子可能有三种情况:0,正负 1, 正负 2。

1.如果更新完成后,parent 的平衡因子变为 0,说明插入之前 parent 的平衡因子一定是 +1 或者 -1。新节点补到了原本更矮的那一侧子树,抹平了左右子树的高度差,以该 parent 为根的子树整体高度没有发生变化,上层祖先不会受到本次插入的影响,AVL 性质维持,插入成功,不需要继续向上更新祖先的平衡因子


2.如果更新完成后,parent 的平衡因子变为 +1 或者 -1,说明插入之前 parent 的平衡因子一定为 0。本次插入使得以该 parent 为根的子树整体高度增加,高度变化会向上传递,因此需要继续向上更新祖先节点的平衡因子。最坏情况下,回溯更新会一直执行到整棵树的根节点。

3.如果更新完成后,parent 的平衡因子变为 +2 或者 -2,说明以该 parent 为根的子树已经违反 AVL 平衡条件,此时需要根据其子节点的平衡因子判断不平衡类型,执行对应的旋转操作。旋转修复完成后,该子树高度复原,不需要再继续向上更新祖先节点


// 插入节点
bool Insert(const pair<K, V>& kv)
{
    //控制平衡:更新平衡因子 
    
    while (parent)  // 最坏情况:一路更新到根节点
    {
       
        // 步骤1:更新父节点的平衡因子
        if (cur == parent->_left)   // 新节点插在父亲左边那么左子树变高
            parent->_bf--;
        else                        // 新节点插在父亲右边那么右子树变高
            parent->_bf++;

        // 步骤2:根据更新后的平衡因子,判断是否需要继续调整
        //情况1:平衡因子变为0
        if (0 == parent->_bf)
        {
            // 说明插入前 parent 的平衡因子是 1 或 -1
            // 插入后变为 0,意味着子树高度没有发生变化
            // 因此不需要继续向上更新
            break;
        }

        //情况2:平衡因子变为 1 或 -1
        else if (abs(parent->_bf) == 1)
        {
            // 说明插入前 parent 的平衡因子是 0
            // 插入后变为 1 或 -1,意味着子树高度增加了
            // 需要继续向上更新祖先节点的平衡因子
            cur = parent;
            parent = cur->_parent;
        }

        //情况3:平衡因子变为 2 或 -2
        else if (abs(parent->_bf) == 2)
        {
            // 说明插入后 parent 已经失衡,需要进行旋转调整

            // 1、左单旋:父节点右边高(2),且右孩子右边也高(1)
            // 用到插入节点在较高右子树的右侧
            if (parent->_bf == 2 && cur->_bf == 1)
            {
                RotateL(parent);
            }

            // 2、右单旋:父节点左边高(-2),且左孩子左边也高(-1)
            //用到插入节点在较高左子树的左侧
            else if (parent->_bf == -2 && cur->_bf == -1)
            {
                RotateR(parent);
            }

            // 3、左右双旋:父节点左边高(-2),但左孩子的右边高(1)
            // 用到插入节点在较高左子树的右侧
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                RotateLR(parent);
            }

            // 4、右左双旋:父节点右边高(2),但右孩子的左边高(-1)
            //用到插入节点在较高右子树的左侧
            else if (parent->_bf == 2 && cur->_bf == -1)
            {
                RotateRL(parent);
            }

            break;  // 旋转完成后树已平衡,退出循环
        }

        //情况4:出现其他值(理论上不可能)
        else
        {
            assert(false);  // 如果走到这里,说明平衡因子有 bug
        }
    }

    return true;
}

4.AVL树的旋转

如果在一棵原本平衡的 AVL 树中插入一个新节点,可能会导致某些节点的平衡因子变为 2 或 -2,从而破坏了树的平衡性。此时必须对树的结构进行调整,使其重新达到平衡状态,这个调整过程称为旋转

根据新节点插入位置的不同,AVL 树的旋转分为四种:左单旋右单旋左右双旋右左双旋。旋转的本质是:在遵循二叉搜索树规则的前提下,让左右子树高度均衡,从而降低整棵树的高度


如何判断旋转类型

观察失衡结点到新插入结点的路径形态:

如果路径为直线:失衡结点、孩子、孙子三代结点偏向同一侧(LL、RR),执行单旋转

如果路径为折线:失衡结点、孩子、孙子三代结点左右方向相反(LR、RL),执行双旋转

出现失衡的这部分,既可以是整棵 AVL 树,也可以是大树中的某一棵局部子树,旋转操作只在该局部范围内完成。


a.新节点插入较高左子树的左侧 —— 左左(LL):右单旋

我们将新的节点插入到了 parent 左孩子的左子树上,导致的不平衡的情况。

在插入前,AVL 树处于平衡状态。新节点插入到节点 30 的左子树中(注意:此处不一定是直接插入到 30 的左孩子位置,只要是插入到 30 的左子树中即可)。这样一来,30 的左子树增加了一层,导致以 60 为根的子树不再平衡。

为了让 60 恢复平衡,需要将 60 的左子树高度降低一层,同时右子树高度增加一层,也就是将左子树向上提,60 自然往下转。

由于 60 > 30,60 只能放在 30 的右子树位置。如果 30 原本有右子树(记为 subLR),那么 subLR 中的所有节点值一定大于 30 且小于 60,所以只能将它放在 60 的左子树位置。

旋转完成后,更新相关节点的平衡因子即可。


右单旋的触发条件

  • parent 的平衡因子为 -2(左边高)

  • parent 左孩子的平衡因子为 -1

观察发现,两个平衡因子同为负数,说明整条路径都是左边高,路径是一条直线,因此只需要做右单旋即可恢复平衡。

给大家一个小小的判断口诀:平衡因子同为负 ---> 左边高 --> 路径是直线 --> 右单旋


右单旋操作步骤:

第一步:让 subL 的右子树 subLR 成为 parent 的左子树

因为 subLR 中所有节点的值都大于 30 且小于 60,放在 60 的左边是符合 BST 规则的。

第二步:让 parent 成为 subL 的右子树

因为 60 大于 30,放在 30 的右边符合 BST 规则。

第三步:让 subL 成为这棵子树的根节点

此时需要考虑 parent 是整棵树的根节点,还是一棵子树:

  • 如果是整棵树的根节点,旋转完成后直接将 _root 更新为 subL

  • 如果是某棵子树,需要判断 parent 是其父节点的左孩子还是右孩子,然后将对应的指针指向 subL

第四步更新 parent 和 subL 的平衡因子为 0


旋转过程中的注意事项

在旋转过程中,有几个关于父指针更新的细节需要留意:

一是 subLR 是否存在的问题。
subL 的右孩子 subLR 可能有,也可能没有。只有当它存在时,才需要将其父指针指向 parent,如果为空则不用管。

二是 parent 原来是根还是子树。
旋转完成后,subL 要顶替 parent 的位置。这里需要判断一下 parent 是整棵树的根节点,还是某棵子树的根:

  • 如果是整棵树的根,旋转后 subL 成为新根,父指针置空即可;

  • 如果只是某棵子树的根,则需要先确定 parent 是父节点的左孩子还是右孩子,然后把父节点对应的指针指向 subL

进行调整 subLR、parent、subL 的位置和双亲指针的指向。 

// 右单旋
void _RotateR(Node* parent)
{  
    //1. 保存关键节点
    Node* subL = parent->_left;      // subL: parent的左孩子(旋转后将成为新根)
    Node* subLR = subL->_right;      // subLR: subL的右孩子(旋转后将成为parent的左子树)

    //2. 将subLR交给parent做左子树
    // 因为subLR中所有节点值 > subL && < parent,放在parent左边符合BST规则
    parent->_left = subLR;
    // 如果subLR存在,更新其父指针指向parent
    if (subLR)
    {
        subLR->_parent = parent;
    }

    //3. 保存parent的原父节点 
    // 因为parent可能是某棵子树的根,旋转后需要将新的子树根subL与上层连接
    Node* ppNode = parent->_parent;

    // 4. 让parent成为subL的右子树
    // parent > subL,放在subL右边符合BST规则
    subL->_right = parent;
    parent->_parent = subL;  // 更新parent的父指针指向subL

    //5. 让subL成为当前子树的根
    if (_root == parent)  // 情况A: parent是整棵树的根节点
    {
        _root = subL;            // subL成为新的根
        subL->_parent = nullptr; // 新根父指针置空
    }
    else  // 情况B: parent只是一棵子树的根
    {
        // 判断parent原是左孩子还是右孩子,用subL替换上去
        if (ppNode->_left == parent)
        {
            ppNode->_left = subL;
        }
        else
        {
            ppNode->_right = subL;
        }
        subL->_parent = ppNode;  // subL接管parent原来的父指针
    }

    //6. 更新平衡因子
    // 右单旋后,parent和subL的左右子树高度相等,平衡因子都变为0
    parent->_bf = 0;
    subL->_bf = 0;
}

b.新节点插入较高右子树的右侧 —— 右右(RR):左单旋

在插入前,AVL 树处于平衡状态。新节点插入到节点 60 的右子树中,导致 60 的右子树增加了一层,使得以 30 为根的子树不再平衡。

为了让 30 恢复平衡,需要将 30 的右子树高度降低一层,左子树高度增加一层,也就是将右子树往上提,30 自然往下转。

由于 30 < 60,30 只能放在 60 的左子树位置。如果 60 原本有左子树(记为 subRL),那么 subRL 中所有节点的值一定大于 30 且小于 60,所以只能将它放在 30 的右子树位置。

旋转完成后,更新相关节点的平衡因子即可。


左单旋的触发条件

  • parent 的平衡因子为 2(右边高)

  • parent 右孩子的平衡因子为 1

两个平衡因子同为正数,说明整条路径都是右边高,路径是一条直线,因此只需要做左单旋即可恢复平衡。

判断口诀:平衡因子同为正 --> 右边高 --> 路径是直线 --> 左单旋


左单旋操作步骤

第一步让 subR 的左子树 subRL 成为 parent 的右子树

因为 subRL 中所有节点的值都大于 30 且小于 60,放在 30 的右边是符合 BST 规则的。

第二步让 parent 成为 subR 的左子树

因为 30 小于 60,放在 60 的左边符合 BST 规则。

第三步让 subR 成为这棵子树的根节点

此时需要考虑 parent 是整棵树的根节点,还是一棵子树:

  • 如果是整棵树的根节点,旋转完成后直接将 _root 更新为 subR

  • 如果是某棵子树,需要判断 parent 是其父节点的左孩子还是右孩子,然后将对应的指针指向 subR

第四步更新 parent 和 subR 的平衡因子为 0


旋转过程中的注意事项

在旋转过程中,需要更新节点之间的父指针指向,有几个地方要特别留意:

一是 subRL 的父指针要不要改。
subR 的左孩子 subRL 可能存在,也可能为空,只有它存在时才需要把它的父指针指向 parent,空节点就不管了。

二是 parent 的父指针要指向 subR
旋转后 parent 变成了 subR 的左孩子,它的父指针自然要跟着改过去。

三是新根 subR 的父指针指向谁。
旋转完成后,subR 顶替了原来的 parent 成为这棵子树的根。此时得看 parent 原来是整棵树的根,还是只是一棵子树的根:

  • 是整棵树根的话,subR 就成了新根,父指针置空;

  • 是子树根的话,subR 的父指针就要指向 parent 原来的父节点,同时还得判断原来的 parent 是左孩子还是右孩子,把对应的指针指向 subR


进行调整 subRL、parent、subR 的位置和双亲指针的指向。

// 左单旋
void treeRotateLeft(Node* parent)
{
    //1. 保存关键节点
    Node* subR = parent->_right;      // subR:parent 的右孩子(旋转后将成为新根)
    Node* subRL = subR->_left;        // subRL:subR 的左孩子(值介于 parent 和 subR 之间)

    // 2. 将 subRL 交给 parent 做右子树
    // 因为 subRL 中所有节点值 > parent && < subR,放在 parent 右边符合 BST 规则
    parent->_right = subRL;
    // 如果 subRL 存在,更新其父指针指向 parent
    if (subRL)
    {
        subRL->_parent = parent;
    }

    //3. 保存 parent 的原父节点
    // 因为 parent 可能是某棵子树的根,旋转后需要将新的子树根 subR 与上层连接
    Node* ppNode = parent->_parent;

    //4. 让 parent 成为 subR 的左子树
    // parent < subR,放在 subR 左边符合 BST 规则
    subR->_left = parent;
    // 更新 parent 的父指针指向 subR
    parent->_parent = subR;

    // 5. 让 subR 成为当前子树的根
    if (parent == _root)  // 情况A:parent 是整棵树的根节点
    {
        _root = subR;            // subR 成为新的根
        subR->_parent = nullptr; // 新根父指针置空
    }
    else  // 情况B:parent 只是一棵子树的根
    {
        // 判断 parent 原是左孩子还是右孩子,用 subR 替换上去
        if (ppNode->_left == parent)
        {
            ppNode->_left = subR;
        }
        else
        {
            ppNode->_right = subR;
        }
        subR->_parent = ppNode;  // subR 接管 parent 原来的父指针
    }

    //6. 更新平衡因子
    // 左单旋后,parent 和 subR 的左右子树高度相等,平衡因子都变为 0
    parent->_bf = 0;
    subR->_bf = 0;
}

c.新节点插入较高左子树的右侧 —— 左右(LR):先左单旋再右单旋(左右双旋)

将新的节点插入到了 parent 左孩子的右子树上,导致的不平衡的情况。这时我们需要的是先对 parent 的右孩子进行一次左旋,再对 parent 进行一次右旋

这里我们将双旋变成单旋后再旋转,也就是:先对 30 进行左单旋,然后再对 90 进行右单旋,旋转完成后再考虑平衡因子的更新

旋转之前,60 的平衡因子可能是 -1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进行调整。


h==0

双旋的触发条件

判断旋转类型的一个简单规律:引发旋转的路径是直线就单旋,是折线就双旋。

当 parent 的平衡因子为 -2,且 parent 左孩子的平衡因子为 1 时,两个平衡因子一负一正,说明左孩子的右子树高,而 parent 的左子树也高,路径呈现折线形状,此时需要先对左孩子进行左旋,再对 parent 进行右旋,也就是左右双旋


左右双旋的平衡因子调整

左右双旋完成后,需要根据树的结构更新平衡因子。由于新节点插入位置的不同,旋转后各节点的平衡因子也会有所差异,具体有以下三种情况:

  1. 新节点插入在 parent 左孩子的右子树的左边

  2. 新节点插入在 parent 左孩子的右子树的右边

  3. 新节点本身就是 parent 左孩子的右孩子(即 subLR 为空,该节点就是新增的)


规律说明

观察左右双旋后的结构可以发现:节点 subLR 的左右子树被分走了 —— 左子树最终成为了 subL 的右子树,右子树最终成了 parent 的左子树。根据新节点插入在 subLR 的左侧还是右侧,左右两棵子树的高度会有所不同,从而影响 parent 和 subL 的平衡因子,而 subLR 的平衡因子最终一定为 0

节点 60 的左右子树被分走了,左子树最终成为了节点 30 的右子树,右子树最终成了节点 90 的左子树。

void _RotateLR(Node* parent)
{
    //  1. 保存关键节点
    Node* subL = parent->_left;      // subL: parent的左孩子
    Node* subLR = subL->_right;      // subLR: subL的右孩子(旋转的"折点")

    // 旋转之前,由于插入新节点的位置不同,subLR的平衡因子可能为 -1/0/1
    // 记录下这个值,旋转完成后需要根据它来调整各节点的平衡因子
    int bf = subLR->_bf;

    //2. 分两步旋转
    // 第一步:先对parent的左孩子进行左单旋(让折线变直线)
    RotateL(parent->_left);
    // 第二步:再对parent进行右单旋(恢复平衡)
    RotateR(parent);

    // 3. 根据bf调整平衡因子
    // 旋转完成后,subLR成为新的根,其平衡因子一定为0
    subLR->_bf = 0;

    if (bf == -1)
    {
        // 新节点插入在subLR的左侧
        parent->_bf = 1;   // parent右子树偏高
        subL->_bf = 0;
    }
    else if (bf == 1)
    {
        // 新节点插入在subLR的右侧
        parent->_bf = 0;
        subL->_bf = -1;    // subL左子树偏高
    }
    else if (bf == 0)
    {
        // subLR本身就是新插入的节点
        parent->_bf = 0;
        subL->_bf = 0;
    }
    else
    {
        // 其他值说明出了问题
        assert(false);
    }
}

d.新节点插入较高右子树的左侧 —— 右左(RL):先右单旋再左单旋(右左双旋)

这里我们将新的节点插入到了 parent 右孩子的左子树上,导致的不平衡的情况。这时我们需要的是先对 parent 的右孩子进行一次右旋,再对 parent 进行一次左旋。


当h == 1时

右左双旋的触发条件

判断旋转类型有个简单的规律:引发旋转的路径是直线就单旋,是折线就双旋。

当 parent 的平衡因子为 2,且 parent 右孩子的平衡因子为 -1 时,两个平衡因子一正一负,说明右孩子的左子树高,而 parent 的右子树也高,路径呈现折线形状,所以需要先对右孩子进行右旋,再对 parent 进行左旋,这就是右左双旋

给大家一个判断口诀:平衡因子一正一负 --> 路径是折线 --> 双旋


右左双旋的平衡因子调整

右左双旋完成后,需要根据树的结构更新平衡因子。由于新节点插入位置的不同,旋转后各节点的平衡因子也会有所差异,具体有以下三种情况:

  1. 新节点插入在 parent 右孩子的左子树的左侧

  2. 新节点插入在 parent 右孩子的左子树的右侧

  3. 新节点本身就是 parent 右孩子的左孩子


规律说明

观察右左双旋后的结构可以发现:节点 subRL 的左右子树被分走了 —— 左子树最终成为了 parent 的右子树,右子树最终成了 subR 的左子树。根据新节点插入在 subRL 的左侧还是右侧,左右两棵子树的高度会有所不同,从而影响 parent 和 subR 的平衡因子,而 subRL 的平衡因子最终一定为 0

节点 60 的左右子树被分走了,左子树 b 最终成了节点 30 的右子树,右子树 c 最终成了节点 90 的左子树。

// 右左双旋
void treeRotateRL(Node* parent)
{
    // 1. 保存关键节点
    Node* subR = parent->_right;      // subR: parent的右孩子
    Node* subRL = subR->_left;        // subRL: subR的左孩子(旋转的折点)

    // 旋转之前,由于插入新节点的位置不同,subRL的平衡因子可能为 -1/0/1
    // 记录下这个值,旋转完成后需要根据它来调整各节点的平衡因子
    int bf = subRL->_bf;

    // 2. 分两步旋转
    // 第一步:先对parent的右孩子进行右单旋(让折线变直线)
    RotateR(parent->_right);
    // 第二步:再对parent进行左单旋(恢复平衡)
    RotateL(parent);

    // 3. 根据bf调整平衡因子
    // 旋转完成后,subRL成为新的根,其平衡因子一定为0
    subRL->_bf = 0;

    if (bf == -1)
    {
        // 新节点插入在subRL的左侧
        parent->_bf = 0;      // parent左右平衡
        subR->_bf = 1;        // subR右子树偏高
    }
    else if (bf == 1)
    {
        // 新节点插入在subRL的右侧
        parent->_bf = -1;     // parent左子树偏高
        subR->_bf = 0;        // subR左右平衡
    }
    else if (bf == 0)
    {
        // subRL本身就是新插入的节点
        parent->_bf = 0;
        subR->_bf = 0;
    }
    else
    {
        // 其他值说明出了问题
        assert(false);
    }
}

AVL树旋转总结

当以 parent 为根的子树失衡时,即 parent 的平衡因子为 2 或 -2,根据具体情况采取以下旋转方式:

1.parent 的平衡因子为 2(右子树高)

此时 parent 的右子树根为 subR,根据 subR 的平衡因子决定旋转方式:

  • subR 的平衡因子为 1 → 路径为直线(右右型) → 执行左单旋

  • subR 的平衡因子为 -1 → 路径为折线(右左型) → 执行右左双旋

2.parent 的平衡因子为 -2(左子树高)

此时 parent 的左子树根为 subL,根据 subL 的平衡因子决定旋转方式:

  • subL 的平衡因子为 -1 → 路径为直线(左左型) → 执行右单旋

  • subL 的平衡因子为 1 → 路径为折线(左右型) → 执行左右双旋

3.旋转完成后

经过旋转调整后,以 parent 为根的子树恢复了平衡,整棵子树的高度相比插入前降低了一层,因此不需要继续向上更新平衡因子,旋转操作到此结束。


5.VL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性限制,因此验证一棵树是否为AVL树,需要从以下两个方面进行检查:

1.验证其为二叉搜索树

根据二叉搜索树的性质,其中序遍历序列一定是有序的(升序)。因此,对树进行中序遍历,如果得到的序列是严格升序的,则说明它满足二叉搜索树的性质。

2.验证其为平衡树

二叉搜索树只能保证有序,还需要进一步验证是否满足AVL树的平衡要求,主要检查以下两点:

  • 平衡因子是否准确:每个节点的平衡因子 = 右子树高度 - 左子树高度,需要验证计算是否正确

  • 高度差是否超标:每个节点左右子树的高度差绝对值不超过 1


(1)计算树的高度

// 计算当前树的高度
int Height(Node* root)
{
    // 当前树为空,则高度为0
    if (root == nullptr)
        return 0;
 
    // 当前树的高度 = 左右子树中高度最大的那个加1
    return max(Height(root->_left), Height(root->_right)) + 1;
}

(2)思路一:自顶向下的暴力解法

每次递归都重新计算子树高度,存在大量重复计算,效率较低。

bool IsBalance1()
{
    return _IsBalance(_root);
}
 
bool _IsBalance1(Node* root)
{
    // 当前树为空,说明是平衡的
    if (root == nullptr)
        return true;
 
    // 当前树不为空,计算左右子树的高度
    int leftHT = Height(root->_left);
    int rightHT = Height(root->_right);
    int diff = rightHT - leftHT;
 
    if (diff != root->_bf) // 检查当前树的平衡因子是否计算正确
    {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }
 
    // 左右子树高度相减的绝对值小于2,说明当前树是平衡的,则继续往下判断其它子树
    return abs(diff) < 2
        && _IsBalance(root->_left)
        && _IsBalance(root->_right);
}

(3)思路二:自底向上的高效解法(动态规划)

后序遍历,先判断子树是否平衡,同时返回子树高度供上一层使用,避免重复计算。

bool IsBalance2()
{
    return _IsBalance2(_root) != -1;
}
 
int _IsBalance2(Node* root)
{
    // 先判断当前树的左、右子树是否平衡,再判断当前树是否平衡
    // 不平衡返回-1,平衡则返回当前树的高度
 
    // 当前树为空,返回高度0
    if (root == nullptr)
        return 0;
 
    // 当前树不为空,分别计算左右子树的高度
    int leftHeight = _IsBalance2(root->_left);
    int rightHeight = _IsBalance2(root->_right);
    int diff = rightHeight - leftHeight;
    
    if (diff != root->_bf) // 检查当前树的平衡因子是否计算正确
    {
        cout << "平衡因子异常:" << root->_kv.first << endl;
    }
    
    // 左子树高度等于-1、右子树高度等于-1、左右子树高度差的绝对值大于1,说明当前树不平衡
    if (leftHeight == -1 || rightHeight == -1 || abs(diff) > 1)
        return -1;
 
    // 运行到这里来了,说明当前树是平衡的,返回当前树的高度
    return max(leftHeight, rightHeight) + 1;
}

(4)思路三:另一种自顶向下写法

与思路一逻辑类似,但将判断条件合并在一起。

bool _IsBalanceTree3(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 (diff != root->_bf || (diff > 1 || diff < -1))
        return false;
 
    // pRoot的左和右如果都是AVL树,则该树一定是AVL树
    return _IsBalanceTree3(root->_left) && _IsBalanceTree3(root->_right);
}
#include <iostream>
using namespace std;

// AVL树节点
template<class K, class V>
struct AVLNode
{
    pair<K, V> _kv;
    AVLNode* _left;
    AVLNode* _right;
    AVLNode* _parent;
    int _bf;

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

// AVL树
template<class K, class V>
class AVLTree
{
    typedef AVLNode<K, V> Node;

public:
    AVLTree() : _root(nullptr) {}

    // 插入
    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 (kv.first > cur->_kv.first)
            {
                parent = cur;
                cur = cur->_right;
            }
            else if (kv.first < cur->_kv.first)
            {
                parent = cur;
                cur = cur->_left;
            }
            else
            {
                return false;
            }
        }

        cur = new Node(kv);
        if (kv.first > parent->_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;

            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)
                    RotateL(parent);
                // 右旋
                else if (parent->_bf == -2 && cur->_bf == -1)
                    RotateR(parent);
                // 左右双旋
                else if (parent->_bf == -2 && cur->_bf == 1)
                    RotateLR(parent);
                // 右左双旋
                else if (parent->_bf == 2 && cur->_bf == -1)
                    RotateRL(parent);

                break;
            }
        }
        return true;
    }

    // 中序遍历
    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }

    // 验证平衡
    bool IsBalance()
    {
        return _IsBalance(_root) != -1;
    }

private:
    Node* _root;

    void _InOrder(Node* root)
    {
        if (root == nullptr)
            return;
        _InOrder(root->_left);
        cout << root->_kv.first << " ";
        _InOrder(root->_right);
    }

    int _IsBalance(Node* root)
    {
        if (root == nullptr)
            return 0;

        int leftH = _IsBalance(root->_left);
        int rightH = _IsBalance(root->_right);
        int diff = rightH - leftH;

        if (diff != root->_bf || abs(diff) > 1)
            return -1;

        return max(leftH, rightH) + 1;
    }

    // 旋转

    void RotateL(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 (parent == _root)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            if (ppNode->_left == parent)
                ppNode->_left = subR;
            else
                ppNode->_right = subR;
            subR->_parent = ppNode;
        }

        parent->_bf = 0;
        subR->_bf = 0;
    }

    void RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;

        parent->_left = subLR;
        if (subLR)
            subLR->_parent = parent;

        Node* ppNode = parent->_parent;
        subL->_right = parent;
        parent->_parent = subL;

        if (parent == _root)
        {
            _root = subL;
            subL->_parent = nullptr;
        }
        else
        {
            if (ppNode->_left == parent)
                ppNode->_left = subL;
            else
                ppNode->_right = subL;
            subL->_parent = ppNode;
        }

        parent->_bf = 0;
        subL->_bf = 0;
    }

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

        RotateL(parent->_left);
        RotateR(parent);

        subLR->_bf = 0;
        if (bf == -1)
        {
            parent->_bf = 1;
            subL->_bf = 0;
        }
        else if (bf == 1)
        {
            parent->_bf = 0;
            subL->_bf = -1;
        }
        else
        {
            parent->_bf = 0;
            subL->_bf = 0;
        }
    }

    void RotateRL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        int bf = subRL->_bf;

        RotateR(parent->_right);
        RotateL(parent);

        subRL->_bf = 0;
        if (bf == -1)
        {
            parent->_bf = 0;
            subR->_bf = 1;
        }
        else if (bf == 1)
        {
            parent->_bf = -1;
            subR->_bf = 0;
        }
        else
        {
            parent->_bf = 0;
            subR->_bf = 0;
        }
    }
};


int main()
{
    cout << "常规场景 {16,3,7,11,9,26,18,14,15}" << endl;
    AVLTree<int, int> t;
    int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };

    for (int e : arr)
    {
        t.Insert({ e, e });
        cout << "插入 " << e << " --> ";
        t.InOrder();
    }

    cout << "\n是否平衡: " << (t.IsBalance() ? " 是" : " 否") << endl;

    cout << "\n特殊场景 {4,2,6,1,3,5,15,7,16,14}" << endl;
    AVLTree<int, int> t2;
    int arr2[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

    for (int e : arr2)
    {
        t2.Insert({ e, e });
        cout << "插入 " << e << " --> ";
        t2.InOrder();
    }

    cout << "\n是否平衡: " << (t2.IsBalance() ? " 是" : " 否") << endl;

    return 0;
}


6.AVL树的删除(了解) 

这里我们只是简单说一下大致过程,因为 AVL 树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。具体实现可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。


7.AVL 树的性能

AVL树是一棵绝对平衡的二叉搜索树,每个节点的左右子树高度差不超过1,所以查找效率很高,稳定在 O(logN)

但它的缺点也很明显:维护平衡的成本太高

插入数据时,可能需要多次旋转才能恢复平衡。删除数据时更麻烦,有时甚至需要一路旋转到根节点,性能开销比较大。

所以,AVL树适合读多写少的场景。如果数据是固定的,建好后只查不改,AVL树很合适。但如果是频繁插入删除的情况,就不太划算了,这时候可以考虑红黑树

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐