当前位置: 首页 > news >正文

【C++】AVL树

目录

  • 前言
  • 平衡二叉树的定义
  • AVL树的插入
    • AVL树插入的大致过程
    • 更新平衡因子
    • 调整最小不平衡因子
      • 左单旋
      • 右单旋
      • 左右双旋
      • 右左双旋
  • AVL树的删除
  • AVL树的查找

请添加图片描述

前言

前面我们在数据结构中学习了树,以及二叉树,还有二叉排序树,这节来学习平衡二叉树。

数据结构专题学习:数据结构学习
C++专题学习:深入学习C++

平衡二叉树的定义

  在使用二叉排序树时,二叉排序树的查找效率取决于树的高度,当构造二叉排序树的输入序列是有序的时候,它就会形成只有左子树(从大到小输入)或者只有右子树(从小到大输入)的单支树,此时二叉排序树的性能显著变坏
  因此,为了避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除节点时,要保证任意节点的左、右子树高度差的绝对值不超过1,将这样的树就称为平衡二叉树,即AVL树。
  它给树中的每个节点都定义了一个平衡因子(bf)。
节点的平衡因子=左子树高度-右子树高度。(当然也有右子树高度-左子树高度的,只要不违背高度差的绝对值不超过一即可)
平衡二叉树具有以下几个性质:

  • AVL树是一棵二叉排序树
  • AVL树的左右子树也都是AVL树
  • AVL树节点的平衡因子的值只能为-1,0,1

AVL树如下图所示:
在这里插入图片描述

AVL树是一个三叉链结构,不仅有指向左右孩子节点的指针,还有指向父节点的指针,AVL树的代码定义如下:

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	//std::pair是C++标准库中的一个模板类,它能把两个不同类型的值组合成一个单元,这两个值分别由 first 和 second 成员访问。
	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)
	{}
};

AVL树的插入

AVL树插入的大致过程

  1. 在插入值时会按照二叉排序树的插入规则进行插入。
  2. 插入新节点后,会影响祖先节点的高度,即影响祖先的平衡因子,因此要检查其插入路径上的节点是否因为此次操作而导致了不平衡。
  3. 如果导致了不平衡,也就是平衡因子变为了2或-2,此时就要调整这棵子树,进行旋转,从而调节平衡因子从新插入的节点开始往上找,找到第一个不平衡的节点,调整以该节点为根的最小不平衡子树。
  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 (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);
		//判断插在parent节点的左边还是右边
		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)
			{
				// 不平衡了,旋转处理
				if (parent->_bf == 2 && cur->_bf == 1)
				{
				    //左子树高了,要右旋
					RotateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					//右子树高了,要左旋
					RotateL(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					//左孩子的右子树高了,先左旋再右旋
					RotateLR(parent);
				}
				else
				{
					//右孩子的左子树高了,先右旋再左旋
					RotateRL(parent);
				}

				break;
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}

调整最小不平衡因子

  为了既能保证搜索树的规则,又能降低树的高度,调整平衡因子,总共有四种旋转操作:左单旋/右单旋/左右双旋/右左双旋。下面依次来解释。
  首先,在调整之前,我们先要找到第一个不平衡的节点,从上面的调整平衡因子就可以知道。然后以该节点作为根节点,它所对应的子树就是我们要调整的最小不平衡子树。如下图:
在这里插入图片描述
  因为==只要把最小的这棵不平衡子树让它恢复之后,它的祖先节点的平衡因子也都会恢复,==因此在插入一个新节点导致不平衡之后,我们只需要调整最小的这棵子树,就可以让其它节点也恢复平衡。

左单旋

  当在节点A的右孩子的右子树上插入了一个新节点后,导致了A的平衡因子由-1减至-2,导致以A为节点的子树失去平衡,此时就需要一次左单旋操作。如下图所示:
在这里插入图片描述

图中:
H代表各个子树的高度都为H,至于为什么都为H,而不是H-1或者H+1,大家可以自行带入,找平衡因子就可以知道了。
RR插入,指的是在右孩子的右子树上插入了一个新节点导致树的不平衡。
BL:B的左孩子,BR:B的右孩子,AL:A的左孩子
二叉排序树的特性:左子树节点值 < 根节点值 < 右子树节点值

  左单旋的过程为:将A的右孩子B左上旋转代替A成为根节点,将A节点左下旋转成为B的左孩子,而B的原左子树成为A的右子树。

void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if(subRL)//如果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;
	}

可能有些复杂,学会大致思路也行,大致思路如下(设节点A是f,节点B是p,节点A的父节点是gf):

  1. f->left = p -> right;
  2. p->right = f;
  3. gf->left/right = p;

右单旋

  当在节点A的左孩子的左子树上插入了一个新节点后,导致了A的平衡因子由1增至2,导致以A为节点的子树失去平衡,此时就需要一次右单旋操作。如下图所示:
在这里插入图片描述

  右单旋的过程为:将A的左孩子B向右上旋转代替A成为根结点,将A右下旋转成为B的右孩子,而B的右子树变为A的左子树。

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;

		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;
	}

大致思路如下(设节点A时f,节点B是p,节点A的父节点是gf):

  1. f->right = p->left;
  2. p->left = f;
  3. gf->left/right = p;

左右双旋

  当在节点A的左孩子的右子树上插入了一个新节点后,导致了A的平衡因子由1增至2,导致以A为节点的子树失去平衡,此时就需要两次旋转操作,先左旋一次,再进行右旋。如下图所示:
在这里插入图片描述
在这里插入图片描述
  左右双旋的过程为:先将A的左孩子B的右子树的根节点C向左上旋转提升到B的位置,然后把C向右上提升到A的位置。

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 = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else
		assert(false);
}

右左双旋

  当在节点A的右孩子的左子树上插入了一个新节点后,导致了A的平衡因子由-1减至-2,导致以A为节点的子树失去平衡,此时就需要两次旋转操作,先右旋转一次,再进行左旋。如下图所示:
在这里插入图片描述
在这里插入图片描述

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);
		}
	}

AVL树的删除

AVL树的删除有以下几个步骤:

  1. 用二叉排序树的方法对节点w执行删除操作。

  2. 若导致了不平衡,则从节点w开始向上回溯,找到第一个不平衡的节点z(最小不平衡子树),找到节点z的高度最高的孩子y(即"个头"最高的儿子),再找到节点y的高度最高的孩子x(即"个头"最高的孙子)。

  3. 对以z为根的子树进行平衡调整:
    ①孙子在LL,则进行右单旋
    ②孙子在LR,则进行左右双旋
    ③孙子在RR,则进行左单旋
    ④孙子在RL,则进行右左双旋

  4. 若调整后子树高度减1,则可能需要对z的祖先节点进行平衡调整直至回溯到根节点。

二叉排序树的删除操作可参考下图:
在这里插入图片描述
删除操作如下图:
在这里插入图片描述

AVL树的查找

在AVL树上查找的过程与BST数相同。因此查找过程中,进行关键字比较次数不超过树的深度。假设nh表示深度为h的AVL树中含有的最少节点数。显然有n0=1,n1=1,n2=2并且有nh=nh-2+nh-1+1,如下图所示,则可以依次推出深度为h的AVL树含有的最少节点。含有n个节点的AVL树的最大深度为O(log~2~n),因此平均查找效率为O(log~2~n)。
在这里插入图片描述
查找的代码如下:

Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}
	return nullptr;
}

小tips:
当关键字以有序的顺序插入初始为空的AVL树中,若关键字个数为n=2h-1时,该二叉树一定是一棵满二叉树。


感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。本篇还涵盖了王道的内容与图片。
请添加图片描述

相关文章:

  • 音视频 一 看书的笔记 基础视频知识
  • 电话号码的字母组合组合总和II 回溯注意事项(Java)
  • AI玩具迎来爆发式增长,IoT行业如何抓住机遇?
  • Linux 目录结构(文件系统结构)示例说明
  • Python第六章15:字典(dict)定义
  • XCode中使用MonkeyDev开发iOS版的Comand-line Tool的daemon程序
  • IntelliJ IDEA(2024版) 的安装、配置与使用教程:常用配置、创建工程等操作(很详细,你想要的都在这里)
  • Redis 和 MySQL双写一致性的更新策略有哪些?常见面试题深度解答。
  • vue3 vite mock实践
  • 使用 Docker 18 安装 Eureka:解决新版本 Docker 不支持的问题
  • Spring Boot 实现定时任务的案例
  • 使用django的DRF业务逻辑应该放在序列化器类还是模型类
  • pyqt第一个窗口程序
  • 黑马点评项目
  • Maven声明周期
  • 第4.1节:使用正则表达式
  • Jira讲解
  • wgcloud怎么实现服务器或者主机的远程关机、重启操作吗
  • 树莓派超全系列文档--(10)RaspberryOS上使用 Python
  • mysql dump某一张表
  • 做电商必须知道的网站/app运营推广是干什么
  • 网站关键词引流/优化网站排名费用
  • 怎样用ps做网站banner/百度推广工作怎么样
  • 开封专业做网站公司/百度一下你就知道下载安装
  • wordpress怎么改后台/seo有什么作用
  • 以家乡为主题做网站/semen是什么意思