【C++】AVL树

一、AVL树的定义

AVL树是最先发明的自平衡⼆叉搜索树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的
左右子树都是二叉搜索树,且左右子树的高度差的绝对值不超过1。AVL树是⼀颗高度平衡搜索⼆叉树,通过控制高度差去控制平衡。

思考⼀下为什么AVL树是高度平衡搜索⼆叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?我们先看下面的图:

画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如图中的⼀棵树是2个结点,4个结点等情况下,高度差最好就是1,达不到高度差为0。

AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis,他们是两个前苏联的科学家,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

AVL树整体结点数量和分布和完全⼆叉树类似,高度可以控制在\log N ,那么增删查改的效率也可
以控制在O(\log N) ,相比⼆叉搜索树有了本质的提升。

二、AVL树的实现

1、平衡因子

关于AVL树的实现,这里我们引入一个平衡因子(balance factor)的概念,每个结点都有⼀个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像⼀个风向标⼀样。

注:平衡因子不是实现AVL树的必备条件,有些地方没有使用平衡因子也能实现AVL树。

下图是含有平衡因子的AVL树:

对于8这个结点来说,它的左子树高度为3,右子树高度为2,那么它的平衡因子就是-1(右子树的高度减左子树的高度);对于1这个结点来说,它的左子树高度为0,右子树的高度也为0,所以它的平衡因子就是0。如果每个结点的平衡因子大于1或者小于-1,那么这棵树就不是AVL树了,它的平衡性遭到了破坏。 (AVL树相较于二叉搜索树最主要的特点就是平衡,若失去平衡增删查改的时间复杂度就无从谈起)

因为AVL是搜索二叉树,它的插入遵循搜索二叉树的规则,若插入13,如下图所示:

此时,10这个结点的平衡因子更新为2,那么就认为这个树不平衡它就不是AVL树了。针对插入前是AVL树,而插入后不是AVL树的情况,我们会对这棵树进行旋转使它继续保持平衡,旋转的概念下面会详细解释。

2、实现框架

template <class K,class V>
struct AVLTreeNode
{
	pair<K, V> _kv; //结点的每个元素类型是pair,搜索场景是key/value
	int _bf; //记录平衡因子,balance factor

	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent; //需要父结点的指针,方便更新平衡因子

	//结点的构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)  //新结点的平衡因子都是0(因为它的左右子树均为空树,高度都是0,相减也为0)
	{}
};

template <class K,class V>
class AVLTree
{
	typedef AVLNode<K, V> Node;
public:

	//... 实现的主要功能写在这个部分

private:
	Node* _root = nullptr; //只需记录树的根节点
};

3、插入函数(insert)

AVL树的插入规则和二叉搜索树一模一样,只不过它要单独更新结点中的parent指针,以及平衡因子。AVL树插入的大致过程如下:

  1. 按⼆叉搜索树规则插入一个值。
  2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上结点的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
  3. 更新平衡因子过程中没有出现问题,则插入结束。
  4. 更新平衡因子过程中出现不平衡,需要对不平衡子树旋转,旋转后调整平衡的同时,本质降低了子树的高度,这个更新过程不会影响上⼀层,所以插入结束。

平衡因子的更新:

更新原则:

  • 平衡因子 = 右子树高度 - 左子树高度(也可以是左子树高度 - 右子树高度,那么逻辑就需要改变)
  • 只有子树高度变化才会影响当前结点平衡因子
  • 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在
    parent的左子树,parent平衡因子--(右-左)
  • parent所在子树的高度是否变化决定了是否会继续往上更新

更新停止条件(分为3种情况):

  1. 更新后parent的平衡因子等于0,更新中parent的平衡因子变化为-1->0或者1->0,说明更新前parent子树⼀边高⼀边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父结点的平衡因子,更新结束。
  2. 更新后parent的平衡因子等于1或-1,更新中parent的平衡因子变化为0->1或者0->-1(不可能是2->1或者-2->-1,因为插入前这棵树必须是AVL树),说明更新前parent子树两边⼀样高,新增的插入结点后,parent所在的子树⼀边高⼀边低,parent所在的子树符合平衡要求,但是高度增加了1,这会影响parent的父结点的平衡因子,所以要继续向上更新平衡因子,最坏情况下更新到根结点,更新结束。
  3. 更新后parent的平衡因子等于2或-2,更新中parent的平衡因子变化为1->2或者-1->-2,说
    明更新前parent子树⼀边高⼀边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把parent子树旋转平衡。2、降低parent子树的高度,恢复到插⼊结点以前的高度。所以旋转后也不需要继续往上更新(插入前后高度不变:插入前高度是h,插入后,经过旋转,高度还是h),更新结束。

以上3种情况都是针对插入前是AVL树,插入后也是AVL树的情景,第三种情况插入后不是AVL树,那我们就需要单独处理,进行旋转。

图例说明:

插入结点13,结点14的平衡因子由0变为-1,需要向上更新,更新到10结点,它的平衡因子由1变为2,此时破坏了平衡,10所在的子树已经不平衡,需要旋转处理。

插入结点-1,结点1的平衡因子由0变为-1,需要向上更新,更新到中间结点3,它的平衡因子由1变为0,但以3为根的子树高度不变,不会影响上⼀层,更新结束。

插入结点7,结点6的平衡因子由0变为1,需要向上更新,更新到8这个根结点,这是最坏情况。

在左子树中插入结点,右子树所有结点的平衡因子都不需要改变;在右子树中插入结点,左子树所有结点的平衡因子都不需要改变。

了解完上面的知识,那么代码就很好理解了:

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);  //根结点的_parent指针为nullptr
		return true;
	}

	Node* parent = nullptr; //注意这里的parent是结点的父结点,不是结点中指向父结点的指针
	Node* cur = _root;
	while (cur)  //利用循环找到合适的位置进行插入
	{
		if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			//不允许插入相等的值
			return false;
		}
	}

	//插入新结点
	cur= new Node(kv); //创建新结点
	if (parent->_kv.first < kv.first) //比较的是key值,value值不参与比较
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
		//链接父亲
	cur->_parent = parent;

	//插入结点结束后还要控制平衡
	//更新平衡因子,沿着cur和parent向上更新
	while (parent) //保证parent不为空,如果parent为空就更新结束了,此时cur就是根结点
	{
		if (cur == parent->_left) //parent的平衡因子需要--
			parent->_bf--;
		else					//parent的平衡因子需要++
			parent->_bf++;

		//对应更新停止的第一种情况
		if (parent->_bf == 0)
		{
			break;
		}
		//对应更新停止的第二种情况
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			cur = parent;
			parent =
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值