【C++】AVL(平衡二叉搜索树)树的原理及实现

news2024/11/26 2:48:27

文章目录

一、引言

二、AVL树的概念

三、AVL树的插入

3、1 AVL树的简单插入

3、2 旋转分类

 3、2、1 新节点插入较高右子树的右侧:左单旋

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

3、2、3 新节点插入较高左子树的右侧:左右双旋(先左后右)

3、2、4 新节点插入较高右子树的左侧:右左双旋(先右后左)

四、检查AVL树

4、1 高度差与平衡因子对比 

4、2 不同的测试用例进行测试

五、性能分析

4.1 查找操作的复杂度

4.2 插入操作的复杂度

六、总结


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:C++、数据结构 👀

💥 标题:AVL树💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️ 

一、引言

   二叉搜索树 虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下
  因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。因此就诞生出了AVL树。
   AVL树是一种自平衡二叉搜索树,它在查找、插入和删除操作中都能保持较好的性能。它的命名来自于它的发明者,Adelson-Velsky和Landis。AVL树的特点是通过旋转操作来保持平衡,使得任何一个节点的左右子树高度差不超过1

  本文将介绍AVL树的概念、实现以及性能分析。首先,我们将解释AVL树的结构和基本概念。然后,我们将详细讨论如何实现AVL树,并提供C++语言的示例代码。最后,我们将对AVL树的性能进行分析。

二、AVL树的概念

  AVL树是一种二叉搜索树,它的每个节点包含一个关键字(键值对)、左右孩子指针和父结点指针。每个节点还包含一个平衡因子(balance factor),用于表示该节点的左右子树高度差(平衡因子=左子树高度−右子树高度。 

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _kv; //key-value
	int _bf; //balance factor

	AVLTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(kv)
	{}
};

  一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树。
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。
  AVL树的平衡性规则是指任何一个节点的左右子树的高度差不超过1。即对于任意节点,其左子树高度和右子树高度的差绝对值不大于1。

三、AVL树的插入

  本篇文章重点介绍AVL树的插入操作。因为在插入操作中包含了所有重要操作。

3、1 AVL树的简单插入

  插入一个新节点到AVL树中,需要进行以下几个步骤:

  1. 执行二叉搜索树的插入操作,找到新节点应该插入的位置。
  2. 更新相关节点的平衡因子。
  3. 检查是否破坏了平衡性规则,如果破坏了,则进行相应的旋转操作来恢复平衡。

  接着,在新节点插入后,我们需要检查树的平衡性。

  1. 插入新节点后的平衡因子调整:由于插入可能导致从插入节点到根节点的路径上的某些节点的平衡被破坏,我们需要从新插入的节点开始向上遍历,更新所有受影响节点的平衡因子。
  2. 平衡调整:一旦检测到某个节点的平衡因子不在范围[-1, 1]之间,就需要对其进行平衡调整,以确保树的平衡性。根据插入节点所在的子树的情况,可以进行四种类型的旋转操作:左旋、右旋、左右旋和右左旋。
  3. 继续向上检查:在执行旋转操作后,可能会影响到更高层次的节点的平衡性。因此,我们需要继续向上检查,直到根节点为止,以确保整个树的平衡性。

  当我们找到合适位置进行插入后,将该节点进行连接,在更新其父节点及相关节点的平衡因子。如下图例子:

  假设我们在上图中的值为6的节点左孩子插入一个节点。新插入的节点的平衡因子为0。但是其父节点(值为6的节点和值为7的节点)的平衡因子都发生了变化:6的节点和值为7的节点的平衡因子分别变为-1和0。为什么呢?这就与更新平衡因子的规则有关了。

  更新平衡因子的规则:

  1. 新增在右,parent-> bf++;新增在左,parent-> bf--;
  2. 更新后,parent->bf ==1or-1,说明parent插入前的平衡因子是0,说明左右子树高度相等,插入后有一边高,parent高度变了,需要继续往上更新;
  3. 更新后,parent->bf == 0,说明parent插入前的平衡因子是1 or -1,说明左右子树一边高一边低,插入后两边一样高,插入填上了矮了那边,parent所在子树高度不变,不需要继续往上更新;
  4. 更新后,parent->bf == 2 or -2,说明parent插入前的平衡因子是1 or -1,已经平衡临界值,插入变成2 or -2,打破平衡,parent所在子树需要旋转处理。
  5. 更新后,parent->bf 大于 2 或者 小于 -2的值,不可能,如果存在,则说明插入前就不是AVL树,需要去检查之前操作的问题。

  通过对上述的理解,我们可写出如下代码:

template<class K, class V>
class 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.second > kv.second)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		while (parent)
		{
			if (parent->_right == cur)
			{
				parent->_bf++;
			}
			else
			{
				parent->_bf--;
			}

			if (parent->_bf == 0)
			{
				break;
			}
			else if (abs(parent->_bf) == 1)
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (abs(parent->_bf) == 2)
			{
				break;
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}
private:
	Node* _root = nullptr;
};

3、2 旋转分类

  旋转也是有其自己的规则的:

  • 旋转后需要成为平衡树;
  • 旋转不能破环搜索树的规则。

  我们先看如下图的情况:

  我们在值为9的节点右边进行插入,我们发现其父节点和祖宗节点的平衡因子都发生了变化。此时我们是需要进行旋转调节的。但是我们也可以把该节点插入到其他节点下面,然后也可能会需要进行旋转。需要旋转的情况太多,所以我们需要进行分类。

  为了保持树的平衡,AVL树可能需要执行以下四种旋转操作之一:

  • 左旋(LL旋转)
  • 右旋(RR旋转)
  • 先左后右旋(LR旋转)
  • 先右后左旋(RL旋转)

  下面我们来一一解释各种旋转的原理。

 3、2、1 新节点插入较高右子树的右侧:左单旋

  左旋是在一个节点的右子树高度大于左子树高度时进行的旋转操作。通过左旋,可以将右子树的高度降低,使得整棵树重新恢复平衡。

  左旋的依据是,右子树的高度大于左子树的高度,因此需要将右子树中的某个节点旋转到根节点的位置,使得该节点成为新的根节点,并将原来的根节点作为新根节点的左子节点。

  具体需要左旋的情况如下:

  左旋是怎么实现的呢?如下图:

  上图就是把父节点中右树的左孩子给父节点父节点连接到右树的左边。通俗解释,把subRL赋给parent->_right(parent->_right=subRL),然后再将parent连接到subR的左(subR->_left=parent)。

  我们再分析是否符合旋转的规则。首先旋转后的高度是平衡的。其次符合搜索树的规则。但是我们不仅仅要更换节点,还要更新变换的父节点和平衡因子。我们改变了parent这棵树和子树subR。所以我们还需更新他们的父节点和平衡因子。我们看左旋的代码实现:

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
        
        // 旋转
		Node* ppNode = parent->_parent;
		subR->_left = parent;
		parent->_right = subRL;
        
        // 更新父节点 
		if (subRL)  // h=0的情况下,subRL为nullptr
			subRL->_parent = parent;
		parent->_parent = subR;

		if (_root == parent)
		{
			subR->_parent = nullptr;
			_root = subR;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}

			subR->_parent = ppNode;
		}
        
        // 更新平衡因子
		subR->_bf = parent->_bf = 0;
	}

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

  右旋是在一个节点的左子树高度大于右子树高度时进行的旋转操作。通过右旋,可以将左子树的高度降低,使得整棵树重新恢复平衡。

  右旋的依据是,左子树的高度大于右子树的高度,因此需要将左子树中的某个节点旋转到根节点的位置,使得该节点成为新的根节点,并将原来的根节点作为新根节点的右子节点。

  右旋的情况如下如:

  我们再根据下图具体分析一下:

  右旋与左旋转很类似,我们都需要借助 parent 来维护该树的平衡。首先是要把subLR子树移动到parent的左树上。然后,再把parent移动到subL的右树上。操作完这两部后,树就平衡了。 我们可结合下图理解:

   不要忘记旋转后,要更新所变动节点的父节点和平衡因子,我们看代码:

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

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

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

	}

3、2、3 新节点插入较高左子树的右侧:左右双旋(先左后右)

  左右旋是在一个节点的左子树的右子树高度大于左子树高度时进行的旋转操作。左右旋可以通过先进行一次左旋,再进行一次右旋来实现树的平衡。

  左右旋的依据是,左子树的右子树高度大于左子树的高度,导致左子树整体比右子树要高。为了保持树的平衡,需要对左子树进行左旋,然后再对原父节点进行右旋。 

  我们已经讲述了如下两种情况:

   那下种情况该怎么旋转呢?我们不妨先自己想一下看看怎么旋转。

  由于旋转的原因,我们还需再次展开子树b。上述情况又分为如下三种情况:

  上述三种情况只不过是更新平衡因子有所不同,旋转的方法还是相同的。都是先对值为30的子树进行左单旋,然后再对值为90的子树进行右单旋,旋转完成后再 考虑平衡因子的更新。整体的需安装过程如下:

  我们可以再结合着代码理解一下:

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

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

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

	}

3、2、4 新节点插入较高右子树的左侧:右左双旋(先右后左)

  右左双旋与左右双旋大同小异,只不过是在不同的情况下进行了不同顺序的旋转。旋转的思路和情况分类是一模一样的。我们先看一下需要右左双旋的情况:

  同样可按照旋转的需求,划分三种情况(与左右双旋类似)。这里就不在具体画出。具体的旋转过程如下:

  再对不同情况下的平衡因子进行更新,代码如下:

	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);
		}
	}

四、检查AVL树

  当我们自己完成AVL树代码后,我们还需要检查一下是否正确。通过什么方式检查呢? 

4、1 高度差与平衡因子对比 

  我们首先想到的是通过中序遍历打印出来,看其是否是有序的。这样就算验证完成了吗?并不是!!!

  我们不仅仅要验证是否为搜索树,话要看其是否为平衡树。但是验证是否是平衡树时,又该怎么检查是否平衡呢?可不可以通过遍历整棵树,看其平衡因子绝对值是否小于等于1呢?怎么就感觉好像又可以,好像又不太行呢?平衡因子是我们自己更新的,还需要我们自己去检查吗?当然不用!!!我们需要求出该子树的高度差与平衡因子比较看是否相同,这样检验才是比较合理的。

  有了上述的思路,我们在看代码是怎么实现的,代码如下:

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

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

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

	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHigh = TreeHeight(root->_left);
		int rightHigh = TreeHeight(root->_right);
		int subHigh = rightHigh - leftHigh;
		if (root->_bf != subHigh)
		{
			cout << "平衡因子有误:" << root->_kv.first << endl;
			return false;
		}


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

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

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

4、2 不同的测试用例进行测试

  我们先来看一段特殊的测试代码:

void TestAVLTree1()
{
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };  // 测试双旋平衡因子调节
	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;
}

五、性能分析

4.1 查找操作的复杂度

  AVL树的查找操作与普通的二叉搜索树相同,都是按照比较大小的规则在树中查找特定的元素。由于AVL树是平衡二叉搜索树,它保持了左右子树高度的平衡,因此查找操作的平均时间复杂度为O(log n),其中n是树中节点的数量

  在平衡状态下,每一层的节点数大约是前一层的两倍,这保证了树的高度始终保持在logarithmic级别。这种高度平衡的特性使得查找操作的时间复杂度始终保持在O(log n)的范围内,使得AVL树在大量数据的情况下依然能够快速有效地进行查找。

  总的来说,AVL树的查找操作性能非常出色,是其设计的主要优势之一。

4.2 插入操作的复杂度

  在AVL树中进行插入操作时,需要首先执行标准的二叉搜索树的插入操作,将新节点插入到合适的位置。然后,需要进行平衡调整,以确保插入操作不会破坏树的平衡性。

  插入操作的时间复杂度包括两个部分:插入节点的时间复杂度和平衡调整的时间复杂度。插入节点的时间复杂度是O(log n),其中n是树中节点的数量,这是因为AVL树保持了树的高度平衡,使得每一层的节点数大约是前一层的两倍。

  平衡调整的时间复杂度取决于进行的旋转操作次数。最坏情况下,插入一个节点可能需要执行O(log n)次旋转操作,但平均情况下,插入节点的时间复杂度仍然是O(log n)。这是因为在绝大多数情况下,只需要进行少数次旋转操作即可将树恢复平衡。

  总体来说,AVL树的插入操作的平均时间复杂度是O(log n),使得在插入新元素时仍然能够保持高效的性能。

六、总结

  AVL树是一种自平衡的二叉搜索树,其平衡性质使得在查找、插入和删除操作上都能保持较高的性能。通过限制树的平衡因子在一定范围内,AVL树能够在任何时刻保持树的高度平衡,从而避免了退化成链表的情况。

  在AVL树的操作中,插入和删除操作可能会导致树的平衡性被破坏,因此需要进行相应的平衡调整操作,包括左旋、右旋、左右旋和右左旋等。这些操作能够使树保持平衡,继续提供高效的性能。

  AVL树的性能分析表明,查找、插入和删除操作的平均时间复杂度均为O(log n),其中n为树中节点的数量。虽然平衡调整可能会增加一些开销,但总体上AVL树仍然能够保持高效的操作性能。

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

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

相关文章

【Eureka技术指南】「SpringCloud」从源码层面让你认识Eureka工作流程和运作机制(下)

原理回顾 Eureka Server 提供服务注册服务&#xff0c;各个节点启动后&#xff0c;会在Eureka Server中进行注册&#xff0c;这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息&#xff0c;服务节点的信息可以在界面中直观的看到。Eureka Client 是一个Java 客…

SAP Fiori 将GUI中的自开发报表添加到Fiori 工作台

1. 首先我们在workbench 中开发一个GUI report 这里我们开发的是一个简单的物料清单报表 2. 分配一个事务代码。 注意这里的SAP GUI for HTML 要打上勾 3. 创建语义对象&#xff08; Create Semantic Object&#xff09; 事物代码&#xff1a; path: SAP NetWeaver ->…

MyBatis学习——第六篇(mybatisPlus)

1&#xff1a;什么是mybatisPlus 1.1&#xff1a;mybatisPlus介绍 mybatisPlus官网&#xff1a;MyBatis-Plus mybatisPlus是一个mybatis的增强工具&#xff0c;只做增强&#xff0c;不做改变。目的是为了简化开发代码&#xff0c;提高效率而生的。 1.2&#xff1a;mybatisPl…

【Android】在Windows11系统上运行VisualStudioEmulator forAndroid

这是一个x86架构处理器的安卓模拟器&#xff0c; 在Visual Studio开发工具上用的&#xff0c;也是运行在Hyper-V虚拟机上的&#xff0c;相比其它的模拟器的性能好&#xff0c;占用磁盘空间小&#xff0c;操作简洁方便&#xff0c;非常适合开发人员调试安卓手机模拟。 安装 首…

网络:路由

1. 路由器 路由器工作在三层&#xff0c;每个接口都处于不用的网段中&#xff0c;即不同的广播域。但大多情况下&#xff0c;两台路由器直接相连的接口是同一个广播域&#xff0c;即一个网段。 2. 路由 通俗地说&#xff0c;去往目标的路径。网络中是指导IP报文转发的路径信息…

APP外包开发的iOS开发语言

学习iOS开发需要掌握Swift编程语言和相关的开发工具、框架和技术。而学习iOS开发需要时间和耐心&#xff0c;尤其是对于初学者。通过坚持不懈的努力&#xff0c;您可以逐步掌握iOS开发技能&#xff0c;构建出功能丰富、优质的移动应用。今天和大家分享学习iOS开发的一些建议方法…

k8s(七) 叩丁狼 service Ingress

负责东西流量&#xff08;同层级/内部服务网络通信&#xff09;的通信 service的定义 apiVersion: v1 kind: Service metadata:name: nginx-svclabels:app: nginx-svc spec:ports:- name: http # service 端口配置的名称protocol: TCP # 端口绑定的协议&#xff0c;支持 TCP、…

logstash 采集nginx 日志

简单安装nginx [rootelkstack03 ~]# yum install -y nginx ## 主配置文件 [rootelkstack03 ~]# cat /etc/nginx/nginx.conf user nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; include /usr/share/nginx/modules/*.conf; event…

【MySQL】ER模型(十六)

&#x1f697;MySQL学习第十六站~ &#x1f6a9;本文已收录至专栏&#xff1a;MySQL通关路 ❤️文末附全文思维导图&#xff0c;感谢各位点赞收藏支持~ ⭐学习汇总贴&#xff0c;超详细思维导图&#xff1a;【MySQL】学习汇总(完整思维导图) 一.引入 数据库设计是牵一发而动全…

极智开发 | 龙芯3a4000机器安装银河麒麟系统

欢迎关注我的公众号 [极智视界],获取我的更多经验分享 大家好,我是极智视界,本文介绍一下 龙芯3a4000机器安装银河麒麟系统。 邀您加入我的知识星球「极智视界」,星球内有超多好玩的项目实战源码和资源下载,链接:https://t.zsxq.com/0aiNxERDq 前几天淘了台龙芯3a4000 的…

机器学习、深度学习项目开发业务数据场景梳理汇总记录四

本文的主要作用是对历史项目开发过程中接触到的业务数据进行整体的汇总梳理&#xff0c;文章会随着项目的开发推进不断更新。 这里是续文&#xff0c;因为CSDN单篇文章内容太大的话就会崩溃的&#xff0c;别问我怎么知道的&#xff0c;问就是血泪教训&#xff0c;辛辛苦苦写了一…

【面试八股文】每日一题:谈谈你对异常的理解

每日一题-Java核心-谈谈你对异常的理解【面试八股文】 异常是程序在运行过程中出现的错误或不正常的情况。当程序执行过程中遇到无法处理的错误或者不符合预期的情况&#xff0c;就会抛出异常。异常可以分为两种类型&#xff1a;受检异常和非受检异常。 受检异常是指在程序编译…

主数据管理案例-北京燃气

1、 背景介绍及难点分析 主数据作为数据资源中最重要、基础的一部分&#xff0c;是北京燃气实现数据资源管理的切入点&#xff0c;对北京燃气而言&#xff0c;实现主数据的集中统一管理也是解决集团信息化建设中“信息孤岛”现象&#xff0c;实现系统集成和业务协同需求最迫切的…

《Zookeeper》源码分析(六)之 ServerCnxnFactory的工作原理(下)

目录 WorkerService数据结构构造函数 ConnectionExpirerThread WorkerService 前面的SelectorThread获取到IO就绪的连接后会将其包装成IOWorkRequest并交给worker线程处理&#xff0c;先看下这个过程&#xff1a; 从代码中可以看到&#xff0c;如果WorkerService中没有就绪的线…

p5.js画布操作实战:创建,绑定指定元素,动态调整大小,隐藏滚动条,删除画布...

theme: smartblue 文章简介 之前在 《p5.js 光速入门》 里粗略讲过一下如何使用 p5.js 创建画布。 这次要介绍几个 p5.js 提供的画布相关的方法。 创建画布时的相关配置。让画布绑定指定元素。重置画布大小。删除画布。 学习本文前你需要具备一点 p5.js 的知识&#xff0c;想了…

在家查阅下载AACR(美国癌症研究学会)数据库文献

AACR&#xff08;美国癌症研究学会&#xff09;简介&#xff1a; 美国癌症研究学会American Association for Cancer Research创建于1907年&#xff0c;是世界上成立最早、规模最大的致力于全面、创新和高水准癌症研究的科学组织。其出版物包括7种正式出版的期刊&#xff1a; …

如何让葡萄酒与晚宴菜单完美搭配?

如果你喜欢喝甜酒&#xff0c;世界上有很多经典的蜂蜜酒&#xff0c;它们通过数百年的精致生产方法达到美味的境界。糖浓缩的技术包括葡萄干燥&#xff0c;冷冻&#xff0c;甚至腐烂&#xff01;甜葡萄酒是最通用的&#xff0c;因为它们可以搭配从开胃菜到甜点的所有晚餐。 来自…

多分类评估 Micro, Macro Weighted Averages of F1 Score和适用场景

classification_report展示了weighted average F1 Score:需要区分类别,计算每个类别的f1-score,对所有类别的f1-score加权平均,权重为类别对应的样本数量占总样本数量的比例 Micro F1 Score:不需要区分类别,直接使用总体样本计算f1-score Macro F1 Score:需要区分类别…

收益抽6成,谁给你的脸?

2023年8月10日18时&#xff0c;CSDN官方发布了《CSDN付费专栏分润调整公告》&#xff0c;称将对付费专栏作者收入分润进行阶梯式调整。 根据公告内容&#xff0c;如果我没理解错的话&#xff0c;定价10元的付费专栏&#xff0c;每一个订阅&#xff0c;作者最低只能拿到40%&…