【C++】二叉搜索树全面升级,深度剖析AVL树

一、AVL树概念

1. 二叉搜索树的缺陷

二叉搜索树在不断插入节点后,可能会呈现出不平衡的现象 ,就比如会生成类似于链表的左右子树,这会极大地影响查找效率,因此,我们引入一颗平衡二叉查找树AVL树

2.AVL概念

AVL树是一颗平衡二叉查找树,它是一颗具备以下性质的二叉搜索树:

  • 左右子树都是AVL树
  • 左右子树高度差不超过1
    AVL树是一颗高度平衡搜索二叉树,通过控制高度差 去控制平衡。
    AVL树引入了一个平衡因子 的概念,每个节点都有一个平衡因子,它等于右子树高度减左子树高度,也就是说,任何节点的平衡因子等于1/-1/0
    AVL树整体节点数量和完全二叉树类似,高度可以控制在logN,因此增删查改效率也可以在O(logN)

二、AVL树的实现

1. AVL树的结构

AVL树节点是一个三叉链,除了**_left** 和**_right** ,还有一个**_parent**指针,这有助于后续更新平衡因子以及旋转的实现

cpp 复制代码
template <typename K, typename V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V> *_left;
	AVLTreeNode<K, V> *_right;
	AVLTreeNode<K, V> *_parent;
	int _bf;		//平衡因子

	AVLTreeNode(const pair<K, V> &kv)
		: _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0)
	{}
};
template <typename K, typename V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree()
		:_root(nullptr)
	{}
private:
	Node *_root;
};

2. AVL树的插入

2.1 插入过程

  1. AVL树插入首先就按二叉搜索树的规则来插入节点
  2. 之后再来更新平衡因子,逐层向上更新
  3. 根据更新的平衡因子的情况来确定是否继续更新以及是否旋转

2.2 平衡因子的更新

2.2.1 更新方法
  • 平衡因子=左子树高度-右子树高度,只有子树高度变化才会影响当前节点平衡因子
  • 插入节点,一定会增加其祖先子树的高度,插入在左子树parent平衡因子- -,在右子树++
  • parent所在子树的高度决定了是否会继续向上更新
2.2.2 更新停止条件
  • 更新后parent平衡因子等于0 ,那么说明它是1->0或-1->0 ,因此parent原先子树一高一低,且插入节点插入在较低的一边 ,两子树平衡,更新结束
  • 更新后parent平衡因子等于1/-1 ,那么说明它是0->1/-1 ,原先parent是平衡的,插入的节点使一子树高度增加使两子树一高一低,继续向上更新
  • 更新后parent平衡因子等于2/-2 ,那么说明它是1->2或-1->-1原先parent子树一高一低,插入节点在高的那边 ,破坏了平衡,因此要发生旋转 ,旋转的目的就是使树恢复平衡同时降低树的高度 ,因此旋转之后无需继续更新
  • 当更新到根节点时,其平衡因子为1/-1/0,更新结束

以下是插入节点及更新平衡因子代码实现:

cpp 复制代码
pair<bool, Node *> insert(const pair<K, V> &kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return {true, _root};
	}
	else
	{
		// 找到位置
		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, nullptr};
			}
		}
		// 已经找到,开始插入
		cur = new Node(kv);
		if (kv.first > parent->_kv.first)
			parent->_right = cur;
		else if (kv.first < parent->_kv.first)
			parent->_left = cur;
		else
			assert(false);
		cur->_parent = parent;
		Node *newnode = cur;
		// 更新平衡因子
		while (parent)
		{
			if (parent->_right == cur)
				parent->_bf++;
			else
				parent->_bf--;
			// 判断是否需要调整
			if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 0)
				break;
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 旋转
				break;
			}
			else
				assert(false);
		}
		return {true, newnode};
	}
}

2.3 旋转

2.3.1 旋转的原则

要发生旋转,树必须要符合搜索树的规则,旋转使被旋转的树从不满足变平衡,其次是降低树的高度

2.3.2 右单旋

如图,在a子树插入一个新节点,导致a子树高度从h变成h+1,不断向上更新平衡因子,使10的平衡因子变成-2,需要向右旋转,控制两树平衡

右单旋核心步骤 :由于5<b<10,因此将b变成10的左子树,10变成5的右子树,5变成这棵树的新根,从而达到平衡,并且降低了高度

右单旋的具体细节

情况一: a,b,c高度均为0

情况二: a,b,c高度均为1

情况三: a,b,c高度均为2,这种情况就比较麻烦了,a,b,c有三种形态,但是a必须是x ,如果它不是x,那么插入节点后,它在5之前就会发生旋转,之后就停止更新,不符合条件,经计算,这里有36中情况

情况四: a,b,c高度等于3,这里情况就相当多了

因此我们采用刚开始那种抽象的形式去表示树,这样就避免了巨量的分析

cpp 复制代码
// 右单旋
void RotateR(Node *parent)
{
	Node *subL = parent->_left;
	Node *subLR = subL->_right;
	Node *pParent = parent->_parent;
	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}
	subL->_right = parent;
	parent->_parent = subL;
	subL->_parent = pParent;
	if (_root == parent)
	{
		_root = subL;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subL;
		}
		else
		{
			pParent->_right = subL;
		}
	}
	subL->_bf = 0;
	parent->_bf = 0;
}
2.3.3 左单旋

在a子树中插入一个新节点,使a子树高度从h变成h+1,向上更新平衡因子,使10的平衡因子变成2,因此要向左旋转,控制两树平衡

左单旋核心步骤: 由于10<b<15,将b变成10的右子树,10变成15的左子树,15变成这棵树的新根,从而达到平衡,并且降低高度

左单旋的细节与右单旋类似,且细节复杂冗余,这里就不分析细节了

cpp 复制代码
// 左单旋
void RotateL(Node *parent)
{
	Node *subR = parent->_right;
	Node *subRL = subR->_left;
	Node *pParent = parent->_parent;
	parent->_right = subRL;
	if (subRL)
	{
		subRL->_parent = parent;
	}
	subR->_left = parent;
	subR->_parent = pParent;
	parent->_parent = subR;
	if (_root == parent)
	{
		_root = subR;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subR;
		}
		else
		{
			pParent->_right = subR;
		}
	}
	subR->_bf = 0;
	parent->_bf = 0;
}
2.3.4 左右双旋

左右双旋在什么场景应用呢,看图,如果我们在b子树插入数据,引发旋转,可以看到,我们将其右单旋后树依旧不平衡,依然无法解决问题

此时就要发生左右双旋,我们可以先以5为旋转点进行一个左单旋,以10为旋转点进行右单旋,这棵树就平衡了

单旋的场景是纯粹的一边高,而双旋的场景相对来说是一高一低

此时我们要分情况讨论双旋的情况,以便于分析旋转后平衡因子的更新:

场景1: h>=1时,新增节点插入在e子树,e子树高度从h-1变成h,不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为-1,旋转后8和5平衡因子为0,10平衡因子为1

场景2: h>=1时,新增节点插入在f子树,f子树高度从h-1变成h,不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为1,旋转后,8和10的平衡因子为0,5平衡因子为-1

场景2: h==0时,a,b,c均为空树,b自己就是新增节点,不断更新5->10,引发旋转,其中8的平衡因子为0,旋转后8和10和5平衡因子均为0

对于双旋,我们可以先找到插入节点的祖先节点,如以上三个场景的8,按以上蓝色虚线圈的方式进行分割,将两个蓝色虚线圈作为8的左右子树,再将8的左右子树分别给蓝色圈被切断的部分,这样就完成了双旋,这种方法仅适用于我们画出旋转的图形,代码实现还是通过两次单旋完成的

cpp 复制代码
// 左右双旋
void RotateLR(Node *parent)
{
	Node *subL = parent->_left;
	Node *subLR = subL->_right;
	int bf = subLR->_bf;
	RotateL(subL);
	RotateR(parent);
	if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
		assert(false);
}
2.3.5 右左双旋

右左双旋与左右双旋类似,也是分三种情况讨论:

场景1 :h>=1时,新增结点插入在e子树,e子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后10和12平衡因子为0,15平衡因子为1。

场景2: h>=1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为1,旋转后15和12平衡因子为0,10平衡因子为-1。

场景3 :h==0时,a,b,c都是空树,b自⼰就是⼀个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0,旋转后10和12和15平衡因子均为0。

cpp 复制代码
// 右左双旋
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);
	}
}

3. AVL树的查找

AVL树的查找与二叉搜索树查找逻辑完全相同,时间复杂度均为O(logN)

cpp 复制代码
pair<bool, Node *> find(const K &k)
{
	Node *parent = nullptr;
	Node *cur = _root;
	while (cur)
	{
		if (k < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (k > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return {true, cur};
		}
	}
	return {false, nullptr};
}

4. AVL树平衡检测

AVL树的平衡检测主要是检测左右子树高度差与所有节点平衡因子是否符合条件

  1. 高度差不超过1/-1
  2. 平衡因子=右子树高度-左子树高度
cpp 复制代码
int _Height(Node *_root)
{
	if (_root == nullptr)
		return 0;
	int Left = _Height(_root->_left);
	int Right = _Height(_root->_right);
	return max(Left, Right) + 1;
}
bool _IsBalanceTree(Node *_root)
{
	if (_root == nullptr)
		return true;
	int Left = _Height(_root->_left);
	int Right = _Height(_root->_right);
	if (abs(Left - Right) >= 2)
		return false;
	if (Right - Left != _root->_bf)
		return false;
	return _IsBalanceTree(_root->_left) && _IsBalanceTree(_root->_right);
}

完整代码实现见:

github:AVLTree

gitee:AVLTree

相关推荐
Mumu12181 小时前
P3211 [HNOI2011] XOR和路径
算法
高一学习c++会秃头吗1 小时前
页面置换算法实现
算法
不会就选b1 小时前
数据结构之双向循环链表
数据结构·链表
奋斗的小方1 小时前
Java基础篇09(2):项目实战之基于swing的石头迷阵
java·开发语言
yuanyuan2o21 小时前
Transformers NLP 任务:阅读理解问答
人工智能·算法·自然语言处理·nlp·github
悠仁さん1 小时前
数据结构OJ 简单算法题
数据结构
做cv的小昊1 小时前
计算机图形学:【Games101】学习笔记06——几何(曲线和曲面、网格处理)、阴影图
c++·笔记·学习·游戏·图形渲染·几何学·光照贴图
菜菜的顾清寒1 小时前
力扣HOT100(52)动态规划 - 最长递增子序列
算法·leetcode·动态规划
Evand J1 小时前
【代码介绍】自适应R的AEKF(自适应扩展卡尔曼滤波)和经典EKF比较,MATLAB例程|三维非线性系统
开发语言·matlab·ekf·自适应·自适应滤波