深入浅出C++ ——手撕AVL树

news2025/1/23 22:25:21

文章目录

  • 前言
  • 一、AVL 树介绍
  • 二、AVL树节点的定义
  • 三、AVL树的插入
  • 四、AVL树的旋转
  • 五、AVL树的验证
  • 六、AVL树的删除
  • 七、AVL树的性能
  • 八、AVL树的实现

前言

  在前面的文章中介绍了map / multimap / set / multiset 容器,这几个容器的底层都是按照二叉搜索树来实现的。但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N)。因此 map、set 等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。


一、AVL 树介绍

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

  AVL树是一棵二叉搜索树,且高度平衡,因此AVL树也叫高度平衡二叉搜索树。如果AVL树有n个结点,其 高度可保持在 O ( l o g 2 N ) O(log_2 N) O(log2N),搜索时间复杂度 O ( l o g 2 N ) O(log_2 N) O(log2N)


AVL树的性质

  • 任意一颗子树的左右子树都是AVL树
  • 任意一颗子树的左右子树高度之差(简称平衡因子)的绝对值不超过1

二、AVL树节点的定义

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. 先按照二叉搜索树的规则将节点插入到AVL树中
  2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树的平衡性。
bool Insert(const pair<K, V>& kv)
{
	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->_right;
		}
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	// 插入节点
	cur = new Node(kv);

	// 链接cur和parent
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

	cur->_parent = parent;

	// 控制平衡
	while (parent)	//最坏的情况是更新到根才会停止
	{
		// 更新平衡因子。新增在右,parent的平衡因子++ ;新增在左,parent的平衡因子--
		if (cur == parent->_right)
			parent->_bf++;
		else
			parent->_bf--;

		if (parent->_bf == 0)//更新后,parent的平衡因子为0,说明插入后两边一样高,插入填入了矮的部分,parent所在子树高度不变,不需要继续往上更新。
		{
			break;
		}
		else if (abs(parent->_bf) == 1)//更新后,abs(parent的平衡因子)为1,说明插入后有一边高,parent所在子树高度变了,需要继续往上更新。
		{
			parent = parent->_parent;
			cur = cur->_parent;
		}
		else if (abs(parent->_bf) == 2)//更新后,abs(parent的平衡因子)为2,说明已经打破平衡,parent所在子树需要旋转处理。
		{
			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;	//一次插入只会有一次旋转,旋转完成了直接break
		}
		else //更新后,abs(parent的平衡因子)大于2,说明插入前就不是AVL树,需要检查之前的操作
		{
			assert(false);
		}
	}
	return true;
}

四、AVL树的旋转

旋转原则:1. 旋转为平衡树 2. 保持搜索树规则


新节点插入较高右子树的右侧—右右:左单旋
在这里插入图片描述

// 新节点插入较高右子树的右侧---右右:左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)						//subRL可能为空
		subRL->_parent = parent;

	Node* ppNode = parent->_parent; //注意:有可能parent不是根节点,保存上一层节点

	subR->_left = parent;
	parent->_parent = subR;

	if (_root == parent)			//parent是整棵树的根
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else                            //parent是子树的根
	{
		if (ppNode->_left == parent)//判断上一层节点和parent的关系
		{
			ppNode->_left = subR;
		}
		else
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}

	subR->_bf = parent->_bf = 0;	//修改平衡因子
}

新节点插入较高左子树的左侧—左左:右单旋
在这里插入图片描述

// 新节点插入较高左子树的左侧---左左:右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

	Node* ppNode = parent->_parent;

	subL->_right = parent;
	parent->_parent = subL;

	if (_root == parent)
	{
		_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;
}

新节点插入较高左子树的右侧—左右:先左单旋再右单旋
在这里插入图片描述
  在b或者c的位置插入,都会引起bf的变化,引发双旋。将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。
在这里插入图片描述

// 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;	//记录subLR的平衡因子,根据它的大小将其他平衡因子的更新分为三种情况

	RotateL(parent->_left); // 先cur左旋再parent右旋
	RotateR(parent);

	subLR->_bf = 0;
	if (bf == 1)			// c插入
	{
		parent->_bf = 0;
		subL->_bf = -1;
	}
	else if (bf == -1)		// b插入
	{
		parent->_bf = 1;
		subL->_bf = 0;
	}
	else if (bf == 0)		//a,b,c,d为空树,subLR为新增
	{
		parent->_bf = 0;
		subL->_bf = 0;
	}
	else					// 说明出问题了
	{
		assert(false);
	}
}

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

整体思路同上:

//新节点插入较高右子树的左侧——右左:先右单旋再左单旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf;

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

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

总结

  假如以Parent为根的子树不平衡,即Parent的平衡因子为2或者-2,分以下情况考虑:

  1. Parent的平衡因子为2,说明Parent的右子树高,设Parent的右子树的根为SubR。当SubR的平衡因子为1时,执行左单旋。当SubR的平衡因子为-1时,执行右左双旋。
  2. Parent的平衡因子为-2,说明Parent的左子树高,设Parent的左子树的根为SubL。当SubL的平衡因子为-1是,执行右单旋。当SubL的平衡因子为1时,执行左右双旋。

  旋转完成后,原Parent为根的子树个高度降低,已经平衡,不需要再向上更新。


旋转的意义

  1. 平衡
  2. 降高度

五、AVL树的验证

验证其为二叉搜索树

  如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
	
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

验证其为平衡树

  每个节点子树高度差的绝对值不超过1,节点的平衡因子是否计算正确

publicbool IsBalance()
	{
		return _IsBalance(_root);
	}
	
private:
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		return max(Height(root->_left), Height(root->_right)) + 1;
	} 
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
	
		int leftHT = Height(root->_left);
		int rightHT = Height(root->_right);
		int diff = rightHT - leftHT;
	
		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
	
		return abs(diff) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

六、AVL树的删除

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


七、AVL树的性能

  AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度 O ( l o g 2 N ) O(log_2 N) O(log2N)

  但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

  因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的,可以考虑AVL树,但一个结构经常修改,就不太适合。


八、AVL树的实现

#include<iostream>
#include<assert.h>
#include <map>
#include <string>
#include <algorithm>
#include<time.h>
#include <assert.h>
using namespace std;


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;					// 该节点的平衡因子
};

template<class K, class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		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->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		// 插入节点
		cur = new Node(kv);

		// 链接cur和parent
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;

		// 控制平衡
		while (parent)	//最坏的情况是更新到根才会停止
		{
			// 更新平衡因子。新增在右,parent的平衡因子++ ;新增在左,parent的平衡因子--
			if (cur == parent->_right)
				parent->_bf++;
			else
				parent->_bf--;


			if (parent->_bf == 0)//更新后,parent的平衡因子为0,说明插入后两边一样高,插入填入了矮的部分,parent所在子树高度不变,不需要继续往上更新。
			{
				break;
			}
			else if (abs(parent->_bf) == 1)//更新后,abs(parent的平衡因子)为1,说明插入后有一边高,parent所在子树高度变了,需要继续往上更新。
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (abs(parent->_bf) == 2)//更新后,abs(parent的平衡因子)为2,说明已经打破平衡,parent所在子树需要旋转处理。
			{
				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;	//一次插入只会有一次旋转,旋转完成了直接break
			}
			else //更新后,abs(parent的平衡因子)大于2,说明插入前就不是AVL树,需要检查之前的操作
			{
				assert(false);
			}
		}
		return true;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool IsBalance()
	{
		return _IsBalance(_root);
	}
private:
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHT = Height(root->_left);
		int rightHT = Height(root->_right);
		int diff = rightHT - leftHT;

		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		return abs(diff) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		return max(Height(root->_left), Height(root->_right)) + 1;
	}

	// 新节点插入较高右子树的右侧---右右:左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)						//subRL可能为空
			subRL->_parent = parent;

		Node* ppNode = parent->_parent; //注意:有可能parent不是根节点,保存上一层节点

		subR->_left = parent;
		parent->_parent = subR;

		if (_root == parent)			//parent是整棵树的根
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else                            //parent是子树的根
		{
			if (ppNode->_left == parent)//判断上一层节点和parent的关系
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}

		subR->_bf = parent->_bf = 0;	//修改平衡因子
	}

	// 新节点插入较高左子树的左侧---左左:右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

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

		Node* ppNode = parent->_parent;

		subL->_right = parent;
		parent->_parent = subL;

		if (_root == parent)
		{
			_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;
	}

	// 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;	//记录subLR的平衡因子,根据它的大小将其他平衡因子的更新分为三种情况

		RotateL(parent->_left); // 先cur左旋再parent右旋
		RotateR(parent);

		subLR->_bf = 0;
		if (bf == 1)			// c插入
		{
			parent->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == -1)		// b插入
		{
			parent->_bf = 1;
			subL->_bf = 0;
		}
		else if (bf == 0)		//a,b,c,d为空树,subLR为新增
		{
			parent->_bf = 0;
			subL->_bf = 0;
		}
		else					// 说明出问题了
		{
			assert(false);
		}
	}

	//新节点插入较高右子树的左侧——右左:先右单旋再左单旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		int bf = subRL->_bf;

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

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

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

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

void TestAVLTree1()
{
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };  // 测试双旋平衡因子调节
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t1;
	for (auto e : a)
	{
		t1.Insert(make_pair(e, e));
	}
	t1.InOrder();
	cout << "IsBalance:" << t1.IsBalance() << endl;
}

void TestAVLTree2()
{
	size_t N = 10000;
	srand(time(0));
	AVLTree<int, int> t1;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand();
		t1.Insert(make_pair(x, i));
	}
	cout << "IsBalance:" << t1.IsBalance() << endl;
}

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

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

相关文章

paddlepaddle目标检测

目录 1 参考链接 2 环境 3 数据集准备 4 训练 train.py 5 导出预测模型 6 预测 源码来自作者 夜雨飘零1&#xff0c;我对参考链接的代码略有修改&#xff0c;网盘地址 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;ipl5 1 参考链接 博客地址 基…

Linux 实现鼠标侧边键实现代码与网页的前进、后退

前言 之前一直是使用windows进行开发&#xff0c;最近转到linux后使用VsCode编写代码。 但是不像在win环境下&#xff0c;使用鼠标侧边键可以实现代码的前向、后向跳转。浏览网页时也不行&#xff08;使用Alt Left可以后退&#xff09;。 修改键盘映射实在没有那么方便&…

文案女王彭芳如何转变为“百万发售系统”创始人?我们来探个究竟!

智多星老师 她的输出跟智多星老师几乎毫无二致&#xff0c;是抄袭还是纯属巧合呢&#xff1f; 你们问的这个问题我也想知道&#xff0c;为了了解真相&#xff0c;我让我的一个学生把那个叫“彭芳老师”的视频给我看&#xff0c;当看到她的简介时&#xff0c;我非常震惊&#…

启智社区“我为开源狂”第六期活动小白教程之基础活跃榜

一、写在前面 春天来啦~启智社区第六期活动也来啦&#xff01; 有奖金的哦~~ 基础活跃榜奖金根据用户活跃程度进行100-300元的激励。 挑战升级榜需要用户完成相应任务&#xff0c;达标者可获得300-1000元的激励。 邀请助力榜根据用户邀请情况进行积分累加&#xff0c;按实际达…

游戏策划想要了解编程和引擎是应该从unity入手还是ue4入手?

建议 考虑自身的职业规划考虑本公司引擎使用情况考虑自身兴趣爱好学习引擎的同时多拆解市面上主流游戏、做游戏数据及系统分析 区别 除去以上内容&#xff0c;说下unity和ue的学习及使用区别&#xff1a; 适用类型&#xff1a; 3D – 两个引擎都具有强大的3D功能&#xff0…

ctcdecode安装

一、写在前面&#xff1a;ctcdecode代码较早&#xff0c;安装过程有许多坑。本文章为ctcdecode安装成功的记录&#xff0c;可能存在不适用的情况&#xff0c;欢迎大家补充。二、致谢&#xff1a;感谢文章https://blog.csdn.net/u011550545/article/details/87926995提供的宝贵参…

HashMap(JDK1.8)源码+底层数据结构分析

HashMap 简介底层数据结构分析 JDK1.8 之前JDK1.8 之后 HashMap 源码分析 构造方法put 方法get 方法resize 方法 HashMap 常用方法测试 感谢 changfubai 对本文的改进做出的贡献&#xff01; HashMap 简介 HashMap 主要用来存放键值对&#xff0c;它基于哈希表的 Map 接口实现…

【React npm】从零搭建react脚手架,发布组件库到npm,并实现按需加载(二)

发布react组件库前情回顾介绍搭建脚手架配置babelrc配置jsconfig写入组件demo修改主入口文件配置生产环境webpack配置package.json发布实现按需加载前情回顾 前面写过一篇&#xff0c;发布单个组件到npm的&#xff1a; https://blog.csdn.net/tuzi007a/article/details/12911…

Anaconda环境配置

1.进入清华大学镜像网站Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror&#xff0c;下载稳定版Anaconda3-5.2.0&#xff0c;如下图。2.放到整理好的文件夹中&#xff0c;双击安装包进行安装。3.安装过程中需要改变的默认值如下&#xff…

Linux 基础知识之文件系统

目录一、文件系统1.文件种类2.Linux和Windows文件后缀的不同3.查看文件类型3.绝对路径与相对路径二、系统分区三、目录结构一、文件系统 1.文件种类 Linux中一切皆文件。目光所及&#xff0c;皆是文件。文件的种类共有七种&#xff0c;每种文件都有自己的独特标识&#xff1a;…

MYSQL 密码修改 (四种方式)

注 &#xff1a; 我们所谓的密码修改肯定是先指的是 你已经清楚用户的原密码&#xff0c;是对原密码进行了修改并不是你忘记了密码&#xff0c;然后设置新密码~&#xff01;&#xff01;方式一 &#xff1a; 使用 mysqladmin示例 &#xff1a; [rootbogon ~]# mysqladmin -uroo…

python文件编译为pyc后运行

一、pyc文件我们开发一个python脚本&#xff0c;文件的后缀为.py。如果运行这个py文件&#xff0c;Python内部会先将源码文件&#xff08;.py文件&#xff09;编译成字节码&#xff08;byte code&#xff09;文件&#xff08;.pyc文件&#xff09;。接着运行编译后的字节码&…

【Spark分布式内存计算框架——离线综合实战】5. 业务报表分析

第三章 业务报表分析 一般的系统需要使用报表来展示公司的运营情况、 数据情况等&#xff0c;本章节对数据进行一些常见报表的开发&#xff0c;广告数据业务报表数据流向图如下所示&#xff1a; 具体报表的需求如下&#xff1a; 相关报表开发说明如下&#xff1a; 第一、数据…

【总结】python3启动web服务引发的一系列问题

背景 在某行的实施项目&#xff0c;需要使用python3环境运行某些py脚本。 由于行内交付的机器已自带python3 &#xff0c;没有采取自行安装python3&#xff0c;但是运行python脚本时报没有tornado module。 错误信息 ModuleNotFoundError&#xff1a;No module named ‘torn…

Unity截屏时将背景的透明度设为0

常用的截屏函数是&#xff1a; UnityEngine.ScreenCapture.CaptureScreenshot(fileName, 5); //5代表dpi大小&#xff0c;数字越大越清晰但是这样保存图片是不能将黑色背景的透明度设为0&#xff0c;最终还是24bit图。 如果将背景透明度设为0而渲染物体透明度设为255&#xff…

学插画的线上机构排名

学插画哪个线上机构好&#xff0c;5个靠谱的插画网课推荐&#xff01;给大家梳理了国内5家专业的插画师培训班&#xff0c;最新5大插画班排行榜&#xff0c;各有优势和特色&#xff01; 一&#xff1a;插画线上培训机构排名 1、轻微课&#xff08;五颗星&#xff09; 主打课程有…

【C语言】函数栈帧的创建与销毁

Yan-英杰的主页 悟已往之不谏 知来者之可追 目录 ​0.ebp和esp是如何来维护栈帧的呢&#xff1f; 1.为什么局部变量的值不初始化是随机的&#xff1f; ​2.局部变量是怎么创建的&#xff1f; ​3 .函数是如何传参的&#xff1f;传参的顺序是怎样的 4.函数是如何调用的 ​…

scrapy-redis分布式爬虫学习记录

目录 1. scrapy-redis是什么&#xff1f; 2. scrapy-redis工作原理 3.分布式架构 4. scrapy-redis的源码分析 5. 部署scrapy-redis 6. scrapy-redis的基本使用 6.1 redis数据库基本表项 6.2 在scrapy项目的基础进行更改 7. redis数据转存入mysql数据库 课程推荐&#…

大学生成人插画培训机构盘点

成人插画培训机构哪个好&#xff0c;成人学插画如何选培训班&#xff1f;给大家梳理了国内较好的插画培训机构排名&#xff0c;各有优势和特色&#xff0c;供大家参考&#xff01; 一&#xff1a;国内成人插画培训机构排名 1、轻微课&#xff08;五颗星&#xff09; 主打课程有…

Head First设计模式---3.装饰者模式

3.1装饰者模式 亦称&#xff1a; 装饰者模式、装饰器模式、Wrapper、Decorator 装饰模式是一种结构型设计模式&#xff0c; 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 举个例子&#xff1a;天气很冷&#xff0c;我们一件一件穿衣服&#xff0c…