C++进阶—【AVL树】

目录

1. AVL树的概念

2. AVL树节点的定义

3. AVL树的插入

4. AVL树的旋转

 4.1 向左单旋

 4.2 向右单旋

 4.3 左右双旋

 4.4 右左双旋

5.  AVL树的验证

6. AVL树的删除

7. AVL树的性能


1. AVL树的概念

        二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下
        因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis 1962 年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过 1( 需要对树中的结点进行调整 ) ,即可降低树的高度,从而减少平均搜索长度。

一棵 AVL 树或者是空树,或者是具有以下性质的二叉搜索树:
  • 它的左右子树都是AVL
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  • 如果一棵二叉搜索树是高度平衡的,它就是 AVL 树。如果它有 n 个结点,其高度可保持在
    O(log_2 n) ,搜索时间复杂度 O(log_2 n)


2. AVL树节点的定义

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)//平衡因子默认是0
	{}

	pair<K, V>  _kv;
	AVLTreeNode<K, V>* _left;//左孩子
	AVLTreeNode<K, V>* _right;//右孩子
	AVLTreeNode<K, V>* _parent;//父节点

	int _bf;//平衡因子
};

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
private:
	Node* _root = nullptr;
};

3. AVL树的插入

AVL 树就是在二叉搜索树的基础上引入了平衡因子,因此 AVL 树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子
//插入,插入的操作还是跟二叉搜索树的插入基本一致,
	//需要注意的是平衡因子的更新,和更新后的旋转
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		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;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		//更新平衡因子
		while (parent)
		{
			//插入在左,parent的平衡因子--
			//插入在右,parent的平衡因子++
			if (parent->_left == cur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}
			//是否继续更新,要看子树高度的变化
			//1.parent的平衡因子是0,说明插入之前,Parent的平衡因子为正负1,
			// 插入后被调整成0,此时满足AVL树的性质,插入成功
			//2.,parent的平衡因子是-1或1,说明插入之前,parent的平衡因子是0,
			// 插入后被调整为正负1,此时子树高度发生变化,需要继续向上更新平衡因子
			//3.parent的平衡因子是正负2,说明插入之前,Parent的平衡因子为正负1,
			// 说明插入之后严重影响平衡,需要就地处理:旋转
			// 
			// 旋转之后:
			// 1、让这颗子树左右高度不超过1
			// 2、旋转过程中继续保持他是搜索树
			// 3、更新调整孩子节点的平衡因子
			// 4、让这颗子树的高度跟插入前保持一致
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || 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)
				{
					RotateLR(parent);
				}
				//右左双旋
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);//防止自己写错出现特殊情况
				}
				break;
			}
			else
			{
				assert(false);//防止自己写错出现特殊情况
			}
		}
		return true;
	}

4. AVL树的旋转

        如果在一棵原本是平衡的AVL 树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL 树的旋转分为四种:

 4.1 向左单旋

        因为插入的情况有很多种,我就不一一画图了,只画其中一种。

        新增节点是80的左或右才会引发左单旋。

        插入之前AVL树是平衡的,插入之后AVL树就不合符要求了。插入后通过更新平衡因子发现,parent的平衡因子变成了2,而subR的平衡因子是1,说明该子树需要以parent为轴点向左单旋。

左旋操作:把subR(60)的左边给parent(30)的右边,如果subRl(40)不是空节点(可能为空,比如简单版),那么就让subRl(40)的父指针指向parent(30)。再让parent(30)变成subR(60)的左边,让parent(30)的父指针指向subR(60)。如果parent节点(30)就是根,那么现在让subR(60)节点变成根。如果 parent节点(30)不是根,而是一个子树,那么就定义一个pNode指针,pNode是parent节点的父亲,如果parent是pNode的左树,就让subR(60)变成pNode的左树,如果parent是pNode的右树,就让subR(60)变成pNode的右树,旋转完成后更新平衡因子。从下图中可以看到,树变平衡了,而此次旋转完成后需要更新平衡因子的只有parent(30)和subR(60)节点,所以将他们两的的平衡因子更新为0即可。

 复杂版:

 简单版:

    //向左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRl = subR->_left;

		parent->_right = subRl;
		//subRl不为空就让父指针指向parent节点
		if (subRl)
			subRl->_parent = parent;
		
		Node* pNode = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;

		if (pNode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (pNode->_left == parent)
			{
				pNode->_left = subR;
			}
			else
			{
				pNode->_right = subR;
			}
			subR->_parent = pNode;

		}
		//更新旋转后的平衡因子
		parent->_bf = subR->_bf = 0;	
	}

 4.2 向右单旋

右单旋具体操作参考左单旋,新增节点是30的左或右才会引发右单旋。

右旋操作:把60的右给80的左,80变成60的右边,链接父指针,如果之前80是根,那么现在60是根。如果不是根,那么让pNode原来指向80,现在指向60,更新60和80的平衡因子。

 复杂版:

 简单版:

//向右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLr = subL->_right;

		parent->_left = subLr;
		if (subLr)
			subLr->_parent = parent;

		Node* pNode = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;

		if (pNode == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (pNode->_left == parent)
			{
				pNode->_left = subL;
			}
			else
			{
				pNode->_right = subL;
			}
			subL->_parent = pNode;

		}
		parent->_bf = subL->_bf = 0;
	}

 4.3 左右双旋

        左右双旋:这里采用复用的方式进行左右双旋,即先以30为轴点左单旋,再以80为轴点右单旋,旋转完成后,通过判断新增节点 是subLr的左,还是右,还是自己来进行平衡因子的更新。这里有三种情况:1.新增节点是subLr的左,此时subLr的bf为-1,旋转完成后,parent的bf为1,subL的bf和subLr的bf都为0;2.新增节点是subLr的右,此时subLr的bf为1,旋转完成后,parent的bf为0,subL的bf为-1,subLr的bf为0;3.subLr自己就是新增节点,此时subLr的bf为0,旋转完成后,parent的bf,subL的bf和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 == -1)//subLr左子树新增
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLr->_bf = 0;
		}
		else if (bf == 1)//subLr右子树新增
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLr->_bf = 0;
		}
		else if (bf == 0)//subLr自己就是新增
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLr->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

 4.4 右左双旋

右左双旋具体操作参考左右双旋。

平衡因子的更新可不是复制下来那么简单,要画图去看。

 复杂版:

  简单版:

//右左双旋:先向右单旋,再向左单旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRl = subR->_left;
		int bf = subRl->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRl->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRl->_bf = 0;
		}
		else if(bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRl->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

5.  AVL树的验证

AVL 树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证 AVL 树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过 1( 注意节点中如果没有平衡因子 )
节点的平衡因子是否计算正确
	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;;
		_InOrder(root->_right);
	}
	//高度
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}
	//判断AVL树是否正常
	bool IsBalance()
	{
		return _IsBalance(_root);
	}
	bool _IsBalance(Node* root)
	{
		//空树也是AVL树
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		//这里可以很方便的帮助我们看到哪个节点的平衡因子异常了
		if (rightHeight - leftHeight != root->_bf)
		{
			cout<< root->_kv.first <<"平衡因子异常" << endl;
		}
		//这里求的是绝对值,所以算出来的值只要小于2,
		//根和每个子树同样满足条件,才认为这个AVL树是正常的
		return abs(leftHeight - rightHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

测试:


6. AVL树的删除

因为 AVL 树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
过与删除不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。平衡因子的更新与插入稍有不同,反向推导即可。

7. AVL树的性能

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

完整代码:AVLTree/AVLTree/AVLTree.h · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)

猜你喜欢

转载自blog.csdn.net/weixin_68993573/article/details/128998267