平衡二叉搜索树--->AVL树

回顾二叉搜索树

首先,我们回顾一下先前我们实现的二叉搜索树.
首先这棵二叉树的每一个节点都满足:左子树都比它小,右子树都比它大。 虽然听上去似乎可以每次查找砍掉一半的搜索范围。但是实际上这个二叉搜索树的时间复杂度是O(n)! 而当数据接近有序的时候,二叉搜索树就会退化成单边树或者是两边高度差的很多!在这种情况下,搜索树的优势就荡然无存了!所以为了保持搜索树的高效性,所以我们需要对搜索树的左右子树高度进行调整!使得这棵树平衡。 而这样平衡的二叉树有两种,其中一种就是我们今天下面要介绍的AVL树

AVL树

AVL树是在1972年由俄罗斯的两位计算机科学家提出的二叉搜索平衡树,所以这颗二叉搜索树以这两位科学家的名字命名。因此得名AVL树。现在我们都是站在巨人的肩膀上学习。而AVL树对于任意的情况下查找时间复杂度都是O(logn),可以说是一个非常优秀的数据结构。 下面我们就来深入探索一下AVL树为什么能够做到如此快速的查找。

AVL树的定义

如果一棵二叉树满足以下的几个条件,那么这棵树才能算得上是AVL树

1.这是一棵二叉搜索树
2.AVL树的每一棵子树的左右子树高度差不超过1
3.空树也是AVL树

下面的这样一棵树就是AVL树:
在这里插入图片描述
而如下的这样的一棵树就不是AVL树:
在这里插入图片描述
接下来我们就来实现一棵AVL树,并实现AVL树的插入,而由于删除算法较为复杂,所以后面会专门用一篇博客来讲解AVL树的删除算法。

AVL的插入

接下来我们就来实现一棵AVL树,和我们先前的二叉树不一样。因为我们可能进行左右子树的平衡调整,所以要求我们能够快速找到父节点,因此这里我们选择实现一棵三叉链的二叉树。 同时我们记录一个平衡因子来确定是否进行平衡调整:

//树的节点结构
//如果不用三叉链的话,可以使用一个栈来保存父节点以便进行回溯
//平衡因子也不是必要,但是这样设计会相对比较好实现
template<class K,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K,V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent; //父节点的指针
	int _bf; //平衡因子,记录是否需要调整
	//这里的explicit可选,这里设计是为了防止隐式类型转换
	//建议单参数构造可以带上
	explicit AVLTreeNode(const pair<K,V>& kv)
		: _kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}
};
//树的总体结构
template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree()
		:_root(nullptr)
	{}
private:
	Node* _root;	

接下来我们来看分析AVL树的插入存在的情况:

我们实现的AVL树是不支持重复插入的!所以在插入的时候需要参看二叉树里面是否已经有存在的节点插入。
和二叉搜索树不同的是,AVL树在插入的时候要严格保证自己的性质,所以要时刻观察平衡因子的情况!

我们先按照二叉搜索树的规则进行查找对应的插入位置:

bool insert(const pair<K, V>& kv)
	{
		if (!_root)
		{
			_root = new Node(kv);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			//如果大于就往左子树走
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//走左子树
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			//存在,不插入
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		//插入
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		//由于是三叉链,所有还要维护父亲指针
		cur->_parent = parent;
}		

接下来我们就来分析插入节点以后,这颗二叉搜索树会发生什么。
毫无疑问的是,对于新增节点的父节点的平衡因子必然变化,而平衡因子是右子树的高度减去左子树的高度。所以假如插入父节点的右,那么bf就要+1,反之bf就要-1。
但是父亲节点可能也是一棵局部的子树,因此父亲的平衡因子的变化可能会影响上层,所以我们还需要进行具体分析!

平衡因子的调整

设新增的节点为cur,对应的父亲节点为parent
情况1:经过调整后parent的平衡因子是0。
在这里插入图片描述
情况2:parent的平衡因子经过调整后是1或者-1
在这里插入图片描述
情况3:parent的平衡因子是2或者-2,这时候平衡树的平衡性遭遇了严重的破坏!需要进行旋转处理来维护子树的平衡!而这也是最复杂的情况!
情况4;parent->_bf>2,这种情况就说明,原先的树的平衡就已经被破坏了。对于这种情况直接暴力断言。

接下来我们来看一看对应的旋转是如何旋转的。

左单旋转

由于子树的情况无穷无尽,所以下面的图我们使用的是抽象图来代替具体的子树。虽然子树的情况很多,但是我们都可以用一种统一的方式去进行处理!
在这里插入图片描述
对应的旋转代码:

void RotateL(Node* parent)
	{  
	//左单旋需要右子树,右子树的左子树
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;
		subR->_left = parent;
		//如果右子树的左子树非空,需要维护三叉链
		if (subRL)
			subRL->_parent = parent;
			//处理根节点的情况
		if (parent == _root)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		//局部的子树
		else
		{
			Node* ppNode = parent->_parent;
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
		//更改父节点
		parent->_parent = subR;
		subR->_bf = 0;
		parent->_bf = 0;
	}
右单旋转

和左单旋相似,右单旋就是处理左边子树太高的情况:
在这里插入图片描述
对应的旋转代码如下:

void RotateR(Node* parent)
	{  
	//右单旋需要找左子树,左子树的右子树
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		subL->_right = parent;
		//维护三叉链
		if (subLR)
			subLR->_parent = parent;
			//如果是根节点需要特殊处理
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		//当前的树可能是一棵局部的子树
		else
		{  
			Node* ppNode = parent->_parent;
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
		parent->_parent = subL;
		subL->_bf = parent->_bf = 0;
	}
右左双旋

来看这样一种右边高的特殊情况:
在这里插入图片描述
而这种双旋转的情况,调整平衡因子就变成了一项复杂的工作,平衡因子的调整关键看subL的平衡因子

情况1:subRL的bf=0,就是如上的图式,那么parent,subR的平衡因子是0
情况2:subRL的平衡因子是1,旋转后的示意图如下

在这里插入图片描述

情况3:subRL的平衡因子是-1。
对应的就是parentd->_bf=0,subR->_bf=1,subRL->_bf=0
在这里插入图片描述
对应的代码如下:

//右左双旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		//调整平衡因子
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
左右双旋

同样如下这种情况,单旋转不能够调整平衡,所以我们需要进行双旋。
在这里插入图片描述

情况1:subLR->_bf==0 ,那么经过旋转后,对应的平衡因子调整为:parent->_bf=0,subL->_bf=0,subLR->_bf=0
情况2: subLR->_bf==1,那么旋转后的结果如下:
对应的处理平衡因子:parent->_bf=0,subL->_bf=1,subLR->_bf=0

在这里插入图片描述

情况3:subLR->_bf==-1,对应的情况如下:
对应的调整为:parent->_bf=1,subL->_bf=0,subLR->_bf=0

在这里插入图片描述
完整的代码如下:

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

插入的完整代码:

//插入
	bool insert(const pair<K, V>& kv)
	{
		if (!_root)
		{
			_root = new Node(kv);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			//如果大于就往左子树走
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//走左子树
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			//存在,不插入
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		//插入
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;
		//调整平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}
			//平衡因子调整
			if (parent->_bf == 0)
			{
				break;
			}
			else if (abs(parent->_bf) == 1)
			{
				//继续向上调整
				cur = parent;
				parent = parent->_parent;
			}
			//旋转处理
			else if (abs(parent->_bf) == 2)
			{
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				break;
			}
			//走不到这里,走到这里先前就不是平衡树!
			else
			{
				assert(false);
			}
		}
		return true;
	}

总结

AVL树是一棵绝对平衡的二叉树,它查找的效率是O(logn), 但是由于插入的时候会频繁的旋转,这会导致性能的消耗。而标准库里面的set和map容器采用的则是红黑树,关于红黑树的具体细节我们将会在下一篇博客里面详细介绍。

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值