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

C++-AVL树

一、AVL树的概念

1.二叉搜索树

二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树。
二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:

  1. 非空左子树的所有键值小于其根结点的键值。
  2. 非空右子树的所有键值大于其根结点的键值。
  3. 左、右子树都是二叉搜索树。

二叉搜索树的性能:

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N

在最坏情况下,二叉搜索树的性能就失去了,为了在最坏情况下也能发挥二叉搜索树的性能,VAL树和红黑树的出现就解决了这个问题。

2.AVL树

平衡二叉树 全称叫做平衡二叉搜索(排序)树,简称 AVL树。AVL 是大学教授G.M. Adelson-Velsky 和E.M. Landis名称的缩写,他们提出的平衡二叉树的概念,为了纪念他们,将平衡二叉树称为 AVL树。

方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

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

如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,其高度可保持在LogN,搜索时间复杂度也是LogN

二、AVL树的定义

这里实现的是KV模型的AVL树,因为set和map的底层是KV模型。

template<class K, class V>
struct AVLTreeNode
{
	//三叉链
	AVLTreeNode<K, V>* _left;   //左子树
	AVLTreeNode<K, V>* _right;  //右子树
	AVLTreeNode<K, V>* _parent; //自己的父亲

	//存储的键值对
	pair<K, V> _kv;

	//平衡因子(balance factor)
	int _bf; //右子树高度-左子树高度

	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

三、AVL树的插入

AVL树插入结点时有以下三个步骤:

  • 按照二叉搜索树的插入方法(如果要插入的值比当前节点小,就往左子树找;如果比当前节点大,就往右子树找),找到待插入位置。
  • 找到待插入位置后,将待插入结点插入到树中。
  • 更新平衡因子,如果出现不平衡,则需要进行旋转。

由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

 我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

  1. 新增结点在parent的右边,parent的平衡因子+ + 
  2. 新增结点在parent的左边,parent的平衡因子− − 

每更新完一个结点的平衡因子后,都需要进行以下判断:

  • 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

1.左单旋

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

我们可以看到,插入9以后,parent的平衡因子已经发生改变,所以需要向左旋转。

左单旋的步骤如下:

  1. 让subR的左子树作为parent的右子树。
  2. 让parent作为subR的左子树。
  3. 让subR作为整个子树的根。
  4. 更新平衡因子。

/左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent; //parent的父亲节点

	//1、建立subR和parent之间的关系
	parent->_parent = subR;
	subR->_left = parent;

	//2、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	//3、建立parentParent和subR之间的关系
	if (parentParent == nullptr)            //说明parent是根节点,不需要再向上调整
	{
		_root = subR;
		subR->_parent = nullptr; //subR的_parent指向需改变
	}
	else
	{
		if (parent == parentParent->_left)  //说明parent不是根节点,并且是他的父节点的左子树
		{
			parentParent->_left = subR;
		}
		else //parent == parentParent->_right //说明parent不是根节点,并且是他的父节点的右子树
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}

	//4、更新平衡因子
	subR->_bf = parent->_bf = 0;
}

2.右单旋

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

我们可以看到,插入1以后,parent的平衡因子已经发生改变,所以需要向右旋转。

右单旋的步骤如下:

  1. 让subL的右子树作为parent的左子树。
  2. 让parent作为subL的右子树。
  3. 让subL作为整个子树的根。
  4. 更新平衡因子。

//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;  //parent的父亲节点

	//1、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;

	//2、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//3、建立parentParent和subL之间的关系
	if (parentParent == nullptr)    //说明parent是根节点,不需要再向上调整
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)  //说明parent不是根节点,并且是他的父节点的左子树
		{
			parentParent->_left = subL;
		}
		else //parent == parentParent->_right  //说明parent不是根节点,并且是他的父节点的右子树
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}

	//4、更新平衡因子
	subL->_bf = parent->_bf = 0;
}

3.左右双旋

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

左右双旋的步骤如下:

  1. 以subL为旋转点进行左单旋。
  2. 以parent为旋转点进行右单旋。
  3. 更新平衡因子。

subLR的平衡因子又分为三种情况:

  • 当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。

  • 当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。

  • 当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。

//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1

	//1、以subL为旋转点进行左单旋
	RotateL(subL);

	//2、以parent为旋转点进行右单旋
	RotateR(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

4.右左双旋

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

右左双旋的步骤如下:

  1. 以subR为旋转点进行右单旋。
  2. 以parent为旋转点进行左单旋。
  3. 更新平衡因子。

subRL的平衡因子又分为三种情况:

  • 当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。

  • 当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。

  • 当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。

//右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	//1、以subR为轴进行右单旋
	RotateR(subR);

	//2、以parent为轴进行左单旋
	RotateL(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

5.整体代码

//插入函数
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若AVL树为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		return true;
	}
	//1、按照二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //待插入结点的key值等于当前结点的key值
		{
			//插入失败(不允许key值冗余)
			return false;
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个新结点
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、更新平衡因子,如果出现不平衡,则需要进行旋转
	while (cur != _root) //最坏一路更新到根结点
	{
		if (cur == parent->_left) //parent的左子树增高
		{
			parent->_bf--; //parent的平衡因子--
		}
		else if (cur == parent->_right) //parent的右子树增高
		{
			parent->_bf++; //parent的平衡因子++
		}
		//判断是否更新结束或需要进行旋转
		if (parent->_bf == 0) //更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
		{
			break; //parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (parent->_bf == -1 || parent->_bf == 1) //需要继续往上更新平衡因子
		{
			//parent树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2) //需要进行旋转(此时parent树已经不平衡了)
		{
			if (parent->_bf == -2)
			{
				if (cur->_bf == -1)
				{
					RotateR(parent); //右单旋
				}
				else //cur->_bf == 1
				{
					RotateLR(parent); //左右双旋
				}
			}
			else //parent->_bf == 2
			{
				if (cur->_bf == -1)
				{
					RotateRL(parent); //右左双旋
				}
				else //cur->_bf == 1
				{
					RotateL(parent); //左单旋
				}
			}
			break; //旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
		}
		else
		{
			assert(false); //在插入前树的平衡因子就有问题
		}
	}

	return true; //插入成功
}

四、AVL树的验证

因为AVL树的本质是二叉搜索树,左子树<根节点<右子树,所以用中序遍历的方法即可判断。

但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,变量步骤如下:

  1. 从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
  2. 先判断左子树是否是平衡二叉树。
  3. 再判断右子树是否是平衡二叉树。
  4. 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)
//判断是否为AVL树
bool IsAVLTree()
{
	int hight = 0; //输出型参数
	return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
	if (root == nullptr) //空树是平衡二叉树
	{
		hight = 0; //空树的高度为0
		return true;
	}
	//先判断左子树
	int leftHight = 0;
	if (_IsBalanced(root->_left, leftHight) == false)
		return false;
	//再判断右子树
	int rightHight = 0;
	if (_IsBalanced(root->_right, rightHight) == false)
		return false;
	//检查该结点的平衡因子
	if (rightHight - leftHight != root->_bf)
	{
		cout << "平衡因子设置异常:" << root->_kv.first << endl;
	}
	//把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
	hight = max(leftHight, rightHight) + 1;
	return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}

相关文章:

  • 云创智城充电系统:基于 SpringCloud 的高可用、可扩展架构详解-多租户、多协议兼容、分账与互联互通功能实现
  • 【第3章:卷积神经网络(CNN)——3.5 CIFAR-10图像分类】
  • idea插件开发,如何获取idea设置的系统语言
  • 电脑变慢、游戏卡顿,你的SSD固态可能快坏了!
  • 2024 CyberHost 语音+图像-视频
  • nodejs安装以及安装nvm控制nodejs版本教程
  • 30天开发操作系统 第 20 天 -- API
  • ESXi安装【真机和虚拟机】(超详细)
  • Docker 的安装与环境配置
  • 在nodejs中使用RabbitMQ(六)sharding消息分片
  • Pandas数据填充(fill)中的那些坑:避免机器学习中的数据泄露
  • Arduino 第四章:数字输出 —— 深入解析引脚差异与 LED 顺序点亮实践
  • 人生的转折点反而迷失了方向
  • 分布式技术
  • 【C++】C++-教师信息管理系统(含源码+数据文件)【独一无二】
  • LabVIEW用户界面设计原则
  • 【Elasticsearch】文本分析Text analysis概述
  • 面向 Data+AI 的新一代智能数仓平台
  • 实现Tree 树形控件的鼠标拖拽功能
  • USB Flash闪存驱动器安全分析(第一部分)
  • 国家卫健委有关负责人就白皮书发布答记者问
  • 坚持科技创新引领,赢得未来发展新优势
  • 国家统计局:一季度全国规模以上文化及相关产业企业营业收入增长6.2%
  • 加总理:目前没有针对加拿大人的“活跃威胁”
  • 湖南小伙“朱雀玄武敕令”提交申请改名为“朱咸宁”
  • 关键词看中国经济“一季报”:稳,开局良好看信心