C++数据结构重要知识点(2)(AVL树及其插入操作)

1.AVL树是什么

AVL树是一种平衡二叉树,是二叉搜索树的优化版本。之前我们讨论过,二叉搜索树存在退化的情况,导致搜索的时间复杂度是O(N)。AVL树就能优化这种情况,使得对于一颗二叉搜索树来说,任何结点的左右子树的高度差不超过1,这就是最理想的二叉树。在AVL树中,搜索的时间复杂度就是O(logN),即高度次。

借助下面的图可以更深理解AVL树的结构特点

AVL树原则:保持二叉搜索树的规则 + 控制平衡,尽量降低高度

下面我会一步一步分析,结合代码帮助理解。

2.平衡因子、结点的定义

当树不符合AVL树的要求时,就会发生旋转。问题在于如何如何判断要不要旋转?怎么旋转?

平衡因子+三叉链(left、right、parent)解决了要不要旋转的问题。

所谓平衡因子,就是记录该节点右子树与左子树高度的差,当平衡因子为2或-2时,就会发生旋转。我们很容易理解,平衡因子只有-2、-1、0、1、2这五种情况,-2、2标志着需要旋转处理。平衡因子应当作为每个节点的标识之一,所以我们将它放进成员变量里。同时,我们要为计算平衡因子的时间复杂度考虑。根据以往二叉树的学习,计算一个结点的左右高度差可以很好的用递归来解决,但在AVL树里,我们要算所有节点的平衡因子,用递归的办法就过慢了,所以我们引入了第三个指针,除了left,right,还有一个parent,指向自己的父节点。父节点指针的存在使得旋转操作也变简单了,后续会一步一步验证的。

下面是节点的定义

其中,平衡因子用_bf来标识,在实现中,平衡因子的计算可以左树高度减右树高度,也可以右树减左树,随便选择,因为判断要不要旋转取决于平衡因子的绝对值,2和-2本质上没区别,但一定要保证整棵树的计算都要遵循这个规则。本文均采用右 - 左

3.insert、平衡因子的更新

平衡因子的更新是AVL树的难点,我们一定要理清楚逻辑,多练习

(1)空树处理

当一棵树为空树时,它的_root为nullptr,这时要特殊处理,涉及修改成员变量了。

(2)找到该插入的位置

要找到该插入的位置,我们的处理方式和搜索二叉树一样,让nodeC作为探路的,nodeP跟在nodeC后面,由于空树前面已经特殊处理过了,初始nodeC不可能为空,所以这里的while循环一定会进一次,nodeP一定不为空。因为在查找的时候就涉及到值的比较,在查找时顺便也把去重的问题解决了

(3)插入结点处平衡因子和parent的维护

这里就和搜索二叉树有了区别。

当插入在右边的时候除了让nodeP和新节点联系起来,我们还要维护好平衡因子和parent这两个成员变量。最好的思考方式是遍历,涉及两个结点,加起来8个成员变量,逐个考虑是否要修改。

很多人会疑惑平衡因子不是右子树减左子树高度吗?为什么直接++或--啊?

很多人会想:万一下面插入一个结点,上面的某个父节点高度不发生变化怎么办?就像下面这张图

这里我们就要注重理解直接父节点,至于祖先的情况,是后面着重要分析的。

4.平衡因子的逐级更新及旋转判断

平衡因子的维护很考验我们的思维严谨性。一个结点的平衡因子应当怎样维护?

1.当它的直接的子节点插入时要能正确++或--(刚刚已经解决了);

2.当它的非直接的子节点插入时要能正确判断和变化。

因为平衡因子每个结点都有,所以当我们新插入一个结点时,它的直接父亲只需要解决1就可以了,但对于它的其他节点而言,它们不需要解决1,需要解决2。当我们每插入一个结点,就对整棵树进行上面的维护操作,每次操作后所有结点的平衡因子都正确变化。由此一来,不论这棵树有多大,它的结点的平衡因子都可以维护。上面的逻辑一定要理解透彻。

接下来我们就讨论问题2,使得平衡因子的维护逻辑严密化

(1)除了直接父结点外,还有哪些结点会可能受到影响?哪些一定不会?

平衡因子计算的是右左子树高度的差,如果插入的结点都不在该节点的左右子树中,谈何影响?所以,当插入一个结点的时候,它的父亲以及父亲的父亲,顺着父辈一直到根节点都可能会受影响,其他节点一定不受影响,既然那些其它结点都不是插入结点的父辈结点,插入节点也必然不是它们的子树结点,必然不会受影响。

(2)什么时候向上更新?

如果上面的都理解了,我们自然也能理解每插入一个新的结点,对应父辈平衡因子的更新应当是从下往上走的,有的情况父辈会受影响,有的就不会,我们从直接父节点开始讨论。

a.当直接父节点的平衡因子在调整之为0,说明不需要向上调整

我们知道,插入只会使得平衡因子+1或-1,说明插入前只能是-1或1,意味着直接父结点的左边或右边已经有了一个节点了,新增的这个结点只是让它变平衡。这种情况下,在祖父看来,它的子树高度并没有变,所以就不需要调整了。

b.当直接父节点的平衡因子在调整之为1或-1,说明需要向上调整

在变化前是0或2或-2,为0说明左右什么都没有,随便加一个结点在祖父看来子树的高度就变了,所以要调整。调整的话和直接父节点的调整方式一致,只不过这个时候看的是子树,如果插入结点在祖父这里是右子树,就++,否则--

2和-2呢?事实上,对于直接父节点来说插入根本不会出现2或-2的情况

c.当在向祖先调整时,祖先的平衡因子调整后为0,也不需要向上调整

d.当在向祖先调整时,祖先的平衡因子调整后为1或-1,也需要向上调整(和上面理解方式一样)

e.当在向祖先调整时,祖先的平衡因子调整后为2或-2,需要旋转处理,且不再向上调整了

第5点我们先记住,下面我会重点讲解如何旋转处理,不再向上调整的原因。我们先把整体代码实现了

整体逻辑

向上调整逻辑

向上调整也是一个节点一个结点地调整,有可能调整完nodeP平衡因子变成2或-2,所以接下来就需要旋转。

仔细捋捋逻辑就知道,直接父节点不会出现平衡因子为-2/2的情况,只有在向上调整的过程中某个nodeP可能变成2/-2,所以其中一个if判断可以省略

这里需要反复体会,大逻辑比旋转处理方式重要多了,带入每个结点,体会这样处理的严谨性。

5.旋转处理

(1)单旋

下面以右单旋为例子分析

这张抽象图的某些部分容易让人疑惑,要不断假设,推翻才能更好理解

在讨论时我们要重点关注h = 0,只有这个时候才会出现空指针,其余所有情况都算作一种,因为真正受到影响的就只有两个结点,nodeC和nodeP

注意相对位置,nodeC和nodeP在旋转函数中不会修改指向的对象。

左单旋也是如此,自己画画图就很简单了

(2)双旋

以左右双旋为例

同样的,插入节点之前树的高度为h + 2,插入后还是h + 2。

注意处理h = 0和h = num这两种情况

右左双旋处理也类似

左单旋、右单旋、左右双旋、右左双旋涵盖了AVL树所有旋转情况。单旋解决一边倒问题(类似于直链),双旋解决折线问题(一边高,但高的那边并不是直链)

除这两种以外就没有第三种旋转方式了,逻辑的严密性需要体会

6.单旋函数

接下来就是两种单旋函数的实现了,单旋会影响nodeC、nodeRL(nodeLR)、nodeP以及nodeP->parent这四个结点,共16个指针,只要遍历式考虑所有情况就能很好地写出代码了。

特别注意当h = 0时nodeRL(nodeLR)为空,nodeP为根时nodeP->parent为空

至此,我们已经实现了整个AVL树的insert操作,重点是大逻辑以及两种旋转的理解。

7.所有代码

#include <iostream>
#include <utility>
#include <string>
#include <assert.h>
using namespace std;
 
namespace my_AVLTree
{
	template<class K, class V>
	struct AVLTreeNode
	{
		AVLTreeNode(const pair<K, V>& p)
			:_val(p)
			, _bf(0)
			, _left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
		{}

		int _bf;//右 - 左
		pair<K, V> _val;
		AVLTreeNode<K, V>* _left;
		AVLTreeNode<K, V>* _right;
		AVLTreeNode<K, V>* _parent;
	};



	template<class K, class V>
	class AVLTree
	{
		typedef AVLTreeNode<K, V> Node;
	public:
		AVLTree() = default;

		AVLTree(const AVLTree<K, V>& t)
		{
			CreateTree(*this, t._root);
		}

		void CreateTree(AVLTree<K, V>& newt, Node* t)
		{
			if (t == nullptr)
				return;

			newt.insert(t->_val);
			CreateTree(newt, t->_left);
			CreateTree(newt, t->_right);
		}

		AVLTree& operator=(AVLTree t)
		{
			std::swap(_root, t._root);
			return *this;
		}

		~AVLTree()
		{
			Destroy(_root);
			_root = nullptr;
		}

		void Destroy(Node* cur)
		{
			if (cur == nullptr)
				return;

			Destroy(cur->_left);
			Destroy(cur->_right);

			delete cur;
		}

		//不能修改nodeP指向的对象
		void RotateL(Node* const nodeP)
		{
			//受影响的
			Node* const nodeC = nodeP->_right;
			Node* const nodeRL = nodeC->_left;
			Node* nodePP = nodeP->_parent;

			nodeC->_left = nodeP, nodeC->_parent = nodeP->_parent;
			nodeP->_parent = nodeC;

			if (nodeRL == nullptr)
				nodeP->_right = nullptr;
			else
				nodeP->_right = nodeRL, nodeRL->_parent = nodeP;

			if (nodeP == _root)
				_root = nodeC;
			else
			{
				if (nodePP->_left == nodeP)
					nodePP->_left = nodeC;
				else
					nodePP->_right = nodeC;
			}
		}

		void RotateR(Node* const nodeP)
		{
			Node* const nodeC = nodeP->_left;
			Node* const nodeLR = nodeC->_right;
			Node* nodePP = nodeP->_parent;

			nodeC->_right = nodeP, nodeC->_parent = nodeP->_parent;
			nodeP->_parent = nodeC;

			if (nodeLR == nullptr)
				nodeP->_left = nullptr;
			else
				nodeP->_left = nodeLR, nodeLR->_parent = nodeP;

			if (nodeP == _root)
				_root = nodeC;
			else
			{
				if (nodePP->_left == nodeP)
					nodePP->_left = nodeC;
				else
					nodePP->_right = nodeC;
			}
		}

		bool insert(const pair<K, V>& val)
		{
			//空树处理
			if (_root == nullptr)
			{
				_root = new Node(val);
				return true;
			}

			//找到该插入的结点的位置
			Node* nodeP = _root, * nodeC = _root;//nodeP也可以是nullptr,至少被nodeC赋值一次
			while (nodeC)
			{
				//如果key相等就不插入
				if (nodeC->_val.first == val.first)
					return false;

				else if (nodeC->_val.first < val.first)
				{
					nodeP = nodeC;
					nodeC = nodeC->_right;
				}
				else
				{
					nodeP = nodeC;
					nodeC = nodeC->_left;
				}
			}

			//在右边插入新节点
			if (nodeP->_val.first < val.first)
			{
				nodeP->_right = new Node(val);
				nodeP->_right->_parent = nodeP;
				nodeP->_bf++;//平衡因子++
			}

			//在左边插入新的结点
			else
			{
				nodeP->_left = new Node(val);
				nodeP->_left->_parent = nodeP;
				nodeP->_bf--;//平衡因子--
			}

			//向上更新平衡因子
			while (nodeP)
			{
				//平衡因子为0就不向上更新了
				if (nodeP->_bf == 0)
					break;
				else
				{
					//向上调整
					
					nodeC = nodeP, nodeP = nodeP->_parent;

					if (nodeP)
					{
						if (nodeP->_right == nodeC)
							nodeP->_bf++;
						else
							nodeP->_bf--;
					}
					

					//旋转处理
					if (nodeP && (nodeP->_bf == 2 || nodeP->_bf == -2))
					{
						if (nodeP->_bf == 2)
						{
							//左单旋
							if (nodeP->_right->_bf == 1)
							{
								RotateL(nodeP);

								//更新平衡因子
								nodeP->_bf = nodeP->_parent->_bf = 0;	
							}

							//先右后左,右左双旋
							else
							{
								int subRL = nodeP->_right->_left->_bf;

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

								//更新平衡因子
								nodeP->_parent->_bf = 0;
								if (subRL == 0)
								{
									nodeP->_bf = 0;
									nodeP->_parent->_right->_bf = 0;
								}
								else if (subRL == -1)
								{
									nodeP->_bf = 0;
									nodeP->_parent->_right->_bf = 1;
								}
			
								else
								{
									nodeP->_bf = -1;
									nodeP->_parent->_right->_bf = 0;
								}
							}

						}

						else
						{
							//右单旋
							if (nodeP->_left->_bf == -1)
							{
								RotateR(nodeP);

								//更新平衡因子
								nodeP->_bf = nodeP->_parent->_bf = 0;
							}

							//先左后右,左右双旋
							else
							{
								//记录nodeLR的平衡因子,判断之后平衡因子的变化
								int subLR = nodeP->_left->_right->_bf;

								RotateL(nodeP->_left);
								RotateR(nodeP);

								//更新平衡因子
								nodeP->_parent->_bf = 0;

								
								if (subLR == 0)//当h = 0时新插入的结点就是nodeLR,这时要特殊处理
								{
									nodeP->_bf = 0;
									nodeP->_parent->_left->_bf = 0;
								}
								else if (subLR == -1)
								{
									nodeP->_bf = 1;
									nodeP->_parent->_left->_bf = 0;
								}
								else
								{
									nodeP->_bf = 0;
									nodeP->_parent->_left->_bf = -1;
								}

							}

						}

						break;
					}
				}

			}

			return true;

		}

		size_t TreeHeight(Node* cur)
		{
			if (cur == nullptr)
				return 0;

			size_t left = TreeHeight(cur->_left);
			size_t right = TreeHeight(cur->_right);

			return left > right ? left + 1 : right + 1;
		}

		bool IsAVLTree(const AVLTree<K, V>& t)
		{
			bool ret = _IsAVLTree(t._root);
			if (ret)
				cout << "IsAVLTree" << endl;
			else
				assert(0);

			return false;
		}

		void InOrder()
		{
			_InOrder(_root);
		}


	private:

		bool _IsAVLTree(Node* cur)
		{
			if (cur == nullptr)
				return true;

			size_t leftH = TreeHeight(cur->_left);
			size_t rightH = TreeHeight(cur->_right);
			
			bool isAVLTree = false;
			if (rightH - leftH <= 1 || leftH - rightH <= 1)
				isAVLTree = true;

			return isAVLTree && _IsAVLTree(cur->_left) && _IsAVLTree(cur->_right);
		}


		void _InOrder(Node* cur)
		{
			if (cur == nullptr)
				return;

			_InOrder(cur->_left); 

			cout << cur->_val.first << " " << cur->_val.second << endl;

			_InOrder(cur->_right);

		}

	private:
		Node* _root = nullptr;
	};

}
  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值