《C++ STL源码剖析与性能优化:基于AVL树的关联容器实现原理》

前言:

二叉搜索树(BST)的理想时间复杂度为O(log n),但在极端情况下会退化为O(n)的链表结构。为解决这一问题,苏联数学家Adelson-Velsky和Landis于1962年提出了AVL树这一自平衡二叉搜索树。

AVL树通过引入平衡因子和旋转操作,始终保持树的严格平衡状态,确保各项操作稳定在O(log n)时间复杂度。其核心在于:每个节点的左右子树高度差(平衡因子)不超过1,当插入或删除操作破坏平衡时,通过四种基本旋转(左旋、右旋、左右旋、右左旋)自动调整恢复平衡。

本文将系统解析AVL树的平衡原理:从平衡因子的数学定义出发,详细讲解四种旋转操作的触发条件和执行过程,并提供模块化的C++实现。掌握AVL树不仅能深入理解自平衡机制,更为红黑树等高级数据结构的学习奠定基础。

目录

一、AVL树的定义

二、AVL树的特点

三、AVL树的节点结构

四、AVL树的插入操作

1.判断

2.链接

3.旋转

1.左单旋

2.右单旋

3.左右双旋

​编辑

4.右左双旋

5.旋转总结

五、AVL树的验证


一、AVL树的定义

AVL树:是一种 自平衡二叉搜索树,由苏联数学家 Georgy Adelson-Velsky 和 Evgenii Landis 在 1962 年提出,其名称来源于这两位发明者的名字缩写。

AVL树要么是空树,要么是满足以下性质的二叉搜索树:
其左、右子树也都是 AVL 树
并且左、右子树的高度差的绝对值不超过 1

二、AVL树的特点

  1. 严格的平衡特性:保证该节点的左右子树平衡因子绝对值控制在【0,1】,避免极端情况
  2. 旋转平衡机制:如果出现极端情况,会通过旋转来恢复平衡(后面会细说)
  3. 高效操作复杂度查找、插入、删除操作均保持O(log n)时间复杂度,平衡因子维护使旋转操作最多影响O(log n)个节点
  4. 性能趋势:适用于频繁查询,查询效率比红黑树更加的稳定

三、AVL树的节点结构

每个树节点需要涉及到下面几个变量:

数据存储(pair< >)

左右指针(left,right)

父节点指针(parent)

平衡因子(banance)(左右子树的高度差值)

如下

//节点
template<class K,class V>
struct AVL_TreeNode
{
	AVL_TreeNode(const pair<K, V>& _date)
		:date(_date)
		,parent(nullptr)
		,left(nullptr)
		,right(nullptr)
		,balance(0)
	{ }
 
	//数据
	pair<K, V> date;
	//节点指针
	AVL_TreeNode<K, V>* parent;
	AVL_TreeNode<K, V>* left;
	AVL_TreeNode<K, V>* right;
	//平衡因子
	int balance;
};

四、AVL树的插入操作

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。

那么AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子

1.判断

我们首先需要判断这棵树是否为空,空则直接创建一个根节点即可。

if (root == nullptr)
{
	root = new Node(date);
	return true;
}

2.链接

//找插入位置
Node* parent = nullptr;
Node* cur = root;
while (cur)
{
	//如果date>cur->date,右子树
	parent = cur;
	if (date.first > cur->date.first)
	{
		cur = cur->right;
	}
	else if (date.first < cur->date.first)
	{
		cur = cur->left;
	}
	else
	{
		//如果值相等,不符合key唯一条件
		return false;
	}
}
//插入连接节点
cur = new Node(date);
if (parent->date.first > date.first)
{
	parent->left = cur;
}
else
{
	parent->right = cur;
}
//连接父节点(重点)
cur->parent = parent;

3.旋转

这里我们首先来看一下有哪些旋转方式,再一一来看:

右单旋(RR 旋转):处理 LL 型失衡
左单旋(LL 旋转):处理 RR 型失衡
左右双旋(RL 旋转):处理 RL 型失衡
右左双旋(LR 旋转):处理 LR 型失衡

1.左单旋

仅仅树的右边不平衡,那么想要右边平衡,只能向左调整

调整方法:将右子树往左边调,可以发现,中间节点是处于临界状态,那么可以如下调整:

这里我们可以看到,左单旋的时候,图中的b旋转成为了30的右孩子,但是要是没有b呢?

那就直接令30取代b的位置就可以了。

//单左旋转
void Whirl_L(Node* parent)
{
	//标记节点
	Node* cur = parent->right;
	Node* curleft = cur->left;
	Node* pphead = parent->parent;
 
	//连接parent和cur
	cur->left = parent;
	parent->parent = cur;
	
	//连接curright和parent
	parent->right = curleft;
	if (curleft)
	{
		curleft->parent = parent;
	}
 
	//如果pphead为空,说明pphead是root的根节点
	if (pphead)
	{
		//确定cur和pphead链接位置
		cur->parent = pphead;
		if (pphead->left == parent)
		{
			pphead->left = cur;
		}
		else
		{
			pphead->right = cur;
		}
	}
	else
	{
		root = cur;
		cur->parent = nullptr;
	}
	//更新cur和parent的平衡因子
	cur->balance = 0;
	parent->balance = 0;
}

2.右单旋

我们再来体会一下右单旋,本质上是镜像的左单旋。

上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。

在旋转过程中,有以下几种情况需要考虑:
  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
     如果是根节点,旋转完成后,要更新根节点
     如果是子树,可能是某个节点的左子树,也可能是右子树

//单右旋转
void Whirl_R(Node* parent)
{
	//标记节点
	Node* cur = parent->left;
	Node* curright = cur->right;
	Node* pphead = parent->parent;
 
	//连接parent和cur
	cur->right = parent;
	parent->parent = cur;
 
	//连接curright和parent
	parent->left = curright;
	if (curright)
	{
		curright->parent = parent;
	}
 
	//连接cur和pphead(注意:pphead可能是root->parent是空
	if (pphead)
	{
		//判断cur的连接位置
		cur->parent = pphead;
		if (pphead->left == parent)
		{
			pphead->left = cur;
		}
		else
		{
			pphead->right = cur;
		}
	}
	else
	{
		root = cur;
		cur->parent = nullptr;
	}
	//更新平衡因子
	cur->balance = 0;
	parent->balance = 0;
}

3.左右双旋

当在ptr的左子树的右子树中插入一个结点后,造成了ptr平衡因子为-2的不平衡,将ptr向下找到当前结点的左孩子的右孩子,先进行左单旋ptr->left = subL,然后将ptr的右子树断开指向subR,此时便完成了旋转,最后将平衡因子进行更新。

 //左右双旋
	void Whirl_L_R(Node* parent)
	{
		//标记
		Node* cur = parent->right;
		Node* curright = cur->right;
		int bf = curright->banance;
		//左旋
		Whirl_L(parent->left);
		//右旋
		Whirl_R(parent);
		
		//如果在curleft的左右插入节点
 
		if (bf != 0)
    	{
            if (bf == -1)
            {
            	cur->balance = 1;
            }
           else
           {
	            parent->balance = -1;
           }
	    }
		
	}

4.右左双旋

//右左双旋
void Whirl_R_L(Node* parent)
{
	//记录节点
	Node* cur = parent->right;
	Node* curleft = cur->left;
	int bf = curleft->banance;
	//右旋
	Whirl_R(parent->right);
	//左旋
	Whirl_L(parent);
	
	//如果在curleft的左右插入节点
 
	if (bf != 0)
	{
		if (bf == -1)
		{
			parent->balance = 1;
		}
		else
		{
			cur->balance = -1;
		}
	}
}

5.旋转总结

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR

  • 当pSubR的平衡因子为1时,执行左单旋
  • 当pSubR的平衡因子为-1时,执行右左双旋

2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL

  • 当pSubL的平衡因子为-1是,执行右单旋
  • 当pSubL的平衡因子为1时,执行左右双旋

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

五、AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确

int _Height(PNode pRoot);
bool _IsBalanceTree(PNode pRoot)
{
 // 空树也是AVL树
 if (nullptr == pRoot) return true;
    
 // 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
 int leftHeight = _Height(pRoot->_pLeft);
 int rightHeight = _Height(pRoot->_pRight);
 int diff = rightHeight - leftHeight;
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
 // pRoot平衡因子的绝对值超过1,则一定不是AVL树
 if (diff != pRoot->_bf || (diff > 1 || diff < -1))
 return false;
 // pRoot的左和右如果都是AVL树,则该树一定是AVL树
 return _IsBalanceTree(pRoot->_pLeft) && _IsBalanceTree(pRoot-
>_pRight);
 }

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值