当前位置: 首页 > news >正文

C++-第十二章: AVL树

目录

第一节:AVL树的特征

第二节:实现思路

        2-1.插入

                2-1-1.右单旋

                2-1-2.左单旋

                2-1-3.左右双旋

                2-1-4.右左双旋

                2-1-5.总结

        2-2.删除

第三节:代码实现

        3-1.Node类

        3-2.AVLTree类

                3-2-1.Insert函数

                3-2-2.Height函数

                3-2-3.Balance函数

                3-2-4.其他函数

                 3-2-5.clear函数

第四节:测试

第五节:优化方案

gitee:AVL树 · 转调/C++ - 码云 - 开源中国 

第六章:总结

下期预告:


第一节:AVL树的特征

        如果一个搜索二叉树的结构如下:

                                

        那么当我想找a的时候,时间复杂度就从 log₂n 退化成 n 了。

        究其原因,搜索二叉树没有一个机制让树的整体高度变得更低。

        所以就出现了AVL树,AVL树通过保证每个节点的左右子树高度差不超过[-1,1],保证整棵树的平衡。

        将左右子树高度差抽象成平衡因子,平衡因子=右子树高度-左子树高度,每个节点都要维护自己的平衡因子不超过1。

第二节:实现思路

        2-1.插入

        1.首先按照搜索二叉树的规则进行插入

        2.插入后更新其父亲节点的平衡因子

                a.插入父亲节点的左子树,父亲节点平衡因子--

                b.插入父亲节点的右子树,父亲节点平衡因子++

        3.如果父亲节点平衡因子是-1或者1,说明父亲所在子树的高度改变了,继续更新父亲的父亲节点的平衡因子,若父亲的父亲节点的平衡因子也为-1或者1,继续向上更新,直到平衡因子不为-1或者1;

        如果某个祖先节点的平衡因子更新后为0,说明祖先节点所在子树高度不变了,对祖先之上的节点也没有影响了,不再往上更新;

        如果某个祖先节点的平衡因子为-2或者2,就需要进行旋转处理了:

                2-1-1.右单旋

        当前节点平衡因子为-2 && 其左孩子的平衡因子为-1 进行右单旋

        用长方形表示子树高度,b、c的高度都为h,a的高度为h+1,红色方块为节点新增位置,蓝色字体为节点的平衡因子:

                                        

        60的平衡因子为-2,并且其左孩子30的平衡因子为-1,进行右单旋

        具体的方法是将60的左孩子变成30的右孩子,30的右孩子被"抢走了",就把60变成30的右孩子,最后把60的父亲变成30的父亲。

        这样做之后30和60的平衡因子都变成了0,就不需要再向上更新了。

        可以发现30向右旋转把60顶替了,60向右旋转屈居30,a、b、c从左向右的顺序依然不变。

                2-1-2.左单旋

        当前节点平衡因子为+2 && 其右孩子的平衡因子为+1

                                                

        30的平衡因子为+2,并且其右孩子60的平衡因子为+1,进行左单旋

        

        这样做之后30和60的平衡因子都变成了0,就不需要再向上更新了。

        可以发现60向左旋转把30顶替了,30向左旋转屈居30,a、b、c从左向右的顺序依然不变。

                2-1-3.左右双旋

        当前节点平衡因子为-2 && 其左孩子的平衡因子为+1时进行左右双旋。

        左右双旋有三种不同的情况,它们的旋转逻辑是一样的,但是最后的平衡因子不同。

        旋转逻辑:

        先不看它们的平衡因子,只看节点和子树的交换关系,此时a=h,d=h,b和c有一个等于h-1,另一个等于h,才能保证90的平衡因子为-2,30的平衡因子为1。

            其实就是孙子变成了它爸爸和爷爷共同的父亲,然后仍然保持a、b、c、d的左右顺序 

        (1)如果60自己就是新增的节点

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​    

        

        3个节点的平衡因子都为0,不再向上更新。

        为了便于理解,我使用绿色字母表示新增节点后a、b、c、d的高度。

        (2)如果60的左子树新增节点

        此时60的平衡因子为0,不再向上更新,30的平衡因子为0,90的平衡因子为1

        (3)如果60的右子树新增节点

        此时60的平衡因子为0,不再向上更新,30的平衡因子为-1,90的平衡因子为0

                2-1-4.右左双旋

        当前节点平衡因子为+2 && 其右孩子的平衡因子为-1时进行左右双旋。

        左右双旋也有三种不同的情况,它们的旋转逻辑是一样的,但是最后的平衡因子不同。

         旋转逻辑:

        

        也是孙子变成了它爸爸和爷爷共同的父亲,然后仍然保持a、b、c、d的左右顺序 ,这和左右双旋的结果一致。 

         所以如果是60的左子树b新增节点,旋转后,30、60、90的平衡因子分别是0、0、1;

        如果是60的右子树c新增节点,旋转后,30、60、90的平衡因子分别是-1、0、0;

        如果60本身是新增的节点,旋转后,30、60、90的平衡因子都是0。

                2-1-5.总结

        只要进行旋转后,"辈分"最高的节点的平衡因子都变成了0,说明只要进行过旋转,就不需要再往上更新了。

第三节:代码实现

        将以上思路整理成代码。

        3-1.Node类

        首先创建一个节点类:

        它除了节点的连接和保存的值之外,还需要一个变量_bf来保存平衡因子。

template<class T>
class Node
{
public:
	Node<T>* _left = nullptr;
	Node<T>* _right = nullptr;
	Node<T>* _parent = nullptr;
	T _val;
	short _bf = 0;
};

        3-2.AVLTree类

        它是AVL树的具体实现。

class AVLTree
{
private:
	Node<T>* _root = nullptr; // 保存根节点
};

                3-2-1.Insert函数

        首先完成插入函数,它传入一个值,然后按照搜索二叉树的思路插入节点:

		void Insert(const T& val)
		{
			// 如果根节点为空,赋值根节点即可
			if (_root == nullptr)
			{
				_root = new Node<T>;
				_root->_val = val;
				return;
			}
			Node<T>* cur = _root;
			Node<T>* parent = nullptr;
			while (cur)
			{
				if (cur->_val < val)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_val > val)
				{
					parent = cur;
					cur = cur->_left;
				}
			}

			cur = new Node<T>;
			cur->_val = val;
			if (parent->_val < val)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			cur->_parent = parent;
			
			// 更新祖先节点的平衡因子
			while (parent)
			{
				if (cur == parent->_left)
				{
					parent->_bf--;
				}
				else
				{
					parent->_bf++;
				}
				// 继续更新
				if (abs(parent->_bf) == 1)
				{
					cur = parent;
					parent = parent->_parent;
					continue;
				}
				// 不再更新
				if (parent->_bf == 0)
				{
					break;
				}
				// 如果失衡,进行旋转
				if (abs(parent->_bf) > 1)
				{
					Rotate(parent);
					break;
				}
			}
		}

                3-2-2.Height函数

        它获取某个节点的高度,获得某个节点左右孩子的高度后,就可以计算_bf:

		size_t _Height(Node<T>* root)
		{
			if (root == nullptr) return 0;
			return std::max(_Height(root->_left), _Height(root->_right)) + 1;
		}
		size_t Height(Node<T>* root)
		{
			return _Height(root);
		}

                3-2-3.Balance函数

        AVL树的核心函数,它用来更新节点的平衡因子:

void Rotate(Node<T>* cur)
{
		// 进行旋转处理,旋转完也不需要继续更新了
		if (cur->_bf == -2 && cur->_left->_bf == -1) // 右单旋
		{
			Node<T>* P = cur;      // 父亲
			Node<T>* S = P->_left; // 儿子
			P->_left = S->_right;
			if (S->_right)
				S->_right->_parent = P;
			S->_right = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}
		else if (cur->_bf == 2 && cur->_right->_bf == 1) // 左单旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			P->_right = S->_left;
			if (S->_left)
				S->_left->_parent = P;
			S->_left = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}
		else if (cur->_bf == -2 && cur->_left->_bf == 1) // 左右双旋
		{
			Node<T>* P = cur;      // 父亲
			Node<T>* S = P->_left; // 儿子
			Node<T>* G = S->_right;// 孙子
			P->_left = G->_right;
			if (G->_right)
				G->_right->_parent = P;
			G->_right = P;

			S->_right = G->_left;
			if (G->_left)
				G->_left->_parent = S;
			G->_left = S;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = G;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = G;
				}
			}
			else
			{
				_root = G;
			}
			G->_parent = P->_parent;
			P->_parent = G;
			S->_parent = G;
			if (G->_bf == 1)
			{
				S->_bf = -1;
				P->_bf = 0;
			}
			else if (G->_bf == -1)
			{
				S->_bf = 0;
				P->_bf = 1;
			}
			else if (G->_bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}
		else if (cur->_bf == 2 && cur->_right->_bf == -1) // 右左双旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			Node<T>* G = S->_left;
			P->_right = G->_left;
			if (G->_left)
				G->_left->_parent = P;
			G->_left = P;

			S->_left = G->_right;
			if (G->_right)
				G->_right->_parent = S;
			G->_right = S;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = G;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = G;
				}
			}
			else
			{
				_root = G;
			}
			G->_parent = P->_parent;
			P->_parent = G;
			S->_parent = G;
			if (G->_bf == 1)
			{
				S->_bf = 0;
				P->_bf = -1;
			}
			else if (G->_bf == -1)
			{
				S->_bf = 1;
				P->_bf = 0;
			}
			else if (G->_bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}
}

                3-2-4.其他函数

        Print 和 IsBalance 用来按升序打印内容和判断每个节点是否都平衡:

	bool IsBalance()
	{
		return _IsBalance(_root);
	}
	void Print()
	{
		_Print(_root);
	}

	bool _IsBalance(Node<T>* root)
	{
		if (root == nullptr) return true;
		if (abs(root->_bf) > 1) return false;
		return _IsBalance(root->_left) && _IsBalance(root->_right);
	}
	void _Print(Node<T>* root)
	{
		if (root == nullptr) return;
		_Print(root->_left);
		std::cout << root->_val << " ";
		_Print(root->_right);
	}

                 3-2-5.clear函数

        因为节点都是new出来的,所以使用清理函数释放空间,析构函数也要调用这个函数:

		void _clear(Node<T>* root)
		{
			if (root == nullptr) return;
			_clear(root->_left);
			_clear(root->_right);
			delete root;
		}
        void clear()
		{
			_clear(_root);
		}
		~AVLTree()
		{
			clear();
		}
		

第四节:测试

        插入多个随机数,然后调用 Print 和 IsBalance。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#include "AVLTree.hpp"

int main()
{
	zd::AVLTree<int> tree;
	srand(time(NULL));
	int i = 100;
	while (i--)
	{
		int x = rand();
		tree.Insert(x);
	}
	tree.Print();
	if (tree.IsBalance())
	{
		printf("true\n");
	}
	else
	{
		printf("false\n");
	}
	return 0;
}

        观察打印结果和最后是否为true即可。

第五节:优化方案

        实际上左右双旋和右左双旋就是左单旋和右单旋的组合,所以可以将左单旋、右单旋封装成函数,供左右单旋和右左单旋调用。

        但是封装的时候G的_bf需要提前保存,因为调用两次单旋之后,G的_bf就改变了:

		void RotateR(Node<T>* cur) // 右单旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_left;
			P->_left = S->_right;
			if (S->_right)
				S->_right->_parent = P;
			S->_right = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}

		void RotateL(Node<T>* cur) // 左单旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			P->_right = S->_left;
			if (S->_left)
				S->_left->_parent = P;
			S->_left = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}

		void RotateLR(Node<T>* cur) // 左右双旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_left;
			Node<T>* G = S->_right;
			short bf = G->_bf;

			RotateL(S);
			RotateR(P);

			if (bf == 1)
			{
				S->_bf = -1;
				P->_bf = 0;
			}
			else if (bf == -1)
			{
				S->_bf = 0;
				P->_bf = 1;
			}
			else if (bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}

		void RotateRL(Node<T>* cur) // 右左双旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			Node<T>* G = S->_left;
			short bf = G->_bf;

			RotateR(S);
			RotateL(P);

			if (bf == 1)
			{
				S->_bf = 0;
				P->_bf = -1;
			}
			else if (bf == -1)
			{
				S->_bf = 1;
				P->_bf = 0;
			}
			else if (bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}

        用以上接口替换Rotate中的对应代码即可。

Gitee:AVL树 · 转调/C++ - 码云 - 开源中国 

第六章:总结

        单旋时,向左会被旋转到向右,向右会被旋转到向左,但是a、b、c的顺序不变

        双旋时,两种双旋的结果一样,a、b、c、d的顺序也不变,而且双旋可以拆分成两次单旋:S先旋、P后旋。 

        负二左负右单旋,反之正一左右旋;正二右正左单旋,反之负一右左旋;两个双旋子先动;a、b、c、d顺序齐。 

下期预告:

        第十三章将介绍另外一种搜索二叉树——红黑树。

相关文章:

  • 入门基础项目(SpringBoot+Vue)
  • python-leetcode-第 N 个泰波那契数
  • Denoising Diffusion Implicit Models (DDIM)
  • Python PDF文件拆分-详解
  • 抖音生活服务加强探店内容治理,2024年达人违规率下降30%
  • 2025年4月1日-2日AutoCable 中国汽车线束线缆及连接技术创新峰会即将开幕
  • 大模型WebUI:Gradio全解12——LangChain原理、架构和组件(2)
  • 2025 年度福建省职业院校技能大赛中职组“网络建设与运维”赛项规程
  • MySQL优化示例 1:关联查询
  • vue3-tree-org创建组织架构图简单案例分享
  • scala基础
  • 剧本杀APP系统开发,市场发展前景
  • 计算机毕业设计SpringBoot+Vue.js基于工程教育认证的计算机课程管理平台(源码+文档+PPT+讲解)
  • 搭建一个私有NuGet服务器
  • 【AI深度学习基础】NumPy完全指南进阶篇:核心功能与工程实践(含完整代码)
  • OpenCV计算摄影学(10)将一组不同曝光的图像合并成一张高动态范围(HDR)图像的实现类cv::MergeDebevec
  • GEE学习笔记 28:基于Google Earth Engine的Landsat8缨帽变换土壤指数反演——亮度、绿度与湿度分量的提取
  • 白话React第九章React 前沿技术与企业级应用实战
  • 跨平台文件互传工具
  • python-leetcode-斐波那契数
  • 临沂网站建设周口/网络推广公司名字大全
  • 中小型网站建设代理商/百度首页纯净版怎么设置
  • 汉化主题做网站效果图/广州疫情最新情况
  • 北京顺义去哪找做网站的/重庆网站制作公司哪家好
  • 网站备案在哪个网/百度站长之家
  • 一个云主机怎么挂两个网站/搜索百度app下载