【高阶数据结构】AVL树(C++实现)

⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++进阶
⭐代码仓库:C++进阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!


一、概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

在这里插入图片描述

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

我们利用AVL树的概念:
AVL树可以是一棵空树,也可以是具有以下性质的一颗二叉搜索树:
1、它的左右子树都是AVL树,都满足AVL树的性质
2、左右子树的高度差(平衡因子)不超过1(-1/0/1)
3、平衡因子=右子树高度-左子树高度(人为规定)

在这里插入图片描述

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度也是O(logN)。

因为AVL树控制平衡因子,这就使我们的AVL树查找的时候很方便,特别是数据量很大的情况下,时间复杂度是O(logN),10亿个数据只需要查找30次而已,效率得到很大的提升。

在这里插入图片描述

扫描二维码关注公众号,回复: 17223953 查看本文章

二、AVL树结点的定义

我们在这里实现K-V模型的AVL树,为了后续方便操作,我们这里是将AVL树定义为三叉链,也就是一个结点链接左右子树,这个结点的孩子结点再链接parent结点链接上它的父亲结点,这就是我们定义的三叉链的结构,此外我们需要在每个结点中引入平衡因子,即右子树减左子树的大小,而这个平衡因子刚开始为0即可,如下定义:

//Key-Value模型
template<class K, class V>
struct AVLTreeNode
{
    
    
	//构造函数
	AVLTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{
    
    }

	// 定义三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//存储键位值
	pair<K, V> _kv;

	//平衡因子
	int _bf;
};

三、AVL树的插入(难)

AVL树的插入有四大秘诀,分别是:
1、找(先找到需要插入的位置)
2、插(插入操作)
3、更新(更新平衡因子)
4、旋转(根据平衡因子的不满足条件进行左/右旋转)

1、找

找的方法很简单,和我们之前写的搜索二叉树是一样的,我们在下面直接写下模板:

template<class K,class V>
class AVLTree
{
    
    
	typedef AVLTreeNode<K, V> Node;
public:
	// 插入
	bool Insert(const pair<K, V>& kv)
	{
    
    
		// 找
		// 1、待插入结点key比当前结点小就往左子树跑
		// 2、待插入结点key比当前结点大就往右子树跑
		// 3、待插入结点key和当前结点的值相等就显示插入失败
		// 刚开始进去为空树
		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 = cur->_right; // 往左跑
			}
			else if (cur->_kv.first > kv.first)
			{
    
    
				parent = cur; // 保存一下cur原本的结点
				cur = cur->_left;
			}
			else
			{
    
    
				// 发现待插入的结点的值和当前节点相同
				return false;
			}
		}
	}
private:
	Node* _root = nullptr;
};

2、插

我们前面利用的parent结点这时候就发挥出奇效了,因为我们的cur最后一步绝对是走到nullptr,也就是空结点,所以接下来我们就利用parent和cur的构造来进行链接插入关系:

	// 插入数值
	cur = new Node(kv);
	// 父亲结点小于待插入结点
	if (parent->_kv.first < kv.first)
	{
    
    
		// 往右子树插入
		parent->_right = cur;
	}
	// 父亲结点小于待插入结点
	else
	{
    
    
		// 往左子树插入
		parent->_left = cur;
	}
	// 当前节点的父节点为parent结点,更新一下防止关系乱
	cur->_parent = parent;

3、控制平衡 – 看是否需要修改平衡因子

判断完插入成功与否,是不是就要判断平衡因子的更新了?

平衡因子是否更新取决于:该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的 祖先结点的平衡因子可能需要更新。

我们来一个图来解释一下更新:

在这里插入图片描述
我们插入结点以后,发现有些更新的结点不仅仅会影响父结点,还会影响其父节点的父节点,甚至再往上影响,也就是说这个更新结点会影响父系的结点。

更新平衡因子的规则:
1、新增在右,parent->_bf++
2、新增在左,parent->_bf–

然而我们发现一个现象,这个新增在左在右对爷结点似乎影响有不同,我新增的结点在单独一颗左/右子树的分支,那么这个爷结点也要更新,假如说我这颗树的高度并没有发生改变,爷爷结点的平衡因子不会发生任何改变,所以我们需要判断parent结点的平衡因子!我们有如下结论:

1、如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子
2、如果parent的平衡因子等于0;表明无需往上更新平衡因子
3、如果parent的平衡因子等于-2或者2;就已经不平衡了,需要旋转处理!
4、如果parent的平衡因子大于2或者小于-2;就说明之前插入的就不是AVL树了,往前检查,肯定有步骤错了

在这里插入图片描述

在这里插入图片描述

因为是更新完parent的结点如果需要继续往上更新的话,需要提供一个迭代的代码继续往上更新,所以继续往上更新的逻辑如下代码:

	cur = parent;
	parent = parent->_parent; 

4、旋转(判断需不需要旋转,判断是左旋还是右旋,判断是单旋还是双旋)

当平衡因子出现了2/-2的情况,要对子树进行旋转处理,但也要遵守原则:
旋转成平衡树
保持搜索树的规则

而旋转有四种大情况,对此我们要进行分类:

1、当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋
2、当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
3、当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋
4、当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋

注意:进行完旋转以后就无需往上更新平衡因子了,因为这个子树本身的高度已经没有发生变化了,根本不会影响到父节点的平衡因子。

		// 控制平衡
		// 平衡因子的平衡
		while (parent)
		{
    
    
			if (cur == parent->_right)
			{
    
    
				parent->_bf++;
			}
			else
			{
    
    
				parent->_bf--;
			}

			if (parent->_bf == 1 || parent->_bf == -1)
			{
    
    
				//继续往上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 0)
			{
    
    
				//over
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
    
    
				//旋转
				if (parent->_bf == 2 && cur->_bf == 1)
				{
    
    
					// 左单旋
					RoateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
    
    
					// 右单旋
					RoateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
    
    
					// 左右单旋
					RoateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
    
    
					// 右左单旋
					RoateRL(parent);
				}
			}
			else
			{
    
    
				assert(false);
			}
		}

插入总代码

// 插入
	bool Insert(const pair<K, V>& kv)
	{
    
    
		// 找
		// 1、待插入结点key比当前结点小就往左子树跑
		// 2、待插入结点key比当前结点大就往右子树跑
		// 3、待插入结点key和当前结点的值相等就显示插入失败
		// 刚开始进去为空树
		if (_root == nullptr)
		{
    
    
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		// 不是空树
		while (cur)
		{
    
    
			if (cur->_kv.first < kv.first)
			{
    
    
				parent = cur; // 保存一下cur原本的结点
				cur = cur->_right; // 往右跑
			}
			else if (cur->_kv.first > kv.first)
			{
    
    
				parent = cur; // 保存一下cur原本的结点
				cur = cur->_left;
			}
			else
			{
    
    
				// 发现待插入的结点的值和当前节点相同
				return false;
			}
		}

		// 插入数值
		cur = new Node(kv);
		// 父亲结点小于待插入结点
		if (parent->_kv.first < kv.first)
		{
    
    
			// 往右子树插入
			parent->_right = cur;
		}
		// 父亲结点小于待插入结点
		else
		{
    
    
			// 往左子树插入
			parent->_left = cur;
		}
		// 当前节点的父节点为parent结点,更新一下防止关系乱
		cur->_parent = parent;

		// 控制平衡
		// 平衡因子的平衡
		while (parent)
		{
    
    
			if (cur == parent->_right)
			{
    
    
				parent->_bf++;
			}
			else
			{
    
    
				parent->_bf--;
			}

			if (parent->_bf == 1 || parent->_bf == -1)
			{
    
    
				//继续往上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 0)
			{
    
    
				//over
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
    
    
				//旋转
				if (parent->_bf == 2 && cur->_bf == 1)
				{
    
    
					// 左单旋
					RoateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
    
    
					// 右单旋
					RoateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
    
    
					// 左右单旋
					RoateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
    
    
					// 右左单旋
					RoateRL(parent);
				}
				break;
			}
			else
			{
    
    
				assert(false);
			}
		}
		return true;
	}

新节点插入较高左子树的左侧—左左:右单旋

在这里插入图片描述

下面抽象图:

在这里插入图片描述

右单旋步骤:

1、sublr成为parent的左子树(sublr和parent的关系)
2、parent成为subl的右子树(parent和subl的关系,要注意subl可能为空)
3、subl成为根节点(要注意parent是子树的根还是整棵树的根)
4、最后更新平衡因子

在这里插入图片描述

更新平衡因子:
在这里插入图片描述

代码:

	// 右单旋
	void RoateR(Node* parent)
	{
    
    
		// 三叉链
		Node* subl = parent->_left;
		Node* sublr = subl->_right;
		Node* ppnode = parent->_parent;


		//sublr和parent之间的关系
		parent->_left = sublr;
		if (sublr)
			sublr->_parent = parent;

		//subl和parent的关系
		subl->_right = parent;
		parent->_parent = subl;


		//ppnode 和 subl的关系
		if (ppnode == nullptr)
		{
    
    
			_root = subl;
			subl->_parent = nullptr;
		}
		else
		{
    
    
			if (ppnode->_left == parent)
			{
    
    
				ppnode->_left = subl;
			}
			else
			{
    
    
				ppnode->_right = subl;
			}
			subl->_parent = ppnode;
		}

		//更新平衡因子
		subl->_bf = parent->_bf = 0;
	}

新结点插入较高右子树的右侧–右右:左单旋

在这里插入图片描述

下面抽象图:

在这里插入图片描述

左单旋旋转步骤:

1、subrl变成parent的右子树(subl和parent的关系,要注意subl可能为空)
2、parent成为subr的左子树(parent和sublr的关系)
3、subr成为根节点(要注意parent是子树的根还是整棵树的根)
4、最后更新平衡因子

在这里插入图片描述
三个问题:
1、parent是整棵树的根还是只是子树的根的结点?
2、判断parent原本在ppnode的左还是右?
3、subrl为空的情况?

平衡因子更新:
在这里插入图片描述

代码:

	// 左单旋
	void RoateL(Node* parent)
	{
    
    
		// 三叉链
		Node* subr = parent->_right;
		Node* subrl = subr->_left;
		Node* ppnode = parent->_parent;

		// subrl与parent的关系
		parent->_right = subrl;
		if (subrl)
			subrl->_parent = parent;

		// subl和parent的关系
		subr->_left = parent;
		parent->_parent = subr;

		// ppnode和subr的关系
		if (ppnode == nullptr)
		{
    
    
			_root = subr;
			subr->_parent = nullptr;
		}
		else
		{
    
    
			if (ppnode->_left == parent)
			{
    
    
				ppnode->_left = subr;
			}
			else
			{
    
    
				ppnode->_right = subr;
			}
			subr->_parent = ppnode;
		}

		// 更新平衡因子
		subr->_bf = parent->_bf = 0;
	}

左右双旋

结点形状形同如下形式的我们更新的是需要先捋直,再折掉上面的那一段。
在这里插入图片描述

1、先插入新结点
在这里插入图片描述

2、捋直 – 以30为旋转点左单旋

在这里插入图片描述

3、折 – 以90为旋转点右单旋

在这里插入图片描述

左右双旋的步骤

1、以subl为旋转点左单旋
2、以parent为旋转点右单旋
3、更新平衡因子

左右双旋后满足二叉搜索树的性质

左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subl和parent的右子树和左子树,再让subl和parent分别作为sublr的左右子树,最后让sublr作为整个子树的根(结合图理解)。

1、sublr的左子树当中的结点本身就比subL的值大,因此可以作为subl的右子树。
2、sublr的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。
3、经过步骤1/2后,subL及其子树当中结点的值都就比sublr的值小,而parent及其子树当中结点的值都就比sublr的值大,因此它们可以分别作为sublr的左右子树。

更新平衡因子

平衡因子的更新随着sublr原始平衡因子的不同分为以下三种情况:

1、当sublr原始平衡因子是-1时,左右双旋后parent、subl、sublr的平衡因子分别更新为1、0、0。

在这里插入图片描述

2、当sublr原始平衡因子是1时,左右双旋后parent、subl、sublr的平衡因子分别更新为0、-1、0。

在这里插入图片描述

3、当sublr原始平衡因子是0时,左右双旋后parent、subl、sublr的平衡因子分别更新为0、0、0。

在这里插入图片描述

经过左右双旋后,即树的高度没有发生变化,所以无需继续往上更新平衡因子。

代码
	// 左右双旋
	void RoateLR(Node* parent)
	{
    
    
		Node* subl = parent->_left;
		Node* sublr = subl->_right;
		int bf = sublr->_bf;

		//subl节点左单旋
		RoateL(subl);

		//parent节点进行右单旋
		RoateR(parent);

		//更新平衡因子
		if (bf == 1)
		{
    
    
			sublr->_bf = 0;
			subl->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
    
    
			sublr->_bf = 0;
			subl->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 0)
		{
    
    
			sublr->_bf = 0;
			subl->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
    
    
			assert(false);
		}
	}

右左双旋

在这里插入图片描述

1、插入新结点

在这里插入图片描述

2、捋直 — 以90为旋转结点右单旋

在这里插入图片描述

3、折下来—以30为旋转结点左单旋

在这里插入图片描述

右左双旋的步骤

1、以subr的结点右单旋
2、以parent的结点左单旋
3、控制平衡因子

右左双旋后满足二叉搜索树的性质

右左双旋后,实际上就是让subrl的左子树和右子树,分别作为parent和subr的右子树和左子树,再让parent和subr分别作为subrl的左右子树,最后让subrl作为整个子树的根

1、subrl的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
2、subrl的右子树当中的结点本身就比subr的值小,因此可以作为subr的左子树。
3、经过步骤1/2后,parent及其子树当中结点的值都就比subrl的值小,而subr及其子树当中结点的值都就比subrl的值大,因此它们可以分别作为subrl的左右子树。

更新平衡因子

平衡因子的更新随着sublr原始平衡因子的不同分为以下三种情况:

1、当subrl原始平衡因子是-1时,右左双旋后parent、subr、subrl的平衡因子分别更新为0、1、0。

在这里插入图片描述

2、当subrl原始平衡因子是1时,左右双旋后parent、subr、subrl的平衡因子分别更新为-1、0、0。

在这里插入图片描述

3、当subrl原始平衡因子是0时,左右双旋后parent、subr、subrl的平衡因子分别更新为0、0、0。

在这里插入图片描述

代码
	// 右左双旋
	void RoateRL(Node* parent)
	{
    
    
		Node* subr = parent->_right;
		Node* subrl = subr->_left;
		int bf = subrl->_bf;

		//subR右单旋
		RoateR(subr);

		//parent左单旋
		RoateL(parent);

		// 更新平衡因子
		if (bf == 1)
		{
    
    
			subrl->_bf = 0;
			subr->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
    
    
			subrl->_bf = 0;
			subr->_bf = 1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
    
    
			subrl->_bf = 0;
			subr->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
    
    
			assert(false);
		}
	}

四、AVL树的查找

AVL树的查找就是二叉搜索树的查找:
1、如果当前树为空,返回nullptr
2、待查找的结点小于当前结点,往左子树走
3、待查找的结点大于当前结点,往右子树走
4、待查找结点的值和当前结点的值相同,返回当前结点

	// AVL树的查找
	Node* Find(const K& key)
	{
    
    
		Node* cur = _root;
		while (cur)
		{
    
    
			// 待查找的结点小于当前结点,往左子树走
			if (key < cur->_kv.first)
			{
    
    
				cur = cur->_left;
			}

			// 待查找的结点大于当前结点,往右子树走
			else if (key > cur->_kv.first)
			{
    
    
				cur = cur->_right;
			}
			// 结点值相等,找到了
			else
			{
    
    
				return cur;
			}
		}
		// 没这个结点,找不到
		return nullptr;
	}

五、AVL树的修改

两种方法:

直接修改法

1、用我们写的Find函数找到当前键值位为key的点
2、修改当前键值位为value的值

	// AVL树的修改
	bool Modify(const K& key, const V& value)
	{
    
    
		// 1、先找到利用Find函数
		Node* ret = Find(key);
		if (ret == nullptr)
			return false;
		// 2、修改value值
		ret->_kv.second = value;
		return true;
	}

利用插入函数修改

为什么要利用插入函数修改,这是因为我们在插入的时候是有查找,指定位置插入,再进行判断平衡因子是否需要进行旋转!

	// AVL树的修改2
	pair<Node*, bool> Modify(const pair<K, V>& kv)
	{
    
    
		// 找
		// 1、待插入结点key比当前结点小就往左子树跑
		// 2、待插入结点key比当前结点大就往右子树跑
		// 3、待插入结点key和当前结点的值相等就显示插入失败
		// 刚开始进去为空树
		if (_root == nullptr)
		{
    
    
			_root = new Node(kv);
			return make_pair(_root, true);
		}

		Node* parent = nullptr;
		Node* cur = _root;
		// 不是空树
		while (cur)
		{
    
    
			// 待插入结点比当前结点小,往右子树跑
			if (cur->_kv.first < kv.first)
			{
    
    
				parent = cur; // 保存一下cur原本的结点
				cur = cur->_right; // 往右跑
			}
			else if (cur->_kv.first > kv.first)
			{
    
    
				parent = cur; // 保存一下cur原本的结点
				cur = cur->_left;
			}
			else
			{
    
    
				// 发现待插入的结点的值和当前节点相同
				return make_pair(cur, false);
			}
		}

		// 插入数值
		cur = new Node(kv);
		Node* newnode = cur;
		// 父亲结点小于待插入结点
		if (parent->_kv.first < kv.first)
		{
    
    
			// 往右子树插入
			parent->_right = cur;
		}
		// 父亲结点小于待插入结点
		else
		{
    
    
			// 往左子树插入
			parent->_left = cur;
		}
		// 当前节点的父节点为parent结点,更新一下防止关系乱
		cur->_parent = parent;

		// 控制平衡
		// 平衡因子的平衡
		while (parent)
		{
    
    
			if (cur == parent->_right)
			{
    
    
				parent->_bf++;
			}
			else
			{
    
    
				parent->_bf--;
			}

			if (parent->_bf == 1 || parent->_bf == -1)
			{
    
    
				//继续往上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 0)
			{
    
    
				//over
				break;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
    
    
				//旋转
				if (parent->_bf == 2 && cur->_bf == 1)
				{
    
    
					// 左单旋
					RoateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
    
    
					// 右单旋
					RoateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
    
    
					// 左右单旋
					RoateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
    
    
					// 右左单旋
					RoateRL(parent);
				}
				break;
			}
			else
			{
    
    
				assert(false);
			}
		}
		return make_pair(newnode, true);
	}

六、AVL树的重载

1、调用插入函数的键对值
2、拿出结点的key
3、返回value的引用

V()是匿名对象变量,因为不确定到底是什么,不确定第二个键对值是false还是true,所以用匿名,因为倘若是key不在树中的时候,就先插入key,V(),再返回value的引用,倘若是key已经在树中的时候,返回键对值对key的value引用!

	// AVL[]重载
	V& operator[](const K& key)
	{
    
    
		// 键对值
		pair<Node*, bool> ret = Insert(make_pair(key, V()));
		// 拿出结点的node
		Node* node = ret.first;
		// 返回该结点的value引用值
		return node->_kv.second;
	}

七、AVL树的删除(难)

在这里插入图片描述

删除的前期操作与我们的插入中的前期操作是一样的,我们首先要先找到这个要删除的结点,也就是需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同:
1、先找到待删除的结点。
2、若找到的待删除结点的左右子树均不为空,则需要使用替换法(左子树最右结点或者是右子树最左结点)进行删除。

替换法:找到当前所需要删除的结点的位置,在其左子树中找到最大值的结点,或者是在其右子树中找到其值最小的结点,然后再将待删除结点的key值以及value值都改为代替其被删除的结点的值。

我们在找到结点要删除的时候先更新一下平衡因子,因为如果先删除了以后平衡因子会非常的乱,所以就需要先更新一下平衡因子,再进行删除操作。

更新平衡因子如下规则:
删除的结点在parent的右边,parent的平衡因子− −。
删除的结点在parent的左边,parent的平衡因子+ + 。

每更新完一个结点的平衡因子后,都需要进行以下判断
如果parent的平衡因子等于-1或者1,无需继续往上更新平衡因子。
如果parent的平衡因子等于0,还需要继续往上更新平衡因子。
如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断是否往上更新以及是否需要旋转处理的图例:
在这里插入图片描述

在将其旋转的时候,我们将其分为下面六种情况
1、当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋。
2、当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋。
3、当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋,无需向上更新平衡因子,更新当前!
4、当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋。
5、当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋。
6、当parent的平衡因子为2,parent的右孩子的平衡因子为0时,也进行左单旋,无需向上更新平衡因子,更新当前。

旋转处理后我们需要更新平衡因子!

更新完平衡因子就是我们实际需要删除的操作了:
1、实际删除的结点的左子树为空,parent链接到实际删除结点的右子树,删除实际需要删除的结点。
2、实际删除的结点的右子树为空,parent链接到实际删除结点的左子树,删除实际需要删除的结点。
3、实际删除的结点的左右子树都不为空,则需要使用替代法(前面已经替代了)将这个结点给替换掉,然后再删除的步骤。

	// AVL树的删除
	bool Erase(const K& key)
	{
    
    
		// 用于遍历二叉树找结点
		Node* parent = nullptr;
		Node* cur = _root;
		// 用于标记实际的删除结点及其父结点
		Node* delparentpos = nullptr;
		Node* delpos = nullptr;
		// 先找到
		while (cur)
		{
    
    
			// 所给key值小于当前节点的值 -- 往左树走
			if (key < cur->_kv.first)
			{
    
    
				parent = cur;
				cur = cur->_left;
			}
			// 所给key值大于当前结点的值 -- 往右树走
			else if (key > cur->_kv.first)
			{
    
    
				parent = cur;
				cur = cur->_right;
			}
			// 找到了
			else
			{
    
    
				// 左子树为空
				if (cur->_left == nullptr)
				{
    
    
					// 待删除结点是根节点
					if (cur == _root)
					{
    
    
						// 让根节点的右子树作为新的结点
						_root = _root->_right;
						if (_root)
							_root->_parent = nullptr;
						delete cur; // 删除原节点
						return true;
					}
					else // 不是根节点
					{
    
    
						delparentpos = parent; // 标记当前待删除结点的父节点
						delpos = cur; // 标记当前待删除的结点
					}
					break; // 删除结点有祖先的结点,需要更新平衡因子
				}
				// 右子树为空
				else if (cur->_right == nullptr)
				{
    
    
					// 待删除结点是根节点
					if (cur == _root)
					{
    
    
						// 让根节点的左子树作为新的结点
						_root = _root->_left;
						if (_root)
							_root->_parent = nullptr;
						delete cur; // 删除原节点
						return true;
					}
					else // 不是根节点
					{
    
    
						delparentpos = parent; // 标记当前待删除结点的父节点
						delpos = cur; // 标记当前待删除的结点
					}
					break; // 删除结点有祖先的结点,需要更新平衡因子
				}
				// 左右子树都不为空
				else
				{
    
    
					// 替换法
					// 寻找待删除结点的右子树中的最小值
					Node* minparent = cur;
					Node* minright = cur->_right;
					while (minright->_left)
					{
    
    
						minparent = minright; // 记录一下父节点
						minright = minright->_left; // 往左子树走
					}
					cur->_kv.first = minright->_kv.first;// 将待删除结点first替换为右子树的最小值
					cur->_kv.second = minparent->_kv.second;// 将待删除结点second替换为右子树的最小值
					// 记录一下要删除的父节点
					delparentpos = minparent;
					// 记录一下实际要删除的结点
					delpos = minright;
					break; // 祖先结点的平衡因子需要改变
				}
			}
		}
		// 没有被修改过,说明没找到当前要删除的结点
		if (delparentpos == nullptr)
			return false;

		// 记录当前要删除结点和当前要删除结点的父节点
		Node* del = delpos;
		Node* delP = delparentpos;

		// 更新平衡因子
		while (delpos != _root) // 最坏情况是一路更新到根节点
		{
    
    
			// 删除结点在右子树
			if (delpos == delparentpos->_right)
				delparentpos->_bf--;
			// 删除结点在左子树
			else if(delpos == delparentpos->_left)
				delparentpos->_bf++;
			// 判断是否需要旋转
			if (delparentpos->_bf == 0)
			{
    
    
				// 向上更新
				delpos = delparentpos;
				delpos = delpos->_parent;
			}
			else if (delparentpos->_bf == -1 || delparentpos->_bf == 1)
			{
    
    
				// 要删除的父节点的平衡因子为1/-1,无需向上更新平衡因子
				break;
			}
			else if(delparentpos->_bf == 2 || delparentpos->_bf == -2)
			{
    
    
				// 旋转
				// 1、右旋
				if (delparentpos->_bf == -2 && delparentpos->_left->_bf == -1)
				{
    
    
					// 记录一下右旋后的根节点
					Node* right_root = delparentpos->_left;
					// 右旋
					RoateR(delparentpos);
					// 更新根节点
					delparentpos = right_root;
				}
				// 2、左右双旋
				else if (delparentpos->_bf == -2 && delparentpos->_left->_bf == 1)
				{
    
    
					// 记录一下左右双旋后的根节点
					Node* right_left_root = delparentpos->_left->_right;
					// 左右双旋
					RoateLR(delparentpos);
					// 更新根节点
					delparentpos = right_left_root;
				}
				// 3、右单旋
				else if (delparentpos->_bf == -2 && delparentpos->_left->_bf == 0)
				{
    
    
					// 不需要往上更新节点
					Node* right_root = delparentpos->_left;
					RoateR(delparentpos);
					delparentpos = right_root;
					// 更新当前平衡因子
					delparentpos->_bf = 1;
					delparentpos->_right->_bf = -1;
					break;
				}
				// 4、右左单旋
				else if (delparentpos->_bf == -1 && delparentpos->_right->_bf == -1)
				{
    
    
					// 记录一下当前要删除的结点
					Node* right_left_root = delparentpos->_right->_left;
					RoateRL(delparentpos);
					delparentpos = right_left_root;
				}
				// 5、左单旋
				else if (delparentpos->_bf == 2 && delparentpos->_right->_bf == 1)
				{
    
    
					Node* left_root = delparentpos->_right;
					RoateL(delparentpos);
					delparentpos = left_root;
				}
				// 6、左单旋
				else if (delparentpos->_bf == 2 && delparentpos->_parent == 0)
				{
    
    
					// 不需要向上更新节点
					Node* left_root = delparentpos->_right;
					RoateL(delparentpos);
					delparentpos = left_root;
					// 更新当前平衡因子
					delparentpos->_bf = -1;
					delparentpos->_left->_bf = 1;
					break;
				}
			}
			else
			{
    
    
				assert(false);
			}

			// 继续向上更新
			delpos = delparentpos;
			delparentpos = delparentpos->_parent;
		}

		// 实际删除操作
		// 左子树为空
		if (del->_left == nullptr)
		{
    
    
			// 实际删除的结点刚好是其父节点的左孩子
			if (del == delP->_left)
			{
    
    
				delP->_left = del->_right;
				if (del->_right)
					del->_right->_parent = parent;
			}
			// 实际删除的结点刚好是其父节点的右孩子
			else
			{
    
    
				delP->_right = del->_right;
				if (del->_right)
					del->_right->_parent = parent;
			}
		}
		// 右子树为空
		else
		{
    
    
			// 实际删除的结点刚好是其父节点的左孩子
			if (del == delP->_left)
			{
    
    
				delP->_left = del->_left;
				if (del->_left)
					del->_left->_parent = parent;
			}
			// 实际删除的结点刚好是其父节点的右孩子
			else
			{
    
    
				delP->_right = del->_left;
				if (del->_left)
					del->_left->_parent = parent;
			}
		}
		// 删除实际结点
		delete del;
		return true;
	}

八、验证AVL树

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    节点的平衡因子是否计算正确
	// 中序遍历验证是个搜索树
	void _InOrder(Node* root)
	{
    
    
		if (root == nullptr)
		{
    
    
			return;
		}

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

	}

	void InOrder()
	{
    
    
		_InOrder(_root);
	}

先验证是否为二叉搜索树

AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。

	// 中序遍历验证是个搜索树
	void _InOrder(Node* root)
	{
    
    
		if (root == nullptr)
		{
    
    
			return;
		}

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

	void InOrder()
	{
    
    
		_InOrder(_root);
	}

再验证是否为平衡树(两边子树的高度)

中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,遍历步骤如下:
1、从叶子结点处开始计算每课子树的高度(每棵子树的高度 = 左右子树中高度的较大值 + 1)并记录!
2、先判断左子树是否是平衡二叉树。
3、再判断右子树是否是平衡二叉树。
4、若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

在这里插入图片描述

	// 判断是不是平衡树
	bool IsBalance()
	{
    
    
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
    
    
		if (root == nullptr)
			return true;
		// 后序遍历
		int leftHight = Height(root->_left);
		int rightHight = Height(root->_right);
		// 计算两个树的高度之差即可,只要是高度之差不等于根节点的平衡因子的值
		if (rightHight - leftHight != root->_bf)
		{
    
    
			cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;
			return false;
		}
		else
		{
    
    
			cout << "没问题" << endl;
		}
		// 递归判断条件
		return abs(rightHight - leftHight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
	}

九、AVL树的性能

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


猜你喜欢

转载自blog.csdn.net/m0_70088010/article/details/132813995