July.cc Blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


数据结构 2022 年 10 月 11 日

[数据结构] AVL-Tree平衡二叉搜索树的相关分析及实现

AVL树 是最早被设计出来的平衡二叉搜索树
上一篇文章介绍了 map multimap set multiset,并且提到过 这些容器的 底层都是 红黑树 实现的
之前介绍过 什么是二叉搜索树,但是二叉搜索树有一个缺点就是 在特定的情况下插入数据,可能会创建一个单边树。这也是为什么setmap不使用二叉搜索树,而是用红黑树的原因。
红黑树是平衡二叉搜索树的一种。本篇文章介绍的数据结构 也是一种平衡二叉搜索树,但并不是 红黑树 而是 AVL树
AVL树 是最早被设计出来的平衡二叉搜索树

平衡二叉搜索树

关于 平衡二叉搜索树,除了它是二叉搜索树之外,最重要的特点莫过于 平衡
对二叉树而言 怎么样才算平衡?
平衡树:任意节点的子树的高度差的绝对值都小于等于 1

任意节点的子树高度差的绝对值都 <= 1,这句话是什么意思?

首先要明白 什么是节点的子树的高度差:

  • 以上面这棵二叉树为例:

  1. 对于 3 节点,其左子树的高度为 3,右子树的高度为 2。则可以说,3 节点的子树的高度差 为 -1
  2. 对于 5 节点,其左子树的高度为 1,右子树的高度为 2。则可以说,5 节点的子树的高度差 为 1
  3. 对于 2 节点,其左子树的高度为 1,右子树的高度为 1。则可以说,2 节点的子树的高度差 为 0

也就是说,节点的子树的高度差,可以根绝节点的左右子树的高度来计算,且计算时 左子树高度为负数,右子树高度为正数

那么可以计算出上面这棵树的所有结点的高度差:

  • 3:-1; 5:1; 6:0; 2:0; 7:0; 4:0; 1:-1; 0:0;

所有结点的子树高度差的绝对值都没有大于 1,所以可以说上面这棵树是一颗平衡树

那么 平衡二叉搜索树,就是在此树 满足二叉搜索树的结构规则的前提 下,还要满足平衡
即,一棵树同时满足这两个条件 就可以称此树是平衡二叉搜索树:
  1. 任意节点的子树的高度差的绝对值都小于等于 1
  2. 任意节点的左孩子恒小于此节点,右孩子恒大于此节点

AVL 树

1. AVL树 的概念

构建二叉搜索树存在一个缺点是 如果数据有序或接近有序 构建二叉搜索树,构建完成时会发现此树为单支树,查找元素相当于在顺序表中查找,时间复杂度接近O(N)
两位俄罗斯的数学家 G.M.Adelson-VelskiiE.M.Landis 发明了一种解决上述问题的方法:插入数据建立二叉搜索树的同时,通过调整节点关系 使每个结点的左右子树高度差不超过 1,进而建立出平衡二叉搜索树 可降低树的高度,从而减少平均搜索长度
G.M.Adelson-VelskiiE.M.Landis 提出的此方法构建出的 平衡二叉搜索树,就被称为 AVL树

2. AVL树 节点的定义

AVL树的是一种三叉链结构,即 每个节点除左右孩子、数据之外,还存有父亲节点
为了方便得出每个节点的子树高度差,所以还可以存储一个变量来记录左右孩子的高度差,一般被称为 平衡因子
所以 AVL树 的节点结构 可以设计为:

当然 在节点内存储平衡因子并不是必须的,也可以通过其他方法记录

3. AVL树 插入数据

AVL树 是平衡二叉搜索树,建立的过程 是在 二叉搜索树的前提下,调整节点构建出来的。而树的构建过程, 其实也就是树节点插入的过程
那么 AVL树 执行插入操作之后, 也就需要保证 结果树依旧是一个 AVL树, 不过 在插入新节点之前需要保证 树已经是 AVL树
所以, AVL树插入数据的过程可以大致分为 两个步骤:
  1. 按照二叉搜索树的插入方式插入新节点
  2. 分析节点的子树高度差,并进行调整
下面也按照这两个步骤来对 AVL树的插入操作进行分析:
  1. 按照二叉搜索树的插入方式插入新节点

二叉搜索树的特点是:节点的左孩子比节点小, 节点的右孩子比节点大

所以 插入新节点就需要通过比较新节点与各个节点的大小, 找到合适的位置, 然后再将新节点连接到树中:

bool insert(const T& data) {
    // 首先按照 二叉搜索树的方式 查找插入位置并插入节点
    if (_root == nullptr) {
        // 树为空 插入节点 直接将新节点作为树的根
        _root = new Node(data);
        _root->_bf = 0;		// 只有根节点的树,根节点平衡因子为 0

        return true;		// 插入成功,直接返回
    }

    // 走到这里就说明需要 查找插入位置 了
    Node* cur = _root;	// 从根节点开始比较
    Node* parent = nullptr;	// 需要记录父亲节点 供插入时连接
    while (cur) {
        // 循环结束的条件是 cur为空,cur为空时就说明 插入位置找到了
        if (cur->_data > data) {
            // 插入值比当前节点值 小,则向左孩子找
            parent = cur;
            cur = cur->_pLeft;
        }
        else if (cur->_data < data) {
            // 插入值比当前节点值 大,则向右孩子找
            parent = cur;
            cur = cur->_pRight;
        }
        else {
            // 走到这里 说明数中已存在相同数据
            return false;
        }
    }

    // 出循环之后,cur 即为数据需要插入的位置
    cur = new Node(data);
    // 将cur与树连接起来
    if (data > parent->_data) {
        parent->_pRight = cur;		// 插入数据比父亲节点数据大,则插入到父亲节点的右孩子
    }
    else if (data < parent->_data) {
        parent->_pLeft = cur;			// 插入数据比父亲节点数据小,则插入到父亲节点的左孩子
    }
    // 三叉链结构,cur节点虚存储父亲节点
    cur->_pParent = parent;
}

二叉搜索树的插入已经轻车熟路了, 不过这并不是AVL树的重点, AVL树插入数据 最重要的、也是最难理解的部分 其实是: 插入数据之后, 对各个节点的平衡因子的分析, 以及对不平衡树的平衡操作

在树中插入新的结点之后, 很有可能会造成树的不平衡, 举几个简单的例子:
  1. 分析节点的子树高度差,并进行调整

插入之后, 存在不会失衡的情况

当然也有 使树失去平衡的情况:

其实 AVL树插入新节点之后, 一些节点的平衡因子一定会发生变化, 进而可能会对整棵树产生一定的影响

如果 因为插入新节点 导致某个节点的平衡因子的绝对值 >1, 那么就说明树不平衡了, 需要进行调整

不过 在实际的插入过程中, 插入新节点之后 首先要做的并不是调整树的平衡, 首先要做的是 调整新插入节点的祖先节点的平衡因子 因为, 插入新节点会改变其位置的祖先节点的平衡因子

那么 插入新节点之后首先要解决的问题就是:如何更新新节点的祖先节点的平衡因子?

其实并不难, 因为 AVL树的节点结构是 三叉链的结构, 所以可以 从新节点向上寻找父亲节点 来进行更新。但 实际上并不是每一个祖先节点都需要更新平衡因子

对下面 这个稍微复杂的 刚插入一个新节点的 AVL树进行分析:

  1. 如果在 绿色 位置插入, 则 31节点平衡因子 由 0 变为 -1, 进而 22节点平衡因子由 -1 变为 0, 不再向上影响, 停止更新
  2. 如果在 棕色 位置插入, 则 46节点平衡因子 由 0 变为 -1, 进而 41节点平衡因子由 1 变为 2, 以 41节点为根的树失衡, 需要进行调节 保持平衡
  3. 如果在 红色 位置插入, 则 77节点平衡因子 由 0 变为 -1, 进而 70节点平衡因子由 0 变为 1, 进而 82节点平衡因子由 -1 变为 -2, 以 82节点为根的树失衡, 需要进行调节 保持平衡

可以发现, 当

  1. 祖先节点平衡因子的变化 会影响 更上层的祖先节点的平衡因子时, 会继续向上对祖先节点进行调节
  2. 某祖先节点的平衡因子变为 2或-2, 导致以此节点为根的树失衡时, 需要停下对此树进行调节, 以保持平衡
  3. 存在 一直向上更新祖先节点的平衡因子, 直到更新到整棵树的根节点 的可能

根据这三种情况, 就可以分析出 如何向上更新祖先节点的平衡因子

  1. 首先要记录 当前节点 和 节点的父亲节点
  2. 如果当前节点 是 父亲节点的左孩子, 则父亲节点的平衡因子-1, 否则+1
  3. 当父亲节点的平衡因子 变为 1或-1 时, 还会影响上层的祖先节点, 所以需要继续向上更新
  4. 当父亲节点的平衡因子 变为 2或-2 时, 以此父亲节点为根的树失衡, 需要对此树进行调节
  5. 当父亲节点的平衡因子 变为 0 时, 不会继续影响上层祖先节点, 所以停止向上更新
  6. 因为 可能更新到整棵树的根节点, 所以循环需要设置到 根节点结束

经过分析, 可以更新祖先节点的平衡因子的操作可以这样写:

// cur 是插入后的新节点
cur->_pParent = parent;

while (parent) {
    if (cur == parent->_pLeft)
        parent->_bf--;			// 新节点在父亲节点的左孩子,则父亲节点的左子树高度+1,则父亲节点的平衡因子-1
    else
        parent->_bf++;

    // 更新完之后,就需要判断 需要继续更新 还是停止更新 或是调整平衡
    if (parent->_bf == 0) {
        // 不会再影响更上边的节点,可以结束
        break;
    }
    else if (parent->_bf == -1 || parent->_bf == 1) {
        // 可能会继续影响更高节点的平衡 所以需要更新parent 和 cur,进而继续更新祖先节点的平衡因子
        cur = cur->_pParent;
        parent = parent->_pParent;
    }
    else if (parent->_bf == -2 || parent->_bf == 2) {
        // 需要调整平衡了
    }
    else {
        // 以上情况都是在 保证树已经是AVL树时 插入新节点
        // 如果不是 则会走到此处 触发断言 进而发现错误
        assert(false);
    }
}

祖先节点的平衡因子更新完成之后, 就需要对失衡的树进行调节了

处理失衡的树, 我们采用的方法是:旋转


当 某节点的平衡因子的绝对值为2时, 需要对以此节点为根的树 进行旋转调平

即使 树根的平衡因子的绝对值为 2, 整棵树的也会存在不同的实际情况, 而针对不同的情况 需要进行的旋转操作也是不同的

左单旋

左单旋 处理的情况一般是这样的:

某 AVL树的根节点平衡因子为 1 (即此树右子树高度), 且又在此树的右子树中 插入新节点导致右子树高度再增加, 进而导致此树失衡

此时, 需要 左单旋 操作 将此树调平

究竟什么是 左单旋

要解释 左单旋 , 就需要对此种情况具体分析:

  1. h = 0

当 h = 0, 即说明 A、B、C树为空。此时 在 C 树中插入新节点 其实就是在 N节点的右孩子处 插入新节点

此时 树失衡, 需要 将树调平。根据 右孩子大 的特点, 假设 M=10 N=20 X=30

要怎么调节才能将树变得平衡?

在此树中 很简单, 只需要将树的结构这样变化:

三个节点都在, 且树平衡
三个节点都在, 且树平衡

好像只是 将 N(20)节点 的 父亲节点(M(10)节点) 变成了, N节点 的 左孩子

  1. h = 1

A、B、C树的高度为 1, 在 C 树中插入新节点:

树失衡, 需要调平。 此时又该怎么调整树的结构呢?

其实也很简单, 将 60节点的左孩子 变成 40节点的右孩子, 再将 40节点 作为 60节点的左子树, 让 60节点变为树的根, 将树变为这样:

60的左孩子变为其父亲节点的右孩子, 再将 60节点的父亲节点 变为 60节点的左孩子

  1. h = 2

A、B、C 树的高度是 2, 在 C 树中插入新节点

在分析 h = 1 时, 可以看到 新节点插入的位置 可以有两个, 那么 h = 2 时, 只会更多

(虚线, 表示其他可插入位置)
(虚线, 表示其他可插入位置)

A、B 树各 3 种情况, C树只能是第3种高度为2的二叉树, 则新节点存在 4 个可插入位置, 所以 h = 2 时, 共有 36 种结构

为什么 C树只能是第三种情况的树?

如果 C树 是前两种情况, 可能出现非左单旋处理的情况, 而我们此时讨论的只是需要左单旋处理的情况

此时 树不平衡, 需要调平。如何 调整?

稍微有些难想, 但是 根据之前的经验:

40节点的左子树 变为 其父亲节点的右子树, 再将 40节点的父亲节点 变为 40节点的左子树:

树结构平衡

  1. h = 3 ······

  2. ······

更复杂的情况还有, 但是分析到 h=2 应该就可以看出, 即使h再大, 对于

某 AVL树的根节点平衡因子为 1 (即此树右子树高), 且又在此树的右子树中 插入新节点导致右子树高度再增加, 进而导致此树失衡

的这种情况, 其实可以用相同的方法解决

对比一下, h不同时 插入新节点 导致树失衡 再到 平衡的过程:

可以发现, 如果将 平衡因子为2的节点 为 parent, 其右孩子节点 为 subR, 则此类情况的调整平衡的具体操作 其实是:

将subR的左孩子 变为 parent的右孩子, 再将 parent 变为 subR的左孩子

执行此操作之后, 树的结构就平衡了, 此操作也就是左单旋操作

但是如果只改变结构, 整棵树其实还是不正确的, 因为 只动了结构, 平衡因子还没有更新

而 观察可以发现, 左单旋操作之后, parentsubR 的 平衡因子变为了 0, 其他节点的平衡因子并没有改变

所以, 左单旋操作的 代码实现可以是:

void RotateL(Node* parent) {
	Node* subR = parent->_pRight;		// 即 不平衡节点的右孩子
	Node* subRL = subR->_pLeft;			// 不平衡节点的右孩子 的 左孩子

	/* parent 可能是 整棵树的根, 也可能是某节点的子树根
	* 而 由于AVL树的节点是三叉链的结构, 所以改变节点的位置 需要改变此节点的父亲节点, 所以
	* 当 parent 是整棵树的根时, 即parent->_pParent 为空, 那么左旋时 就需要直接将 subR改为整棵树的根
	* 当 parent 是某节点的子树时, 就需要将 parent->_pParent 与 subR 连接起来
	* 所以 需要将 parent->_pParent 存储起来
	*/
	Node* ppNode = parent->_pParent;

	// 不平衡节点的右孩子的左孩子 变为 父亲节点的右孩子, 并将 父亲节点 变为 此节点的左孩子
	// 并记得 链接三叉链
	parent->_pRight = subRL;
	if (subRL)
		subRL->_pParent = parent;

	subR->_pLeft = parent;
	parent->_pParent = subR;

	// 改变不平衡节点 的 父亲节点的指向
	if (parent == _root) {
		_root = subR;
		_root->_pParent = nullptr;
	}
	else {
		if (parent == ppNode->_pLeft)		// 不平衡节点是其父亲节点的左孩子
			ppNode->_pLeft = subR;			// 把 subR 连接到 其父亲节点的左孩子上
		else
			ppNode->_pRight = subR;			// 把 subR 连接到 其父亲节点的右孩子上

		subR->_pParent = ppNode;		// 更新 subR 的父亲节点
	}

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

注意:

  1. 三叉链的连接

AVL树的节点的结构是 三叉链结构, 除左右孩子指针之外 还存在一个存储父亲节点地址的 父亲节点指针

所以在调节平衡 改变节点的位置 或 关系的时候, 需要 特别注意 父亲节点的链接

  1. 不平衡节点的父亲节点为空时的处理

不平衡节点的父亲节点为空, 也就是说 平衡因子的绝对值为 2 的节点 其实就是整棵树的根

此时 需要单独处理, 因为是整颗树的根, 所以旋转之后 subR 节点应该变为整棵树的根

  1. 不平衡节点的右孩子的左孩子为空时的处理

因为需要三叉链需要链接父亲节点, 所以在左单旋中, 由于 subR节点 的左孩子变成了 parent节点的左孩子, 那么 subR节点的左孩子的父亲节点就需要变为 parent节点, 但是如果 subR节点的左孩子为空, 就不能链接, 因为不能通过 空指针访问节点内容

左单旋 用于处理 某 AVL树的根节点平衡因子为 1 (即此树右子树高), 且又在此树的右子树中 插入新节点导致右子树高度再增加, 进而导致此树失衡 的情况

经过上面的分析, 可以知道 此种情况的特点是:不平衡节点必须是因为右孩子的右子树高而不平衡的 。也就是说

parent->_bf == 2 && cur->_bf == 1 成立时, 才会 执行左单旋进行调平

并且, 左单旋之后, parent->_bf 会 -2, 且 subR->_bf 会 -1

parent->_bf - 2, 是因为 右子树中 少了一个新节点 和 subR节点 的高度

subR->_bf - 1, 是因为 左子树中 多了parent节点

而 对于另一种与之对应的情况:

某 AVL树的根节点平衡因子为 -1 (即此树左子树高), 且又在此树的左子树中 插入新节点导致左子树高度再增加, 进而导致此树失衡

为了处理此种情况, 也有一种操作, 成为 右单旋


右单旋

右单旋 处理的情况一般是这样的:

某 AVL树的根节点平衡因子为 -1 (即此树左子树高), 且又在此树的左子树中 插入新节点导致左子树高度再增加, 进而导致此树失衡

此情况的解决方法 几乎与左单旋完全相同:

如果将 平衡因子为-2的节点 为 parent, 其左孩子节点 为 subL, 则此类情况的调整平衡的具体操作 其实是:

将subL的右孩子 变为 parent的左孩子, 再将 parent 变为 subL的右孩子

此种方法 就是 右单旋, 对应的实现代码即为:

void RotateR(Node* parent) {
	Node* subL = parent->_pLeft;		// 不平衡节点的左孩子
	Node* subLR = subL->_pRight;		// 不平衡节点的左孩子 的 右孩子

	Node* ppNode = parent->_pParent;

	// 将 不平衡节点的左孩子的的右孩子 变为 父亲节点的左孩子, 并将 父亲节点 变为 此节点的右孩子
	// 并记得 链接三叉链
	parent->_pLeft = subLR;
	if (subLR)			// 右孩子不为空才链接父亲节点
		subLR->_pParent = parent;

	subL->_pRight = parent;
	parent->_pParent = subL;

	// 改变不平衡节点 的 父亲节点的指向
	if (parent == _root) {
		_root = subL;
		_root->_pParent = nullptr;
	}
	else {
		if (parent == ppNode->_pLeft)		// 不平衡节点是其父亲节点的左孩子
			ppNode->_pLeft = subL;			// 把 subL 连接到 其父亲节点的左孩子上
		else
			ppNode->_pRight = subL;			// 把 subL 连接到 其父亲节点的右孩子上

		subL->_pParent = ppNode;		// 更新 subL 的父亲节点
	}

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

可以知道 此种情况的特点是:不平衡节点必须是因为 左孩子的左子树高 而不平衡的 。也就是说

parent->_bf == -2 && cur->_bf == -1 成立时, 才会 执行右单旋进行调平

并且, 左单旋之后, parent->_bf 会 +2, 且 subL->_bf 会 +1

parent->_bf + 2, 是因为 左子树中 少了一个新节点 和 subL节点 的高度

subR->_bf + 1, 是因为 右子树中 多了parent节点

左单旋右单旋 分别解决了:

  1. 某 AVL树的根节点平衡因子为 1 (即此树右子树高), 且又在此树的右子树中 插入新节点导致右子树高度再增加, 进而导致此树失衡

不平衡节点因为其 右孩子的右子树高 而不平衡(简称 右右)

  1. 某 AVL树的根节点平衡因子为 -1 (即此树左子树高), 且又在此树的左子树中 插入新节点导致左子树高度再增加, 进而导致此树失衡

不平衡节点因为其 左孩子的左子树高 而不平衡(简称 左左)

但是, 不平衡的情况不仅仅只有这两种情况, 还存在其他情况。

并且 这些情况更为复杂, 简单的单旋并不能完全解决问题, 所以需要继续分析

左右双旋

左右双旋 处理的情况是这样的:

这种情况 是:

某 AVL树的根节点平衡因子为 -1 (即此树左子树高), 且又在此根节点的左孩子的右子树中插入新节点 导致 以 左孩子为根的树的高度 再增加, 进而导致此树失衡

也就是 不平衡节点因为其 左孩子的右子树高 而不平衡(简称 左右)

  1. h = 0

h = 0, 也就意味着 A、B、C、D 树 都为空, 并且 60节点 都应该为空。因为 B、C 树的高度是 h-1

所以 h = 0 时的实例图 应该为:

60节点 就是新插入的节点
60节点 就是新插入的节点

此时应该怎么调整呢?

最终要调节的是 80节点, 80节点是因为左子树高而失衡的, 所以 最终需要右单旋来调节

但是 右单旋处理的是 左左 的情况

所以 需要将 此树调整为 左左

而 左单旋就是将 parent 旋转到 subR 的左孩子, 并将subR连接到parent的父亲节点下

那么 就以 40节点为parent进行左单旋

这样 就把树的结构调整为了 左左 的情况

然后 以 80节点 为 parent 进行 右单旋

树平衡

  1. h = 1

h = 1, A、D树 高度为 1, B、C树 高度为 0, 在 60节点左、右孩子插入新节点

(虚线, 表示其他可插入位置)
(虚线, 表示其他可插入位置)

以80节点为根的树 失衡的情况是 左右

80节点因为左子树高 而失衡, 最终需要 右单旋调节

所以 需要先将此树调整为 左左

以 40节点为 parent 进行左单旋, 可以将 subR(60节点)调整为左子树高, 且将 subR 连接在 parent 的父亲节点下

即可以将 此树调整为 左左

所以, 以 40节点为 parent 进行左单旋:

此时 以80节点为根的树 失衡的情况就变成了 左左

就可以 以 80节点为 parent 进行右单旋

树平衡

  1. h = 2

A、D树 高度为2, B、C树 高度为1,在B、C树插入新节点

(虚线, 表示其他可插入位置)
(虚线, 表示其他可插入位置)

A、D树各 3 种情况, 新节点可能插入位置有 4 个, 所以此情况的结构一共有 36

但是还是可以用相同的思路分析:

80节点平衡因子是 -2, 最终需要 右单旋进行平衡

右单旋解决的是 “左左” 的情况, 而现在是 “左右”

根绝 左单旋的结果的特点 可以知道, 以 40节点为parent 执行左单旋操作

会将 subR(60节点)的左子树增高, 并将 subR 连接在 parent 的父亲节点之下

进而 可以使 以 80节点为根的树的失衡情况变为 “左左”

然后就可以, 以 80节点为parent 执行右单旋操作

树平衡

  1. h = 3 ······

  2. ······

h 当然可以更大, 但是 方法都是相同的, 因为是同一种失衡情况:“左右”, 不平衡节点因为其 左孩子的右子树高 而不平衡

对比 h 不同时, 此种失衡情况, 从失衡到平衡的调节 过程:

可以看到, 插入新结点之后, 调节平衡的过程是:

  1. 先 以 不平衡节点的左孩子subLparent, 执行左单旋操作
  2. 再 以 不平衡节点parentparent, 执行右单旋操作

这就是 左右双旋 , 其处理的情况是:

某 AVL树的根节点平衡因子为 -1 (即此树左子树高), 且又在此根节点的左孩子的右子树中插入新节点 导致 以 左孩子为根的树的高度 再增加, 进而导致此树失衡

不平衡节点因为其 左孩子的右子树高 而不平衡

所以, 当 parent->_bf == -2 && cur->_bf == 1 时, 使用 左右双旋 调节平衡

左右双旋 的具体实现可以通过 调用左单旋右单旋实现, 但是需要注意的是, 左右双旋 调节结构之后

树的其中三个节点的平衡因子是会发生改变的, 需要根据情况更新平衡因子

根据对比实例可以看出, 插入新结点之后:

  1. subLR 的平衡因子为0, 那么此次双旋操作中 parent->_bf = 0subL->_bf = 0subLR->_bf = 0
  2. subLR 的平衡因子为1, 那么此次双旋操作中 parent->_bf = 0subL->_bf = -1subLR->_bf = 0
  3. subLR 的平衡因子为-1, 那么此次双旋操作中 parent->_bf = 1subL->_bf = 0subLR->_bf = 0

所以 左右双旋 的实现代码:

void RotateLR(Node* parent) {
	Node* subL = parent->_pLeft;		// 不平衡节点的左孩子
	Node* subLR = subL->_pRight;		// 不平衡节点的左孩子的右孩子
	int bf = subLR->_bf;
	// 左右双旋
	RotateL(parent->_pLeft);
	RotateR(parent);

	// 画图可以看出来 如果插入的位置不同 平衡因子的更新规则也不同
	if (bf == 0) {
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else 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 {
		assert(false);
	}
}

既然有 左右双旋, 那肯定也有 右左双旋

右左双旋

左单旋右单旋 之间的关系一样, 左右双旋右左双旋 也有相似的关系

右左双旋 处理的失衡情况 是这样的:

对比 左右双旋 可以看出, 右左双旋 解决的失衡情况是:

某 AVL树的根节点平衡因子为 1 (即此树右子树高), 且又在此根节点的右孩子的左子树中插入新节点 导致 以 右孩子为根的树的高度 再增加, 进而导致此树失衡

不平衡节点因为其 右孩子的左子树高 而不平衡, 可以简称为 “右左”

并且, 与 左右双旋 类似, 右左双旋 则是:

  1. 先 以 不平衡节点的右孩子subRparent, 执行右单旋操作
  2. 再 以 不平衡节点parentparent, 执行左单旋操作

执行 双旋操作之后, 再根据插入新节点后 subRL 的平衡因子, 来更新 parentsubRsubRL的平衡因子

  1. subRL 的平衡因子为0, 那么此次双旋操作中 parent->_bf = 0subL->_bf = 0subLR->_bf = 0
  2. subRL 的平衡因子为1, 那么此次双旋操作中 parent->_bf = -1subL->_bf = 0subLR->_bf = 0
  3. subRL 的平衡因子为-1, 那么此次双旋操作中 parent->_bf = 0subL->_bf = 1subLR->_bf = 0

右左双旋 的代码实现为:

void RotateRL(Node* parent) {
	Node* subR = parent->_pRight;
	Node* subRL = subR->_pLeft;
	int bf = subRL->_bf;
	// 右左双旋
	RotateR(parent->_pRight);
	RotateL(parent);

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

右左双旋也可以画图 分析一下

这 AVL树 插入数据大致分为的 两个步骤:
  1. 按照二叉搜索树的插入方式插入新节点
  2. 分析节点的子树高度差,并进行调整
都已经分析完了, 所以 AVL树的插入操作 具体的代码实现应该是:
public:
	bool insert(const T& data) {
		// 首先按照 二叉搜索树的方式 查找插入位置并插入节点
		if (_root == nullptr) {
			// 树为空 插入节点 直接将新节点作为树的根
			_root = new Node(data);
			_root->_bf = 0;		// 只有根节点的树,根节点平衡因子为 0

			return true;		// 插入成功,直接返回
		}

		// 走到这里就说明需要 查找插入位置 了
		Node* cur = _root;	// 从根节点开始比较
		Node* parent = nullptr;	// 需要记录父亲节点 供插入时连接
		while (cur) {
			// 循环结束的条件是 cur为空,cur为空时就说明 插入位置找到了
			if (cur->_data > data) {
				// 插入值比当前节点值 小,则向左孩子找
				parent = cur;
				cur = cur->_pLeft;
			}
			else if (cur->_data < data) {
				// 插入值比当前节点值 大,则向右孩子找
				parent = cur;
				cur = cur->_pRight;
			}
			else {
				// 走到这里 说明数中已存在相同数据
				return false;
			}
		}

		// 出循环之后,cur 即为数据需要插入的位置
		cur = new Node(data);
		// 将cur与树连接起来
		if (data > parent->_data) {
			parent->_pRight = cur;		// 插入数据比父亲节点数据大,则插入到父亲节点的右孩子
		}
		else if (data < parent->_data) {
			parent->_pLeft = cur;			// 插入数据比父亲节点数据小,则插入到父亲节点的左孩子
		}
        
		// 三叉链结构,cur节点虚存储父亲节点
		cur->_pParent = parent;
		while (parent) {
			if (cur == parent->_pLeft)
				parent->_bf--;			// 新节点在父亲节点的左孩子,则父亲节点的左子树高度+1,则父亲节点的平衡因子-1
			else
				parent->_bf++;

			// 更新完之后,就需要判断 需要继续更新 还是停止更新 或是调整平衡了
			if (parent->_bf == 0) {
				// 某祖先节点的平衡因子 从 -1 或 1 -> 0,说明 插入新节点使此祖先节点的左右子树高度相等了
				// 不会再影响更上边的节点,所以可以结束
				break;
			}
			else if (parent->_bf == -1 || parent->_bf == 1) {
				cur = cur->_pParent;
				parent = parent->_pParent;
			}
			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;
			}
			else {
				// 以上情况都是在保证插入新节点时,树已经是平衡二叉搜索树
				// 如果不是 则会走到此处 触发断言 进而发现错误
				assert(false);
			}
		}
		
		return true;
	}

private:
	void RotateL(Node* parent) {
		Node* subR = parent->_pRight;		// 此节点, 即不平衡节点的右孩子
		Node* subRL = subR->_pLeft;			// 此节点左孩子

		/* parent 可能是 整棵树的根, 也可能是某节点的子树根
		* 而 由于AVL树的节点是三叉链的结构, 所以改变节点的位置 需要改变此节点的父亲节点, 所以
		* 当 parent 是整棵树的根时, 即parent->_pParent 为空, 那么左旋时 就需要直接将 subR改为整棵树的根
		* 当 parent 是某节点的子树时, 就需要将 parent->_pParent 与 subR 连接起来
		* 所以 需要将 parent->_pParent 存储起来
		*/
		Node* ppNode = parent->_pParent;

		// 将 此节点的左孩子 变为 父亲节点的右孩子, 并将 此节点的父亲节点 变为 此节点的左孩子
		// 并记得 链接三叉链
		parent->_pRight = subRL;
		if (subRL)
			subRL->_pParent = parent;

		subR->_pLeft = parent;
		parent->_pParent = subR;

		// 改变不平衡节点 的 父亲节点的指向
		if (parent == _root) {
			_root = subR;
			_root->_pParent = nullptr;
		}
		else {
			if (parent == ppNode->_pLeft)		// 不平衡节点是其父亲节点的左孩子
				ppNode->_pLeft = subR;			// 把 subR 连接到 其父亲节点的左孩子上
			else
				ppNode->_pRight = subR;			// 把 subR 连接到 其父亲节点的右孩子上

			subR->_pParent = ppNode;		// 更新 subR 的父亲节点
		}

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

	void RotateR(Node* parent) {
		Node* subL = parent->_pLeft;		// 此节点, 即不平衡节点的左孩子
		Node* subLR = subL->_pRight;		// 此节点右孩子

		Node* ppNode = parent->_pParent;
        
		parent->_pLeft = subLR;
		if (subLR)
			subLR->_pParent = parent;

		subL->_pRight = parent;
		parent->_pParent = subL;

		// 改变不平衡节点 的 父亲节点的指向
		if (parent == _root) {
			_root = subL;
			_root->_pParent = nullptr;
		}
		else {
			if (parent == ppNode->_pLeft)		// 不平衡节点是其父亲节点的左孩子
				ppNode->_pLeft = subL;			// 把 subL 连接到 其父亲节点的左孩子上
			else
				ppNode->_pRight = subL;			// 把 subL 连接到 其父亲节点的右孩子上

			subL->_pParent = ppNode;		// 更新 subL 的父亲节点
		}

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

	void RotateLR(Node* parent) {
		Node* subL = parent->_pLeft;
		Node* subLR = subL->_pRight;
		int bf = subLR->_bf;
		// 左右双旋
		RotateL(parent->_pLeft);
		RotateR(parent);
		
		// 画图可以看出来 如果插入的位置不同 平衡因子的更新规则也不同
		if (bf == 0) {
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else 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 {
			assert(false);
		}
	}

	void RotateRL(Node* parent) {
		Node* subR = parent->_pRight;
		Node* subRL = subR->_pLeft;
		int bf = subRL->_bf;
		// 右左双旋
		RotateR(parent->_pRight);
		RotateL(parent);
		
		if (bf == 0) {
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1) {
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1) {
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else {
			assert(false);
		}
	}
AVL树的 插入操作, 是AVL树第二难理解的内容, 最难理解的内容是 AVL树 数据的删除

AVL树 数据的删除 更难解决一些, 本篇文章不做分析

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

作者: 哈米d1ch 发表日期:2022 年 10 月 11 日