C++学习笔记——AVL树

上回说到,二叉搜索树的时间复杂度并不稳定,于是进化出了另外一种树,AVL树,也叫平衡树。

目录

1、概念

2、定义

3、插入

3.1 左单旋

3.2右单旋

3.3左右双旋

3.4右左双旋

彩蛋


1、概念

由于搜索二叉树对一个已经有序的数据进行插入的话,则会变成一个上一篇博文所画的树,变成一个类似单链表的树。它的查找效率就和单链表的时间复杂度是一样的,O(N)。于是有了一种新的二叉搜索树。

这种树引入了平衡因子的概念。每个结点都有一个平衡因子,这个平衡因子就是该结点右子树与左子树的高度之差。也就是说:

平衡因子 = 右子树高度 - 左子树高度。

这种树在插入的时候,会进行调整,保证每个结点的平衡因子的绝对值不超过1。也就是说平衡因子取值只有-1、0、1。

于是对于这种每个结点的平衡因子的绝对值都是1/0/-1的树就是AVL树。

这个树是由俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis提出的。于是取首字母便是AVL。

2、定义

在上图中每个结点的平衡因子的绝对值都不超过1,故是一棵AVL树。在代码中,如下定义:

template <class T>
class BTnode
{
	BTnode(const T& val = T())
	:val_(val)
	,left_(nullptr)
	,right_(nullptr)
	,parent_(nullptr)
	,bf_(0)
	{}
	T val_;
	BTnode<T>* left_;
	BTnode<T>* right_;
	BTnode<T>* parent_;//该节点的父结点
	int bf_;//该节点的平衡因子
};

3、插入

AVL树在二叉搜索树的基础上增加了一个平衡因子,在定义中增加了一个指向父结点的指针。AVL树的插入其实就是在二叉搜索树的基础上增加了一些东西——对平衡因子的调整。

由于AVL树的每个结点的平衡因子的绝对值都是不超过1的,因此在插入结点后会有一个调整的过程。因此:

AVL树的插入 = 二叉搜索树的插图 + 调整

调整就是对平衡因子的调整。但是在调整过程中还有对树的结构进行变化,这个变化叫做旋转。一共有以下旋转:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋

为了便于理解,将一颗树抽象为:

因此对于旋转就可以如下区分:

3.1 左单旋

对于左单旋的情况就是,在插入结点后,平衡因子变成了parent->bf_ == 2,cur->bf_ == 1,可以看出是parent的结点的右孩子的右子树的高度增加了,因此可以简称为该节点右边的右边高。平衡因子的情况如上图所示,于是旋转之后的平衡因子就都变成了0,达成平衡。

左单旋就是,把父结点拉下来,自己顶上去。把自己的左子树放在父结点的右孩子上,父结点成为自己右孩子。代码如下:

void RotateLeft(pNode parent)
{
	pNode subR = parent->right_;
	pNode subRL = subR->left_;
	
	subR->left_ = parent;
	parent->right_ = subRL;
	if(subRL)//如果左子树存在
		subRL->parent_ = parent;
	if (parent->parent_ == nullptr)//如果是根节点
	{
		subR->parent_ = nullptr;
		root_ = subR;
	}
	else
	{
		subR->parent_ = parent->parent_;
		if (parent->parent_->left_ == parent)
			parent->parent_->left_ = subR;
		else
			parent->parent_->right_ = subR;
	}
				
	parent->parent_ = subR;
	parent->bf_ = subR->bf_ = 0;
}

3.2右单旋

对于右单旋的情况可以对比左单旋,在插入结点后,平衡因子变成了parent->bf_ == -2,cur->bf_ == -1,就是是parent的结点的左孩子的左子树的高度增加了,因此可以简称为该节点左边的左边高。平衡因子的情况如上图所示,于是旋转之后的平衡因子就都变成了0,达成平衡。

与左单旋类似,父结点被拉下来,把cur的右孩子给父结点作为左孩子,父结点成为自己的右孩子。

void RotateRight(pNode parent)
{
	pNode subL = parent->left_;
	pNode subLR = subL->right_;
	subL->right_ = parent;
	parent->left_ = subLR;
	if(subLR)
		subLR->parent_ = parent;
	if (parent->parent_ == nullptr)
	{
		subL->parent_ = nullptr;
		root_ = subL;
	}			
	else
	{
		subL->parent_ = parent->parent_;
		if (parent->parent_->left_ == parent)
			parent->parent_->left_ = subL;
		else
			parent->parent_->right_ = subL;
	}
	parent->parent_ = subL;
	parent->bf_ = subL->bf_ = 0;
}

3.3左右双旋

左右双旋其实就是先左单旋再右单旋。至于为什么会有这样的情况,是由于上面两者都是插在同一边,要么是左边的左边,要么是右边的右边。其实也可能是左边的右边,也就是插在parent的左孩子的右结点子树上,当然可能是左子树也可能是右子树。

if(cur->bf_ == 1 && parent->bf_ == -2)//左右双旋
{
	pNode subL = parent->left_;
	pNode subLR = subL->right_;
	int bf = subLR->bf_;
	RotateLeft(subL);
	RotateRight(parent);
	if(bf == -1)//结点插入到左子树,左边高
	{
		parent->bf_ = 1;
		subL->bf_ = 0;
	}
	else if(bf == 1)//结点插入到右子树,右边高
	{
		parent->bf_ = 0;
		subL->bf_ = -1;
	}
}

3.4右左双旋

与左右双旋相反,则是右左双旋。

if(cur->bf_ == -1 && parent->bf_ == 2)//右左双旋
{
	pNode subR = parent->right_;
	pNode subRL = subR->left_;
	int bf = subRL->bf_;
	RotateRight(subR);
	RotateLeft(parent);
	if(bf == 1)//这里就是结点在右子树
	{
		parent->bf_ = -1;
		subR->bf_ = 0;
	}
	else if(bf == -1)//结点在左子树
	{
		parent->bf_ = 0;
		subR->bf_ = 1;
	}
}

彩蛋

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度是O(lg(N)) 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如: 插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。 因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树, 但一个结构经常修改,就不太适合。

于是又进化了,红黑树诞生。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值