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

news2024/9/24 1:23:38

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

AVL树(C++实现)

  • 一、概念
  • 二、AVL树结点的定义
  • 三、AVL树的插入(难)
    • 1、找
    • 2、插
    • 3、控制平衡 -- 看是否需要修改平衡因子
    • 4、旋转(判断需不需要旋转,判断是左旋还是右旋,判断是单旋还是双旋)
      • 插入总代码
      • 新节点插入较高左子树的左侧---左左:右单旋
      • 右单旋步骤:
      • 新结点插入较高右子树的右侧--右右:左单旋
        • 左单旋旋转步骤:
      • 左右双旋
        • 左右双旋的步骤
        • 左右双旋后满足二叉搜索树的性质
        • 更新平衡因子
        • 代码
      • 右左双旋
        • 右左双旋的步骤
        • 右左双旋后满足二叉搜索树的性质
        • 更新平衡因子
        • 代码
  • 四、AVL树的查找
  • 五、AVL树的修改
    • 直接修改法
    • 利用插入函数修改
  • 六、AVL树的重载
  • 七、AVL树的删除(难)
  • 八、验证AVL树
    • 先验证是否为二叉搜索树
    • 再验证是否为平衡树(两边子树的高度)
  • 九、AVL树的性能


一、概念

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

在这里插入图片描述

因此,两位俄罗斯的数学家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次而已,效率得到很大的提升。

在这里插入图片描述

二、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树,但一个结构经常修改,就不太适合。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1012891.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将一个字符串转换为驼峰命名法(camel case)?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 思路⭐ 示例⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领…

ODrive移植keil(三)—— USB虚拟串口和快速正弦余弦运算

目录 一、USB虚拟串口1.1、硬件连接1.2、代码移植1.3、测试1.4、最终代码 二、快速正弦余弦运算2.1、硬件连接2.2、代码移植2.3、测试2.4、结论 三、软件中断3.1、配置中断3.2、官方代码的使用方式 ODrive、VESC和SimpleFOC 教程链接汇总&#xff1a;请点击 一、USB虚拟串口 单…

Pdf文件签名检查

如何检查pdf的签名 首先这里有一个已经签名的pdf文件&#xff0c;通过pdf软件可以看到文件的数字签名。 下面就是如何代码检查这里pdf文件的签名 1.引入依赖 <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId&g…

彩色相机工作原理——bayer格式理解

早期&#xff0c;图像传感器只能记录光的强弱&#xff0c;无法记录光的颜色&#xff0c;所以只能拍摄黑白照片。 1974年,拜尔提出了bayer阵列&#xff0c;发明了bayer格式图片。不同于高成本的三个图像传感器方案&#xff0c;拜尔提出只用一个图像传感器&#xff0c;在其前面放…

【进阶C语言】数据在内存中的存储

一、数据类型的介绍 1.整形家族 &#xff08;1&#xff09;char--字符型 单位&#xff1a;一个字节&#xff0c;包括unsigned char和signed char (2)short--短整形 单位&#xff1a;两个字节&#xff0c;包括unsigned short[int]和signed short[int] (3)int--整形 单位&…

水果店如何通过小程序商城完成配送路径

水果店线上发展的主要目标就是销售卖货&#xff0c;随着电商经济发展&#xff0c;传统线下店面临不少困境&#xff0c;线上部分商家会选择进驻到电商平台及外卖平台&#xff0c;但收获流量的同时也有高昂的流量费、抽成等成本的支出&#xff0c;难以外部宣传及内部打通流程、较…

电脑工具远程定时任务关机开机

使用方法 定时跟远程是两回事情不要搞混了 定时 不需要 扫码登录 直接就可以 软件设置 时间 到规定时间 就自动关机 远程操作 关机 锁屏 只要扫码登录软件挂后台就可以远程操作了 用自己手机微信扫码登录 后发送&#xff08;口令&#xff09;到文件传输助手 就可以看到口令…

电子技术基础(三)__第1章电路分析基础_第13篇__正弦交流电的相量表示

本文讲解 正弦交流电的稳态分析————正弦量的相量表示 一 基本概念 接下来&#xff0c; 注意: 大写字母 上 加点 表示相量 例如&#xff1a; 因为这里有 I m I_{m} Im​ 是幅值&#xff0c; 所以此相量称为幅值相量。 相量 其实就是一个复数&#xff0c; 表示正弦量的复…

小鹏:交出最差的财报,展现最膨胀的信心

上市三年&#xff0c;小鹏在今年第二季度交出了几乎是史上最差的财报&#xff0c;多项惨烈的数据叠加在一起&#xff0c;远远望去&#xff0c;就像一张病危通知单。 自2020年上市后&#xff0c;小鹏的扩张速度令资本惊叹&#xff0c;截至2023年6月30日&#xff0c;小鹏的门店数…

IntelliJ IDEA使用_Debug操作

文章目录 版本说明图标和快捷键查看变量计算表达式条件断点多线程调试 版本说明 当前的IntelliJ IDEA 的版本是2021.2.2&#xff08;下载IntelliJ IDEA&#xff09; ps&#xff1a;不同版本一些图标和设置位置可能会存在差异&#xff0c;但应该大部分都差不多。 图标和快捷键…

APP自动化之weditor工具

由于最近事情颇多&#xff0c;许久未更新文章。大家在做APP自动化测试过程中&#xff0c;可能使用的是Appium官方提供的inspect进行元素定位&#xff0c;但此工具调试不方便&#xff0c;于是今天给大家分享一款更好用的APP定位元素工具&#xff1a;weditor weditor基于web网页…

前端请求接口地址反向代理后 状态码404、反向代理注意事项

目录 1. 反向代理代码&#xff08;有问题&#xff09;问题 2. 问题排查原因【排查步骤】【问题1】产生原因【问题2】产生原因【附&#xff1a;排查代码】 3. 总结 - 解决代码4. 总结 - 反向代理 1. 反向代理代码&#xff08;有问题&#xff09; 接口封装 export function Get…

JenniAI:基于AI的文章生成器写作助手

【产品介绍】 • 名称 JenniAI • 成立/上线时间 2022年 • 具体描述 Jenni AI是一个基于AI的文章写作助手&#xff0c;通过使用先进的人工智能技术帮助用户更快、更好、更有创意地写作。无论用户需要写一篇论文、一篇博客、一封邮件、…

68、Spring Data JPA 的 方法名关键字查询

★ 方法名关键字查询&#xff08;全自动&#xff09; &#xff08;1&#xff09;继承 CrudRepository 接口 的 DAO 组件可按特定规则来定义查询方法&#xff0c;只要这些查询方法的 方法名 遵守特定的规则&#xff0c;Spring Data 将会自动为这些方法生成 查询语句、提供 方法…

Echarts 折线图的详细配置过程

文章目录 折线图 简介配置步骤简易示例 折线图 简介 Echarts是一款基于JavaScript的开源可视化库&#xff0c;由百度开发和维护。它提供了丰富多样的图表类型&#xff0c;其中折线图是其中一种常用的图表类型。 折线图通过连接数据点所形成的折线来展示数据的变化趋势。在折线…

华为云云耀云服务器L实例评测 | 购买流程及使用教程

目录 前言服务器购买进入购买界面选择服务器配置进行支付购买流程体验 控制台界面进入控制台控制台界面设置初始密码安全组配置 服务器使用使用xshell连接安装常用包实际测试 总结 前言 一直在考虑购买何种服务器&#xff0c;像我正在读研究生&#xff0c;如果想要在linux部署…

设计模式之代理模式的懂静态代理和动态代理

目录 1 概述1.1 如何实现&#xff1f;1.2 优点1.3 缺点1.4 适用场景 2 静态代理实现3 JDK 动态代理实现4 CGlib 动态代理实现5 总结 1 概述 代理模式&#xff08;Proxy Pattern&#xff09;是一种结构型设计模式&#xff0c;它的概念很简单&#xff0c;它通过创建一个代理对象来…

便捷又炸街!Mate 60的智感支付,是如何做到快速又安全的?

扫码支付已成为线下消费的主流付款方式&#xff0c;平时出门&#xff0c;手机一带&#xff0c;钱包拜拜&#xff01; 以微信支付为例&#xff0c;正常线下支付&#xff0c;手机解锁状态下&#xff1a; 第一步&#xff1a;找到微信APP&#xff1b; 第二步&#xff1a;打开右上…

9月15日作业

Qt代码 #include "mywnd.h"//构造函数的定义 mywnd::mywnd(QWidget *parent): QWidget(parent) //显性调用父类的有参构造完成对子类从父类继承下来成员的初始化工作 {//窗口设置this->resize(QSize(500, 433));this->setWindowTitle("Widget&quo…

MySQL-Linux安装、卸载:

MySQL8.0.26-Linux版安装 1. 准备一台Linux服务器 云服务器或者虚拟机都可以; Linux的版本为 CentOS7; 2. 下载Linux版MySQL安装包 https://downloads.mysql.com/archives/community/ 3. 创建目录&#xff0c;上传MySQL安装包 /usr/local/src/MySql/ 4. 解压 # 进入到…