C++:探索AVL树旋转的奥秘

文章目录

  • [前言 AVL树为什么要旋转?](#前言 AVL树为什么要旋转?)
  • 一、插入一个值的大概过程
    • [1. 插入一个值的大致过程](#1. 插入一个值的大致过程)
    • [2. 平衡因子更新原则](#2. 平衡因子更新原则)
    • [3. 旋转处理的目的](#3. 旋转处理的目的)
  • 二、左单旋
    • [1. 左单旋旋转方式总处理图](#1. 左单旋旋转方式总处理图)
    • [2. 左单旋具体会遇到的情况](#2. 左单旋具体会遇到的情况)
    • [3. 左单旋代码总结](#3. 左单旋代码总结)
  • 三、右单旋
    • [1. 右单旋旋转方式总处理图](#1. 右单旋旋转方式总处理图)
    • [2. 右单旋具体会遇到的情况](#2. 右单旋具体会遇到的情况)
    • [3. 右单旋代码总结](#3. 右单旋代码总结)
  • 四、双旋
    • [1. 右左双旋旋转逻辑](#1. 右左双旋旋转逻辑)
    • [2. 右左双旋可能会遇到的问题辨析](#2. 右左双旋可能会遇到的问题辨析)
    • [3. 右左双旋平衡因子的处理](#3. 右左双旋平衡因子的处理)
    • [4. 右左双旋代码总结](#4. 右左双旋代码总结)
  • 五、左右双旋
  • 总结

前言 AVL树为什么要旋转?

AVL树需要旋转是为了保持平衡 。当插入或删除节点后,某些子树的高度差超过1,就会破坏AVL树的平衡性。为了让树重新平衡,避免一边过高、一边过低的情况,旋转可以调整节点的位置,使树保持左右高度差不超过1。这样做的目的是确保查找、插入、删除操作的效率始终保持在 O(log₂ n)。简单来说,旋转就是"调位置,让树站得更稳"。


一、插入一个值的大概过程

1. 插入一个值的大致过程

  1. 按照二叉搜索树规则插入

    插入的新节点位置依据二叉搜索树的性质确定,即小于当前节点放左子树,大于当前节点放右子树。

  2. 更新平衡因子

    新增节点后,只会影响其祖先节点的高度,可能导致部分祖先节点的平衡因子发生变化。从新增节点向根节点逐步更新平衡因子。如果在更新过程中某节点的平衡因子变为2或-2,说明该节点的子树已经不平衡,需要旋转处理;否则,插入结束。

  3. 检查平衡并调整

    • 如果更新平衡因子的过程中没有发现问题(平衡因子均为0、1或-1),插入操作完成。
    • 如果出现不平衡,则对不平衡的子树进行旋转处理。旋转不仅恢复子树的平衡,还会降低子树的高度,确保不再影响其父节点的平衡因子,从而结束插入过程。

2. 平衡因子更新原则

  1. 平衡因子公式

    只有子树高度发生变化时,才会影响当前节点的平衡因子。

  2. 更新规则

    • 若新增节点在父节点的右子树,则父节点的平衡因子增加1(+1)。
    • 若新增节点在父节点的左子树,则父节点的平衡因子减少1(-1)。
  3. 更新停止条件

    • 平衡因子变为0
      父节点平衡因子从 -1 变为 0 或从 1 变为 0,说明新增节点插入到高度较低的一侧,子树高度未改变,更新过程可以停止。
    • 平衡因子变为1或-1
      父节点平衡因子从 0 变为 1 或从 0 变为 -1,说明新增节点插入后子树高度增加,但仍符合平衡要求,需继续向上更新。
    • 平衡因子变为2或-2
      父节点平衡因子从 1 变为 2 或从 -1 变为 -2,说明子树高度过高,已失去平衡,需要进行旋转处理。

3. 旋转处理的目的

  1. 恢复平衡
    通过单旋转或双旋转调整节点位置,使当前子树符合平衡要求。
  2. 降低子树高度
    旋转后,子树高度恢复到插入前的水平,确保不会对更高层节点产生影响,插入过程结束。

二、左单旋

形成条件:parent->_bf == 2 && cur->_bf == 1

1. 左单旋旋转方式总处理图

  1. 失衡检测

    • 当插入的新节点导致父节点的平衡因子为 2,并且新节点被插入到右子树的右侧时,发生右右失衡(RR失衡)。
  2. 旋转操作

    • 左单旋 的核心目标是将父节点的右子树(即失衡节点的右子树根)提升为新的根节点,并将原来的父节点挂接到新根节点的左子树上。
cpp 复制代码
parent->right = cur->left;  // 将右子树的左子树挂接到父节点的右子树
cur->left = parent;         // 将父节点挂接为右子树的左子树
  1. 处理父节点链接问题

    • 需要确保旋转后父节点的父节点(如果存在)正确地连接到新的根节点。
      • 如果原父节点有父节点(即不是根节点),则要更新父节点的左右子树指向新的根节点。
      • 如果原父节点是根节点,则更新树的根节点。
  2. 更新平衡因子

    • 旋转后,原父节点和新的根节点的平衡因子都应设置为 0,因为旋转使得这两颗子树已经平衡。
  3. 旋转结束

    • 完成旋转后,新的根节点成为子树的根,树恢复平衡。

2. 左单旋具体会遇到的情况

我们具体会遇到比如 h = 0, h = 1, h = 2 ...无穷多种情况:

分析如下:


3. 左单旋代码总结

cpp 复制代码
// 左单旋
void RotateL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;

	// 重新链接
	parent->_right = curleft;
	if (curleft) // 如果curleft存在
	{
		curleft->_parent = parent;
	}

	cur->_left = parent;

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

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

		cur->_parent = ppnode;
	}

	// 更改平衡因子
	parent->_bf = cur->_bf = 0;
}

三、右单旋

形成条件:parent->_bf == -2 && cur->_bf == -1

1. 右单旋旋转方式总处理图

右单旋处理的方式与左单旋是一致的,只不过是反过来了,就不多介绍了。

  1. 失衡检测

    • 当插入的新节点导致父节点的平衡因子为 -2,并且新节点被插入到左子树的左侧时,发生左左失衡(LL失衡)。
  2. 旋转操作

    • 右单旋 的核心目标是将父节点的左子树(即失衡节点的左子树根)提升为新的根节点,并将原来的父节点挂接到新根节点的右子树上。
cpp 复制代码
parent->left = cur->right;  // 将左子树的右子树挂接到父节点的左子树
cur->right = parent;        // 将父节点挂接为左子树的右子树
  1. 处理父节点链接问题

    • 需要确保旋转后父节点的父节点(如果存在)正确地连接到新的根节点。
      • 如果原父节点有父节点(即不是根节点),则要更新父节点的左右子树指向新的根节点。
      • 如果原父节点是根节点,则更新树的根节点。
  2. 更新平衡因子

    • 旋转后,原父节点和新的根节点的平衡因子都应设置为 0,因为旋转使得这两颗子树已经平衡。
  3. 旋转结束

    • 完成旋转后,新的根节点成为子树的根,树恢复平衡。

2. 右单旋具体会遇到的情况


3. 右单旋代码总结

cpp 复制代码
// 右单旋
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 (ppnode == nullptr)
	{
		cur->_parent = nullptr;
		_root = cur;
	}
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = cur;
		}
		else
		{
			ppnode->_right = cur;
		}
		cur->_parent = ppnode;
	}

	// 改变平衡因子
	parent->_bf = cur->_bf = 0;
}

四、双旋

1. 右左双旋旋转逻辑

形成条件:parent->_bf == 2 && cur->_bf == -1

这里是右左双旋的处理方式:

  1. 插入新节点
  2. 以cur进行右单旋
  3. 以parent进行左单旋

2. 右左双旋可能会遇到的问题辨析

h = 0 的情况:

h = 1 的情况:

h = 2 的情况:


3. 右左双旋平衡因子的处理

右左双旋的平衡因子与前面的单旋的平衡因子处理方式不同,单旋平衡因子再旋转过后全都是0,但是双旋的平衡因子不一样。

它分为以下三种情况:

  1. h = 0 的情况,及curleft._bf = 0

结果------>parent._bf = 0,cur._bf = 0,curleft._bf = 0


  1. h > 0 的情况下,及curleft._bf == 1
    以以下C位置插入引发的旋转。

结果: parent._bf = -1,cur._bf = 0,curleft._bf = 0


  1. h > 0 的情况下,及curleft._bf == -1
    以以下B位置插入引发的旋转。

结果: parent._bf = 0,cur._bf = 1,curleft._bf = 0


4. 右左双旋代码总结

cpp 复制代码
// 右左双旋
void RotateRL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;
	int bf = curleft->_bf;

	// 右旋
	RotateR(cur);
	// 左旋
	RotateL(parent);

	// 调整平衡因子
	if (bf == 0)
	{
		parent->_bf = 0;
		cur->_bf = 0;
		curleft->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = -1;
		cur->_bf = 0;
		curleft->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		cur->_bf = 1;
		curleft->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

五、左右双旋

形成条件:parent->_bf == -2 && cur->_bf == 1

左右双旋与右左双旋类型,就是反过来的过程~

cpp 复制代码
// 左右双旋
void RotateLR(Node* parent)
{
	Node* cur = parent->_left;
	Node* curright = cur->_right;
	int bf = curright->_bf;

	RotateL(cur);
	RotateR(parent);

	// 调整平衡因子
	if (bf == 0)
	{
		parent->_bf = 0;
		cur->_bf = 0;
		curright->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = 0;
		cur->_bf = -1;
		curright->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 1;
		cur->_bf = 0;
		curright->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

总结

那么,到这里就结束啦!

通过学习AVL树的旋转机制,我们不仅掌握了数据结构平衡的重要性,更感受到了算法的巧妙与严谨。

如果对您有帮助的话,麻烦点一个一键三连,谢谢啦~😘😘😘😘

相关推荐
*TQK*16 分钟前
ZZNUOJ(C/C++)基础练习1031——1040(详解版)
c语言·c++·编程知识点
※DX3906※1 小时前
cpp实战项目—string类的模拟实现
开发语言·c++
wjs20241 小时前
Nginx 安装配置指南
开发语言
美味小鱼1 小时前
实践Rust:编写一个猜数字游戏
开发语言·游戏·rust
萌の鱼1 小时前
leetcode 2080. 区间内查询数字的频率
数据结构·c++·算法·leetcode
Dr.勿忘2 小时前
C#面试常考随笔8:using关键字有哪些用法?
开发语言·unity·面试·c#·游戏引擎
dal118网工任子仪2 小时前
92,[8] 攻防世界 web Web_php_wrong_nginx_config
开发语言·php
wjs20242 小时前
SQLite Update 语句详解
开发语言
加油,旭杏2 小时前
【go语言】接口
开发语言·后端·golang
xianwu5433 小时前
反向代理模块jmh
开发语言·网络·数据库·c++·mysql