C++AVL树详解

什么是AVL树

AVL树是最先发明的⾃平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的

左右⼦树都是AV树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,

通过控制⾼度差去控制平衡。
简单的来说AVL树就是可以控制自身高度差的搜索二叉树

AVL树通过什么来控制平衡

AVL树实现平衡这⾥我们引⼊⼀个平衡因⼦(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何

结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,

AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡,

就像⼀个指向标⼀样。通过平衡因子我们可以更加直观的去判断AVL树到底是不是平衡的

为什么要求⾼度差不超过1,⽽不是⾼度差是0呢 0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。⽐如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法作为⾼度差是0

我们可以来画图看看

AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 ,那么增删查改的效率也可

以控制在 ,相⽐⼆叉搜索树有了本质的提升。避免了像搜索二叉树出现的一些极端情况,比如说一直插入一个比根节点小的树,就一直往左边插入,这样搜索效率就会变得很差

AVL树的插入

AVL树大致可以分为一下插入过程

1.插入和搜索二叉树一样,都是通过插入节点和当前节点进行比较小的往左走,大的往右走。

  1. 新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦,所以更新

    从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可

    以停⽌了,具体情况我们下⾯再详细分析。

  2. 更新平衡因⼦过程中没有出现问题,则插⼊结束

  3. 更新平衡因⼦过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树

    的⾼度,不会再影响上⼀层,所以插⼊结束。

平衡因子的更新

更新规则

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

只有⼦树⾼度变化才会影响当前结点平衡因⼦

插⼊结点,会增加⾼度,所以新增结点在parent的右⼦树,parent的平衡因⼦++,新增结点在

parent的左⼦树,parent平衡因⼦--

parent所在⼦树的⾼度是否变化决定了是否会继续往上更新
更新结果
这里要分三种情况

如果更新完成之后parent是0则说明更新前子树的高度是-1或者1,说明原来的子树不平衡,一边高一边低,更新后变成0说明新插入的节点插入在低的那一边,插⼊后parent所在的⼦树⾼度不变,不会

影响parent的⽗亲结点的平衡因⼦,更新结束。

更新后parent的平衡因⼦等于1 或 -1,更新前更新中parent的平衡因⼦变化为0->1 或者 0->-1,说

明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所

在的⼦树符合平衡要求,但是⾼度增加了1,会影响parent的⽗亲结点的平衡因⼦,所以要继续向上

更新

更新后parent的平衡因⼦等于2 或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说

明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在⾼的那边,parent所在的⼦树⾼的那边更⾼

了,破坏了平衡,parent所在的⼦树不符合平衡要求,需要旋转处理,旋转的⽬标有两个:1、把

parent⼦树旋转平衡。2、降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不

需要继续往上更新,插⼊结束。

这个旋转等下会详细说明,下面来看看AVL树的插入的实现代码

AVL树的插入

在看插入之前,我们先看看AVL树的每个节点的构造

cpp 复制代码
template<class key,class value>
struct avl_node
{
	int _bf;
	pair<key, value>_kv;
	avl_node<key, value>* _left;
	avl_node<key, value>* _right;
	avl_node<key, value>* _pre_node;

	avl_node(const pair<key,value>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_bf(0)
		,_pre_node(nullptr)
	{}
};

这里我们有三个指针,一个指向当前节点的左边,一个指向当前节点的右边,一个指向当前节点的上一个节点(父节点),然后我们用了一个pair键值对来存放key,value的值

cpp 复制代码
template<class key,class value>
class AVLtree
{
	typedef avl_node<key, value> node;
public:
	AVLtree()
		:_root(nullptr)
	{}
	bool insert(const pair<key, value>& kv)
	{
		if (_root == nullptr)
		{
			_root = new node(kv);
			return true;
		}
		node* cur = _root;
		node* parent = nullptr;
		while (cur != nullptr)
		{
			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->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_pre_node = parent;
		while (parent != nullptr)//回溯跟新
		{
			if (parent->_left == cur)
			{
				parent->_bf--;//插入在左边--
			}
			else
			{
				parent->_bf++;//插入在右边++
			}
			if (parent->_bf == 0)
			{
				break;
			}
			if (parent->_bf == 1 || parent->_bf == -1)//快不平衡了
			{
				cur = parent;
				parent = parent->_pre_node;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)//更新节点
			{
				if (parent->_bf == -2 && cur->_bf == -1)
				{
					rotetR(parent);//这些调整下面会详细说明
				}
				else if (parent->_bf == 2 && cur->_bf == 1)
				{
					rotetL(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					rotetRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					rotetLR(parent);
				}
				else
				{
					assert(false);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;
    }

这里插入逻辑还是和搜索二叉树一样,我们依次比较大小,当找到位置的时候,就需要向上更新平衡因子

好了接下来来说说旋转

AVL树的旋转

旋转的规则

  1. 保持搜索树的规则**(这是很重要的)**

  2. 让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度

    旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。

rotetR右单旋转

首先我们来画图看看,什么情况需要进行右旋转

这里我们用a,b,c来代表一个抽象的子树,这样的好处是可以一张图片概括所有的右单旋转的所有场景


下面就需要开始旋转了

具体步骤说明

在b⼦树中插⼊⼀个新结点,导致b⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平

衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要

往右边旋转,控制两棵树的平衡。

旋转核⼼步骤,因为5 < c⼦树的值 < 10,将c变成10的左⼦树,10变成5的右⼦树,5变成这棵树新

的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2(加2是把5和10加进去算的),符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。
下面来带入具体的数字来说明一下

情况1插入之前 a/b/c的高度是0

情况2插入之前a/b/c的高度是1

好了下面我们来看看代码是怎么实现的

右旋代码

这里面其实有很多的坑,不要看着简单,实际上要注意的点有点多,首先我们需要记录三个节点第一个是subl节点(以不平衡的节点为主的左边节点),然后sublr节点也就是subl的右边节点,pparent也就是(以不平衡节点为主的上一个节点)
其实不难发现旋转时要动的节点就那三个

cpp 复制代码
void rotetR(node* parent)
{
	node* subl = parent->_left;
	node* sublr = subl->_right;
	node* Pparent = parent->_pre_node;
	首先我们让parent的左边指向sublr
	parent->_left = sublr;
	为什么这里需要判断一下sublr是不是为空呢,这是因为有可能出现一种情况一直插入的
	比根小,一直往左边插入,这时候就会形成一条像"/"形状的二叉树,这时sublr是空的,所以需要判断一下
	if (sublr != nullptr)
	{
	不为空就sublr的上一个节点就是parent
		sublr->_pre_node = parent;
	}
	旋转完成之后subl就是新的根了,需要把右边->panrent
	subl->_right = parent;
	parent->_pre_node = subl;同理,向上更新
    到这里就旋转完成了
    下面就需要判断旋转的点到底是不是root节点
    
	if (parent == _root)
	{
		_root = subl;
		subl->_pre_node = nullptr;如果是就把上一个节点置为空
	}
	else
	{
	这里就体现了我们pparent的用法了
		if (Pparent->_left == parent)
		{
			Pparent->_left = subl;
		}
		else
		{
			Pparent->_right = subl;
		}
		subl->_pre_node = Pparent;
	}
	更新节点完成之后更新相应改变的平衡因子
	subl->_bf = 0;
	parent->_bf = 0;
}

这就是右旋

左旋代码

同理右旋

cpp 复制代码
void rotetL(node* parent)
{
	node* subr = parent->_right;
	node* subrl = subr->_left;
	node* pparent = parent->_pre_node;

	parent->_right = subrl;
	if (subrl != nullptr)
	{
		subrl->_pre_node = parent;
	}
	subr->_left = parent;
	parent->_pre_node = subr;
	if (parent == _root)
	{
		_root = subr;
		subr->_pre_node = nullptr;
	}
	else
	{
		if (pparent->_left == parent)
		{
			pparent->_left = subr;
		}
		else
		{
			pparent->_right = subr;
		}
		subr->_pre_node = pparent;
	}
	subr->_bf = 0;
	parent->_bf = 0;
}

左右双旋

通过下面的图可以看到,左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变

成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边

⾼,但是插⼊在b⼦树中,10为跟的⼦树不再是单纯的左边⾼,对于10是左边⾼,但是对于5是右边

⾼,需要⽤两次旋转才能解决,以5为旋转点进⾏⼀个左单旋,以10为旋转点进⾏⼀个右单旋,这棵树

这棵树就平衡了。
下面是a/b/c高度是0的情况

我们来看看旋转的过程

这样这颗树就平衡了
现在我们来看看a/b/c高度是1的情况

有了以上详细的例子我们来看看抽象的把a/b/c抽象成一个子树

下⾯我们将a/b/c⼦树抽象为⾼度h的AVL⼦树进⾏分析,另外我们需要把b⼦树的细节进⼀步展开为8和左⼦树⾼度为h-1的e和f⼦树,因为我们要对b的⽗亲5为旋转点进⾏左单旋,左单旋需要动b树中的左⼦树。b⼦树中新增结点的位置不同,平衡因⼦更新的细节也不同,通过观察8的平衡因⼦不同,这⾥我们要分三个场景讨论。

• 场景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。

• 场景3:h == 0时,a/b/c都是空树,b⾃⼰就是⼀个新增结点,不断更新5->10平衡因⼦,引发旋

转,其中8的平衡因⼦为0,旋转后8和10和5平衡因⼦均为0。
像下面这样

首先我们讨论情况1

旋转完成之后,我们发现10的平衡因子是1(左边低右边高)
再来看看情况2

这时5的平衡因子是-1(右边低左边高)

最后一种情况类似于上面这种,就不做过多解释了

下面来看看代码

cpp 复制代码
void rotetLR(node* parent)
{
	node* subl = parent->_left;
	node* sublr = subl->_right;
	int bf = sublr->_bf;
	rotetL(parent->_left);
	rotetR(parent);
	if (bf == 0)//对应第三种
	{
		parent->_bf = 0;
		subl->_bf = 0;
		sublr->_bf = 0;
	}
	else if (bf == 1)//对应第一种
	{
		subl->_bf = -1;
		parent->_bf = 0;
		sublr->_bf = 0;
	}
	else if (bf == -1)//对应第二种
	{
		parent->_bf = 1;
		subl->_bf = 0;
		sublr->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

右左双旋

逻辑和上面一样

cpp 复制代码
void rotetRL(node* parent)
{
	node* subr = parent->_right;
	node* subrl = subr->_left;
	int bf = subrl->_bf;
	rotetR(parent->_right);
	rotetL(parent);
	if (bf == 1)
	{
		subr->_bf = 0;
		parent->_bf = -1;
		subrl->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subrl->_bf = 0;
		subr->_bf = 1;
	}
	else if (bf == 0)
	{
		parent->_bf = 0;
		subrl->_bf = 0;
		subr->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

AVL树的中序遍历

和普通搜索二叉树一样

cpp 复制代码
void _InOrder(node* root)
{
	if (root == nullptr)
	{
		return;
	}

	_InOrder(root->_left);
	cout << root->_kv.first << ":" << root->_kv.second << endl;
	_InOrder(root->_right);
}

怎么判断是不是AVL树

这里可以采用递归的方法来判断

先从根节点去递归检查高度,用高度差和平衡因子来检查,以此类推

cpp 复制代码
int heigh(node* root)
{
	if (root == nullptr)
	{
		return 0;
	}
	int lef = heigh(root->_left);
	int rig = heigh(root->_right);
	return lef > rig ? lef + 1 : rig + 1;
}
bool cheak_AVL(node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	int lef = heigh(root->_left);
	int rig = heigh(root->_right);
	int diff =rig-lef;
	if (abs(diff) >= 2)
	{
		printf("高度异常");
		return false;
	}
	 if (root->_bf != diff)
	{
		printf("平衡因子异常");
		return false;
	}
	return cheak_AVL(root->_left) && cheak_AVL(root->_right);
}

源码

cpp 复制代码
#pragma once
#include <utility>
#include<assert.h>
# include<iostream>
using namespace std;
template<class key,class value>
struct avl_node
{
	int _bf;
	pair<key, value>_kv;
	avl_node<key, value>* _left;
	avl_node<key, value>* _right;
	avl_node<key, value>* _pre_node;

	avl_node(const pair<key,value>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_bf(0)
		,_pre_node(nullptr)
	{}
};
template<class key,class value>
class AVLtree
{
	typedef avl_node<key, value> node;
public:
	AVLtree()
		:_root(nullptr)
	{}
	bool insert(const pair<key, value>& kv)
	{
		if (_root == nullptr)
		{
			_root = new node(kv);
			return true;
		}
		node* cur = _root;
		node* parent = nullptr;
		while (cur != nullptr)
		{
			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->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_pre_node = parent;
		while (parent != nullptr)//回溯
		{
			if (parent->_left == cur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}
			if (parent->_bf == 0)
			{
				break;
			}
			if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_pre_node;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)//更新节点
			{
				if (parent->_bf == -2 && cur->_bf == -1)
				{
					rotetR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)
				{
					rotetL(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					rotetRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					rotetLR(parent);
				}
				else
				{
					assert(false);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;
    }
	void rotetR(node* parent)
	{
		node* subl = parent->_left;
		node* sublr = subl->_right;
		node* Pparent = parent->_pre_node;
		parent->_left = sublr;
		if (sublr != nullptr)
		{
			sublr->_pre_node = parent;
		}
		subl->_right = parent;
		parent->_pre_node = subl;
		if (parent == _root)
		{
			_root = subl;
			subl->_pre_node = nullptr;
		}
		else
		{
			if (Pparent->_left == parent)
			{
				Pparent->_left = subl;
			}
			else
			{
				Pparent->_right = subl;
			}
			subl->_pre_node = Pparent;
		}
		subl->_bf = 0;
		parent->_bf = 0;
	}

	void rotetL(node* parent)
	{
		node* subr = parent->_right;
		node* subrl = subr->_left;
		node* pparent = parent->_pre_node;

		parent->_right = subrl;
		if (subrl != nullptr)
		{
			subrl->_pre_node = parent;
		}
		subr->_left = parent;
		parent->_pre_node = subr;
		if (parent == _root)
		{
			_root = subr;
			subr->_pre_node = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = subr;
			}
			else
			{
				pparent->_right = subr;
			}
			subr->_pre_node = pparent;
		}
		subr->_bf = 0;
		parent->_bf = 0;

	}
	void rotetLR(node* parent)
	{
		node* subl = parent->_left;
		node* sublr = subl->_right;
		int bf = sublr->_bf;
		rotetL(parent->_left);
		rotetR(parent);
		if (bf == 0)
		{
			parent->_bf = 0;
			subl->_bf = 0;
			sublr->_bf = 0;
		}
		else if (bf == 1)
		{
			subl->_bf = -1;
			parent->_bf = 0;
			sublr->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subl->_bf = 0;
			sublr->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	void rotetRL(node* parent)
	{
		node* subr = parent->_right;
		node* subrl = subr->_left;
		int bf = subrl->_bf;
		rotetR(parent->_right);
		rotetL(parent);
		if (bf == 1)
		{
			subr->_bf = 0;
			parent->_bf = -1;
			subrl->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subrl->_bf = 0;
			subr->_bf = 1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subrl->_bf = 0;
			subr->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	void inoder()
	{
		_InOrder(_root);
	}
	bool ck()
	{
		return cheak_AVL(_root);
	}
private:
	void _InOrder(node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}
	int heigh(node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		int lef = heigh(root->_left);
		int rig = heigh(root->_right);
		return lef > rig ? lef + 1 : rig + 1;
	}
	bool cheak_AVL(node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
		int lef = heigh(root->_left);
		int rig = heigh(root->_right);
		int diff =rig-lef;
		if (abs(diff) >= 2)
		{
			printf("高度异常");
			return false;
		}
		 if (root->_bf != diff)
		{
			printf("平衡因子异常");
			return false;
		}
		return cheak_AVL(root->_left) && cheak_AVL(root->_right);
	}
	node* _root;
};
相关推荐
Minyy1115 分钟前
小程序项目实践(一)--项目的初始化以及前期的准备工作
开发语言·前端·git·小程序·gitee·uni-app
今晚吃什么呢?16 分钟前
module
开发语言·javascript·ecmascript
汪汪君。21 分钟前
求矩阵的鞍点
c++·算法·矩阵
碳苯23 分钟前
【rCore OS 开源操作系统】Rust 异常处理
开发语言·人工智能·后端·rust·操作系统·os
尘浮生40 分钟前
Java项目实战II基于Java+Spring Boot+MySQL的桂林旅游景点导游平台(源码+数据库+文档)
java·开发语言·数据库·spring boot·mysql·maven·intellij-idea
这孩子叫逆1 小时前
axios 使用
开发语言·javascript·ecmascript
careathers1 小时前
【Python】物流行业数据分析与可视化案例
开发语言·python·数据分析
多多*1 小时前
OJ在线评测系统 微服务高级 网关跨域权限校验 集中解决跨域问题 拓展 JWT校验和实现接口限流降级
java·开发语言·微服务·云原生·架构·maven
另类程序员1321 小时前
安装jdk安装开发环境与maven
java·开发语言
姆路1 小时前
Qt文件对话框、颜色对话框、字体对话框、输入对话框
c++·qt