上节我们介绍了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的平衡因子只能是-1、0、1这三种情况之一。

具体更新规则如下:

  • 如果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社区

更多推荐