C++ 实现红黑树

news2024/12/23 1:49:34

在这里插入图片描述

红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。

红黑树的性质

  1. 每个结点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,则它的两个孩子结点一定是黑色的。
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

红黑树第四条性质的理解:

在这里插入图片描述

如上图所示,该红黑树一共存在11条路径,每条路径上黑色节点的数量均为2
路径 1: 13 ----> 8 ----> 1 ----> NULL 黑色节点数量:2
路径 2: 13 ----> 8 ----> 1 ----> 6 ----> NULL (6这个节点的左孩子) 黑色节点数量:2
路径 3: 13 ----> 8 ----> 1 ----> 6 ----> NULL (6这个节点的右孩子) 黑色节点数量:2
··········

思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

最短路径的理解:红黑树的节点要么是红色要么是黑色,并且要求**从根节点到空节点路径(以下简称路径)**上的黑色节点的数量一样多,因此我们可以得出结论:当一颗红黑树路径上的黑色节点数量确定时,路径上的节点全部为黑色节点即为路径长度的最小值

最长路径的理解:因为红黑树要求路径上不能出现连续的红色节点,并且根节点必须为黑色,因此,最长路径的节点颜色变化一定是:黑 --> 红 --> 黑 --> 红 --> 黑 --> 红 --> ·······

综上所述:当一颗红黑树路径上的黑色节点数量确定时,最短路径 * 2 <= 最长路径
例如(如下图所示):一颗红黑树路径上的黑色节点数量为 2 ,最短路径:黑 --> 黑 最长路径:黑 --> 红 --> 黑 -->红。

在这里插入图片描述

红黑树节点定义

红黑树节点存储的数据类型我们依然选择 key-value 的结构,原因是 STL 库中的 map 和 set 底层的数据结构就是红黑树,到时候我们模拟实现 map 和 set 的时候,就要用我们自己实现的红黑树作为他们的底层数据结构。

我们先来思考一个问题:

红黑树应当插入什么颜色的节点?

红黑树的性质4:从根节点出发,到达每一个空节点的简单路径上黑色节点的数量是一样的。

性质4要求路径上的黑色节点数量相同,一条路径上黑色节点数量改变,那么我们必须调整其他路径的黑色节点数量,显然代价十分巨大。

因此,红黑树的应始终插入红色节点,这样就会有两种情况:

  • 新插入节点的父亲是黑色,新节点插入完成。
  • 新插入节点的父亲是红色,只需调整当前路径上节点的颜色使其满足红黑树的性质即可。(如下图所示)

在这里插入图片描述

有了上面的知识,我们就可以定义出红黑树的节点,以及怎么书写节点的构造函数了。

#pragma once

enum Color
{
    RED,
    BLACK
};

template<class K, class V>
struct RBTreeNode
{
    pair<K, V> _kv;

    RBTreeNode<K, V>* _parnet; //父节点指针
    RBTreeNode<K, V>* _left; //左孩子
    RBTreeNode<K, V>* _right; //右孩子

    Color _col; //节点的颜色
	
    //构造函数
    RBTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        ,_parent(nullptr)
        ,_left(nullptr)
        ,_right(nullptr)
        _col(RED) //新插入的节点默认是红色的
    {}
};

template<class K, class V>
class AVLTree
{
    typedef RBTreeNode<K, V> Node;
public:
    AVLTree()
        :_root(nullptr) //初始时候根节点为为 nulptr
    {}

private:
    Node* _root;
};

红黑树的插入

插入一个新的节点,最重要的是插入之后经过变化,使得新插入一个节点之后,整棵树依然满足红黑树的性质。

1:父节点为黑色

这种情况在一开始就探讨过了,新插入的节点父亲为黑色,直接插入就完事儿了!新插入节点的位置在哪里?还是根据二叉搜索树的那里的插入一样。

在这里插入图片描述

2:父节点为红色,叔叔存在且为红

这个叔叔节点是什么呢?叔叔节点就是父节点的亲兄弟,你懂我那个意思吧!

在这里插入图片描述

这种情况应该怎么做呢?

将父节点,叔叔节点的颜色改为黑色,将祖父节点的颜色改为红色。

插入新的节点之前,该树是一颗红黑树,满足红黑树的性质,其中性质三:如果一个节点是红色的,则它的两个孩子结点一定是黑色的

这句话说明一条路径上不可能存在连续的两个红色节点。因此,如果父节点的颜色为红色,那么父节点的父节点的颜色一定为黑色。将父节点颜色改为红色,父节点颜色改为黑色,叔叔节点改为黑色,所有路径黑色节点的数量依旧相同。

在这里插入图片描述

  • 我们发现经过这样一次变化之后一条路径上出现了连续的红节点(17 和 25),任然不满足红黑树的性质

  • 我们将红黑树的插入情况进行了分类,每种插入情况都会对应一种调整方式(或者直接完成插入,情况1),调整后并不一定能完成插入,而是进入了红黑树插入分类的另一种情况(或者与上一次的情况一样,这个例子中就是与上一次的情况相同)。因此,我们需要更新父节点,叔叔节点,祖父以及新插入的节点(cur),判断更新后处于插入的哪种情况,直到完成插入。

  • 更新方式:令新插入的节点(cur)指向祖父节点,然后找到新的父节点,叔叔节点,祖父节点即可。

我们发现,我们的这个例子就是与上一次变换的方式相同,直接继续进行相同的变换即可。

在这里插入图片描述

我们可以看到在向上更新,变换颜色的过程中可能将根节点的颜色搞成红色。根据红黑树的性质二:**根节点是黑色的。**如果出现这种情况就需要将根节点的颜色改成黑色。这样修改之后每条路径上的黑色节点数量都会加一,该树依然满足红黑树的性质。

3:父节点为红色,叔叔存在且为黑

我们先来证明新插入一个节点之后的第一次调整颜色是不可能出现这种情况的!一定是另一种情况经过调整,更新 cur,parent,uncle,grandparent 之后出现的这种情况!

证明:如果新插入的节点的叔叔节点是黑色,因为父亲节点和叔叔节点到根节点的路径是相同的,如果叔叔节点是黑色的,那么,父亲节点必然也是黑色的(根据红黑树的性质思:不同路径上的黑色节点数量相同),因为是新插入一个节点嘛,父节点之下是不可能有黑色节点的,只能是父节点本身是黑色的。如果父节点是黑色的,在父节点下插入新节点时,我们的插入就直接成功了,不需要调整。

综上所述,在插入新节点的第一次调整节点颜色的时候是不可能出现这种情况的!只能是某种情况进行调整节点颜色之后,出现的这种情况!

我们来看下面的例子:

我们向下面的一颗红黑树中插入一个新节点 28,发现调整颜色的策略和 2 相同,我们就进行相应的颜色调整。

在这里插入图片描述

第一次调整之后的结果就是这个样子啦:是不是就满足我们的第三种情况啦!

在这里插入图片描述

那么这种情况应该怎么调整节点的颜色呢!!仔细观察你会发现无论怎么调整节点的颜色都比较麻烦,于是我们就要使用旋转啦!那进行什么类型的旋转呢?这就得根据 cur,parent,grandparent 的相对位置来看啦:

在这里插入图片描述

旋转想必你在 AVL 树的学习过程中已经了然于胸了!

我们上面的那个例子就是一个左单旋哈:以 grandparent 为旋转点进行左单旋。

在这里插入图片描述

第三种情况旋转之后都需要将 grandparent 改为红色,parent 改为黑色哈!

4:父节点为红色,叔叔不存在

在下面的例子中,我们向这颗红黑树中插入了 28 这个节点,满足情况四:父节点为红色,叔叔不存在,和情况三一样,同样需要根据 cur,parent,grandparent 的相对位置确定旋转方式。如下图所示:显然是一个左单旋哈!

在这里插入图片描述

左单旋之后的结果:

在这里插入图片描述

很明显仅仅是左单旋之后还不满足红黑树的性质,我们需要调整颜色,调整颜色的策略和情况三是一样的:将 grandparent 的颜色改为红色,将 parent 的颜色改为黑色即可。

聪明的你肯定已经发现了情况三和情况四其实是可以合并的啦!合并之后代码看起来就简单多了!

检查一棵树是不是红黑树

原理就是根据红黑树的性质来嘛!我们可以先求出一条路径中黑色节点的数量。然后根据这个基准值来对比其他路径黑色节点的数量!在求其他路径黑色节点的数量时,记得检查是否出现连续的红色节点哦!

代码

#pragma once

enum Color
{
	RED,
	BLACK
};

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

	pair<K, V> _kv;
	Color _col; //节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED) //节点默认的颜色是红色
	{}
};

template<class K, class V>
struct RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
        //插入的时候如果根节点为 nullptr, 申请一个节点,将该节点的颜色改为黑色之后作为根节点就行啦
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			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->_col = RED;
        //确定新的节点插入到那个位置,左孩子还是右孩子
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
        //向上链接父节点
		cur->_parent = parent;

        //调整节点的颜色,使之满足红黑树的性质,只有 parent 的颜色为红才会调整节点的颜色,parent 为黑就直接完成插入了嘛
		while (parent && parent->_col == RED)
		{
            //祖父节点
			Node* grandfather = parent->_parent;
            //这里根据父节点在祖父节点的位置进行分类,因为我们要确定 uncle 的位置嘛,这样分类代码比较简洁
            //parent 位于 grandparent 的左侧
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // uncle 不存在 或 uncle 存在且为黑
				{
					if (cur == parent->_left)
					{
						//     g
						//   p
						// c
                        //右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//     g
						//   p
						//		c
                        //左右双旋
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
			else // parent == grandfather->_right
			{
                //找到叔叔节点
				Node* uncle = grandfather->_left;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						// g
						//	  p
						//       c
                        //左单旋
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						// g
						//	  p
						// c
                        //右左双旭那
						RotateR(parent);
						RotateL(grandfather);
                        //调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
		}

		_root->_col = BLACK; // 无论根节点的颜色是否在调整的过程中变成了红色,最后我们都将根节点的颜色变为黑色,方便写代码

		return true;
	}

    //左单旋。逻辑和 AVL 树完全一样就不写注释
	void RotateL(Node* parent)
	{

		Node* cur = parent->_right;
		Node* curleft = cur->_left;

		parent->_right = curleft;
		if (curleft)
		{
			curleft->_parent = parent;
		}

		cur->_left = parent;

		Node* ppnode = parent->_parent;

		parent->_parent = cur;


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

			}

			cur->_parent = ppnode;
		}
	}

    //右单旋。逻辑和 AVL 树完全一样就不写注释
	void RotateR(Node* parent)
	{

		Node* cur = parent->_left;
		Node* curright = cur->_right;

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

		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;

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

			cur->_parent = ppnode;
		}
	}
	
    //下面就是检查一颗树是不是红黑树的代码
    //benchmark 就是那个基准值。blacknum 就是用来统计路径中的黑色节点数量的,请体会传值的妙处
	bool CheckColour(Node* root, int blacknum, int benchmark)
	{
		if (root == nullptr)
		{
			if (blacknum != benchmark)
				return false;

			return true;
		}

		if (root->_col == BLACK)
		{
			++blacknum;
		}
        //检查是否出现连续的红色节点
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "出现连续红色节点" << endl;
			return false;
		}
        //递归左右子树
		return CheckColour(root->_left, blacknum, benchmark)
			&& CheckColour(root->_right, blacknum, benchmark);
	}

	bool IsBalance()
	{
		return IsBalance(_root);
	}

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

		if (root->_col != BLACK)
		{
			return false;
		}

		// 找到一条路径中的黑色节点数量
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				++benchmark;

			cur = cur->_left;
		}

		return CheckColour(root, 0, benchmark);
	}

private:
	Node* _root = nullptr;
};

红黑树的删除

红黑树的删除比插入还要复杂一些,但是难度并没有上升多少!具体过程包含了普通的二叉搜索树的删除,懂的都懂!同样面试的过程中顶多考红黑树的插入呢!有兴趣可以了解了解!

红黑树与AVL树的比较

红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数。所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

红黑树的删除

红黑树的删除比插入还要复杂一些,但是难度并没有上升多少!具体过程包含了普通的二叉搜索树的删除,懂的都懂!同样面试的过程中顶多考红黑树的插入呢!有兴趣可以了解了解!

红黑树与AVL树的比较

红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数。所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
在这里插入图片描述

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

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

相关文章

SMART PLC开放式以太网通信(UDP通信)

西门子S7-200 SMART PLC不仅支持开放式以太网通信,还支持MODBU-RTU,以及ModbusTcp通信,详细内容请参考下面文章: MODBUS-RTU主站通信 【精选】PLC MODBUS通信优化、提高通信效率避免权限冲突(程序+算法描述)-CSDN博客文章浏览阅读2.5k次,点赞5次,收藏10次。MODBUS通讯…

Ubuntu 20.04源码安装git 2.35.1

《如何在 Ubuntu 20.04 上从源代码安装 Git [快速入门]》和《如何在 Ubuntu 20.04 上安装 Git》是我参考的博客。 https://git-scm.com/是git官网。 lsb_release -r看到操作系统版本是20.04。 uname -r看到内核版本是5.4.0-156-generic。 sudo apt update更新一下源。 完…

操作系统复习(3)处理机调度与死锁

一、概述 1.1了解调度的层次 调度是指&#xff0c;在一个队列中&#xff0c;按照某种方法&#xff08;算法&#xff09;&#xff0c;选择一个合适的个体的过程。进程调度的功能就是按一定策略、动态地把CPU分配给处于就绪队列中的某一进程&#xff0c;并使之执行。 作业调度&…

设置DevC++支持c++11标准

1.点击编译选项 2. 设置语言标准 3.点击确认 4.测试代码 使用auto成功 测试&#xff01;

【LeetCode】第 370 场周赛

100115. 找到冠军 I 一场比赛中共有 n 支队伍&#xff0c;按从 0 到 n - 1 编号。 给你一个下标从 0 开始、大小为 n * n 的二维布尔矩阵 grid 。对于满足 0 < i, j < n - 1 且 i ! j 的所有 i, j &#xff1a;如果 grid[i][j] 1&#xff0c;那么 i 队比 j 队 强 &…

该酷的酷该飒的飒~质感满满的羊羔绒皮外套

皮毛一体材质柔软舒适 保暖性能极佳 冬天穿也不惧严寒的哦 宽松版型&#xff0c;对身材的包容性很强 外套的细节处理也非常出色&#xff0c;车缝线整齐 纽扣和拉链质量也是很不错的哟

鼠标加速不一定都是好事,有时是适得其反,如在游戏当中

如果你玩游戏&#xff0c;你会想在Mac上禁用鼠标加速。这是因为虽然鼠标加速可能很好&#xff0c;但它可能会导致鼠标光标速度不一致&#xff0c;这在游戏中并不理想。 简单地说&#xff0c;如果macOS检测到你正在以特定的速度或强度移动鼠标&#xff0c;鼠标指针加速会提高光…

PivotNet:Vectorized Pivot Learning for End-to-end HD Map Construction

参考代码&#xff1a;BeMapNet。PS&#xff1a;代码暂未放出&#xff0c;关注该仓库动态 动机和主要贡献 在MapTR系列的算法中将单个车道线建模为固定数量的有序点集&#xff08;对应下图Evenly-based&#xff09;&#xff0c;这样的方式对于普通道路场景具备一定适应性。但是…

深度好文|听懂这些逻辑,你会越变越好。

哈喽&#xff0c;大家好&#xff0c;我是雷工。 最近看了一篇文章&#xff0c;噱头很大&#xff0c;说是手把手的教你&#xff0c;如何从一个普通人&#xff0c;成长为身价千万的中产。 这种话我肯定是不信的&#xff0c;要么标题党&#xff0c;要么割韭菜&#xff0c;千万的中…

048基于web+springboot的校园资料分享平台

欢迎大家关注&#xff0c;一起好好学习&#xff0c;天天向上 文章目录 一项目简介技术介绍 二、功能组成三、效果图四、 文章目录 一项目简介 本校园资料分享平台有管理员和用户两个角色。管理员功能有个人中心&#xff0c;学生管理&#xff0c;资料分享管理&#xff0c;资源分…

【JVM】JDBC案例打破双亲委派机制

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 JVM 打破双亲委派机制&#xff08;JDBC案例…

使用springBoot+Redis实现分布式缓存

使用springBootRedis实现分布式缓存 1. 添加redis框架 依赖 2. 配置redis连接信息 #redis 连接的配置信息 spring.redis.database15 spring.redis.port6379 spring.redis.host82.157.236.116 # 可省略 spring.redis.lettuce.pool.min-idle5 spring.redis.lettuce.pool.max-id…

QT进度条 QProgressDialog基础、高级和样式表使用详解

一、基础使用 QProgressDialog是Qt中的一个类&#xff0c;用于显示一个进度条和一个取消按钮&#xff0c;让用户可以在长时间的操作中看到进度&#xff0c;并且可以随时取消。QProgressDialog的基本用法是创建一个对象&#xff0c;设置最小值和最大值&#xff0c;然后在循环中…

多态 多继承的虚表深度剖析 (3)

&#x1f4af; 博客内容&#xff1a;多态 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准C后端工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎私信&#xff01; &#x1f496; 欢迎大家&#xff1a;这里是CSD…

加法运算、 || 、 赋值运算

一、加法运算 在这里插入图片描述 二、&& || 三、赋值运算 四、js类型就八种&#xff1a; 五、css权重、 六&#xff1a;布局&#xff0c;尽量使用块盒。 七、小数精度存储的问题&#xff1a;存的不精确&#xff0c;算的肯定也是有问题的。 八、找单身狗算法题…

STM32G030F6P6 芯片实验 (二)

STM32G030F6P6 芯片实验 (二) Hello World - GPIO LED 尝试了下, 从 0 开始建 MDK HAL M0plus Project, 成功点亮 LED了。 但是 ST-LINK跑着跑着, 码飞了! 不知飞哪去了。 只好拿 MX 建了个 MDK Base。 呼叫 SysTick HAL_Delay(), 切换 LED。 基本上都是一样的用法, 只是换…

逆向学习记录(4)adb

adb用于PC和手机端通讯。 常用命令如下&#xff1a; 如果不是模拟器&#xff08;模拟器一般都有自己的adb&#xff09;&#xff0c;adb会出现在Andirod的SDK中&#xff0c;路径为&#xff1a;Android/SDK/platform-tools。 最好加入环境变量中。

免费亲人微信聊天记录提取软件新版本v1.1,使用说明,注意事项 2023.11.06

V 1.1 优化了备份速度&#xff0c;新增了备份消息类型的选型&#xff0c;可以选择仅仅备份文本&#xff0c;或者文本与音频&#xff0c;或者文本音频视频图片。 有什么办法可以导出与某个人的微信聊天记录&#xff1f; 只想导出与某个微信好友的聊天记录&#xff0c;有办法做到…

懒汉模式和饿汉模式

目录 单例模式 饿汉模式 懒汉模式 单例模式 所谓单例模式,就是在有些场景中,有些特定的类,只能创建一个实例(对象),当程序员不小心创建多个实例,就会出现编译报错. ★ 这种模式涉及到一个单一的类&#xff0c;该类负责创建自己的对象&#xff0c;同时确保只有单个对象被创…

同步网盘与云盘:哪个更好用?

同步网盘、同步云盘现在是热门的文件管理工具&#xff0c;在回答“同步网盘云盘哪个好用”这个问题之前&#xff0c;我们需要知道什么样的同步网盘、同步云盘算好用&#xff1f; 什么样的同步网盘云盘好用&#xff1f; 1、存储空间大 对于文件管理工具而言&#xff0c;存储空…