超详细红黑树的模拟实现

news2024/9/29 15:32:52

在这里插入图片描述

前言

有人说设计出AVL树的的人是个大牛,那写红黑树(RBTree)的人就是天才!
上一篇文章,我们已经学习了AVL树,牛牛个人认为AVL树已经够优秀了,那让我们一起探究一下,为什么红黑树比AVL树的结构还要优秀吧!

目录

  • 前言
  • 一、红黑树的介绍
  • 二、手撕红黑树
    • 2.1 框架结构分析
      • 2.11 结点颜色
      • 2.12 结点类
      • 2.13 红黑树结构
    • 2.2 接口实现
      • 2.21 插入接口(重点)
        • 情况1: 父亲是爷爷的左,cur结点是父亲的左。 (左左)
        • 情况2: 父亲是爷爷的左,cur结点是父亲的右。 (左右)
        • 情况3: 父亲是爷爷的右,cur结点是父亲的左。 (右左)
        • 情况4: 父亲是爷爷的右,cur结点是父亲的左。 (右右)
      • 2.22 最左侧结点(LeftMost)
      • 2.23 最右侧结点(RightMost)
      • 2.24 检测函数(次重点)
      • 2.25 获取根节点
      • 2.25 获取红黑树的高度
      • 2.26 find函数
  • 三、结语:

一、红黑树的介绍

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

红黑树,是一种自平衡的二叉查找树,它的性质比较复杂,但却非常重要,常用于C++中的STL库中的setmap等容器。红黑树的节点有两种颜色:红色(red)和黑色(black)。它具有如下五个性质:

  1. 每个节点是红色或者黑色的。
  2. 根节点是黑色的。
  3. 每个叶子节点(这里特指最下面的空节点)是黑色的。
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。(即:每条路径上不能出现连续的红结点)
  5. 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。

由于红色结点的父亲必须是黑色结点,并且每条路径上的黑色结点的个数也必须相同,所以得到了红黑树最长路径中节点个数不会超过最短路径节点个数的两倍

这也就决定了,红黑树的高度是log(n)级别的。
例如,下面这个就是红黑树

在这里插入图片描述

二、手撕红黑树

2.1 框架结构分析

2.11 结点颜色

红黑树较于AVL树,不在使用平衡因子,而是增设了颜色变量,这里我们可以枚举出这两种颜色,方便使用。

	enum Colour	//枚举出颜色
	{
		RED,		//红色
		BLACK		//黑色
	};

2.12 结点类

同AVL树一样,红黑树也是三叉链

	//结点类
	template<class K, class V>
	struct RBTreeNode
	{
		//指针域
		RBTreeNode<K, V>* _left;
		RBTreeNode<K, V>* _right;
		RBTreeNode<K, V>* _parent;


		pair<K, V> _kv;	//数据域
		Colour _Col;


		RBTreeNode(const pair<K, V>& kv)//构造函数
			:_left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			, _kv(kv)
			, _Col(RED)				//注意这里,默认新构造的结点是红色的
		{}
	};

2.13 红黑树结构

	//红黑树的结构
	template<class K, class V>
	class RBTree
	{
		typedef RBTreeNode<K, V> Node;
	public:
	
		// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false
		//此版本红黑树对于重复元素,插入失败
		bool Insert(const pair<K, V>& kv);
		
		// 搜索红黑树中是否存在值为data的节点。
		//存在返回该节点的地址,不存在则返回nullptr
		Node* Find(const pair<K, V>& data);
		
		// 获取红黑树最左侧节点
		Node* LeftMost();
		// 获取红黑树最右侧节点
		Node* RightMost();
		
		//(这里的玩法大家应该不陌生了)	
		int Height();//计算红黑树的高度
		bool IsValidRBTRee();// 检测红黑树是否为有效的红黑树
	private:
		bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack);
		int Height(Node* root);
		// 左单旋
		void RotateL(Node* pParent);
		// 右单旋
		void RotateR(Node* pParent);
		// 为了操作树简单起见:获取根节点
		Node*& GetRoot();
	private:
		Node* _root = nullptr;
	};

2.2 接口实现

2.21 插入接口(重点)

本篇主要讲解的部分就是红黑树的插入操作。

函数名 :insert
返回值插入成功,返回true;插入失败,返回false
形参键值对
//插入函数
template<class K, class V>
bool RBTree<K, V>::Insert(const pair<K, V>& kv) {
}

(1).如果是第一次插入,则插入的是根节点,则需要特殊处理,因为要给根节点root赋值。

在结点类中我们提到,在创建的新节点我们给与了默认颜色RED(红色),而红黑树的根节点必须是BLACK(黑色)的,这里一定要记得修改一下颜色。

//第一次插入
	if (_root == nullptr) {
		_root = new Node(kv);
		_root->_Col = BLACK;		//注意根节点一定是黑色的,默认构造的新节点是红色的,所以这里要改一下。
		return true;
	}

(2) 寻找插入位置
红黑树也是二叉搜索树,学到这里,相信友友们在AVL树和二叉搜索树学习阶段,已经知道如何寻找插入位置。

	//寻找插入位置
	while (cur) {
		parent = cur;
		if (_root->_kv.first > kv.first) {
			cur = cur->_left;		//插入的值当前结点的值小,往左走
		}
		else if (_root->_kv.first < kv.first) {
			cur = cur->_right;		//插入的值当前结点的值大,往右走
		}
		else {
			return false;	//本篇实现的红黑树,对于重复值,插入失败
		}
	}
	//判断插入在左边还是右边
	cur = new Node(kv);
	if (kv.first < parent->_kv.first) {				//插入在左边
		parent->_left = cur;
	}
	else {									//插入在右边
		parent->_right = cur;
	}
	cur->_parent = parent;					//保证三叉链的关系

3.看uncle(叔叔)

叔叔(uncle)?这里我将当前结点的父亲(parent)的兄弟称为叔叔结点。

示例:
在这里插入图片描述
当我们新增一个结点时,默认新节点的颜色为RED,如果它的父亲结点是黑色的,则不需要做任何调整,直接插入成功!

在这里插入图片描述
当父亲结点是红色的时候,则与新增结点一起,会构成连续的红色结点,此时需要调整。

调整规则主要看uncle叔叔结点。

情况1: 父亲是爷爷的左,cur结点是父亲的左。 (左左)

👻情况:叔叔存在且为红
🔑调整方案: 变色+向上更新在这里插入图片描述(图片为博主原创,请勿随意转发使用)

👻情况:叔叔不存在,或者存在且为黑
🔑调整方案: 右旋+变色

在这里插入图片描述

在这里插入图片描述(图片为博主原创,请勿随意转发使用)

情况2: 父亲是爷爷的左,cur结点是父亲的右。 (左右)

👻情况:叔叔存在且为红
🔑调整方案: 变色+向上更新
在这里插入图片描述(图片为博主原创,请勿随意转发使用)

👻情况:叔叔不存在,或者存在且为黑
🔑调整方案: 左右双旋+变色

示例图:
在这里插入图片描述

未写(图片为博主原创,请勿随意转发使用)

情况3: 父亲是爷爷的右,cur结点是父亲的左。 (右左)

👻情况:叔叔存在且为红
🔑调整方案: 变色+向上更新
这里不画图了,牛牛画累了。

👻情况:叔叔不存在,或者存在且为黑
🔑调整方案: 右左双旋+变色
在这里插入图片描述

在这里插入图片描述(图片为博主原创,请勿随意转发使用)

情况4: 父亲是爷爷的右,cur结点是父亲的左。 (右右)

👻情况:叔叔存在且为红
🔑调整方案: 变色+向上更新
在这里插入图片描述(图片为博主原创,请勿随意转发使用)

👻情况:叔叔不存在,或者存在且为黑
🔑调整方案: 左旋+变色
在这里插入图片描述(图片为博主原创,请勿随意转发使用)

总结:
红黑树的插入主要看uncle
分为两种情况:
(1)uncle存在且为红
调整方案: 变色+继续向上调整
(2)uncle不存在或者uncle存在且为黑
调整方案: 旋转+变色

至于如何旋转,因为红黑树没有采用平衡因子的方式,所以我们采用判断grandfather与parent 和 parentcur的关系结构来决定。
下图是具体调整总结:
在这里插入图片描述

总代码:

bool Insert(const T& kv) {
	if (_root == nullptr) {
		_root = new Node(kv);
		_root->_Col = BLACK;		//注意根节点一定是黑色的,默认构造的新节点是红色的,所以这里要改一下。
		return true;
	}

	Node* cur = _root, * parent = nullptr;
	//寻找插入位置
	while (cur) {
		parent = cur;
		if (_root->_kv.first > kv.first) {
			cur = cur->_left;
		}
		else if (_root->_kv.first < kv.first) {
			cur = cur->_right;
		}
		else {
			return false;
		}
	}
	//判断插入在左边还是右边
	cur = new Node(kv);

	if (kv.first < parent->_kv.first) {				//插入在左边
		parent->_left = cur;
	}
	else {									//插入在右边
		parent->_right = cur;
	}
	cur->_parent = parent;					//保证三叉链的关系
	
	//
	while (parent && parent->_Col == RED) {
		//爷爷结点
		Node* grandfather = parent->_parent;

		if (parent == grandfather->_left) {			//如果父亲是爷爷的左孩子
			Node* uncle = grandfather->_right;		//那么叔叔就是爷爷的右孩子

			//叔叔存在且为红
			if (uncle && uncle->_Col == RED) {
				//变色
				uncle->_Col=parent->_Col = BLACK;
				grandfather->_Col = RED;

				//继续向上更新
				cur = grandfather;
				parent = grandfather->_parent;
			}
			else {									//叔叔不存在或者 存在且为黑
				if (cur == parent->_left) {			
					//			g
					//		p
					//c
					RotateR(grandfather);
					grandfather->_Col = RED;
					parent->_Col = BLACK;
				}
				else {
					//		g
					//	p
					//		c
					RotateL(parent);
					RotateR(grandfather);
					cur->_Col = BLACK;
					grandfather->_Col = RED;
				}
				break;	//此时最顶端的结点已经变成黑色了,不需要继续向上更新了。
			}
		}
		else {		// 如果父亲是爷爷的右孩子
			Node* uncle = grandfather->_left;		//那么叔叔就是爷爷的左孩子

			//叔叔存在且为红
			if (uncle && uncle->_Col == RED) {
				//变色
				uncle->_Col = parent->_Col = BLACK;
				grandfather->_Col = RED;

				//继续向上更新
				cur = grandfather;
				parent = grandfather->_parent;
			}
			else {									//叔叔不存在或者 存在且为黑
				if (cur == parent->_left) {
					//	g
					//		p
					//	c
					//注意旋转时的传参
					RotateR(parent);
					RotateL(grandfather);
					grandfather->_Col = RED;
					cur->_Col = BLACK;
				}
				else {
					//	g
					//		p
					//			c
					RotateL(grandfather);		//注意旋转时的传参
					
					parent->_Col = BLACK;
					grandfather->_Col = RED;
				}
				break;	//此时最顶端的结点已经变成黑色了,不需要继续向上更新了。
			}
		}
	}
	_root->_Col = BLACK;		//最后根节点一定是黑的
	return true;

}

2.22 最左侧结点(LeftMost)

对于二叉搜索树,如果我们按中序遍历,则可以得到一个有序序列。
中序遍历的首个结点: 最左侧结点
中序遍历的最后结点: 最右侧结点

在这里插入图片描述

	// 获取红黑树最左侧节点
	template<class K, class V>
	typename RBTree<K, V>::Node* RBTree<K, V>::LeftMost() {
		Node* left_most = _root;
		while (left_most->left) {
			left_most = left_most->left;
		}
		return left_most;
	}

2.23 最右侧结点(RightMost)

	// 获取红黑树最右侧节点
	template<class K, class V>
	typename RBTree<K, V>::Node* RBTree<K, V>::RightMost() {
		Node* right_most = _root;
		while (right_most->right) {
			right_most = right_most->left;
		}
		return right_most;
	}

2.24 检测函数(次重点)

在实现红黑树时,也许我们会遇到各种问题,好不容易跑通代码后,我们缺无法判断自己实现的红黑树是否正确,是否符合红黑树的规则。

此时,我们可以设计一个检测函数,检测实现的红黑树是否平衡。

  1. 空树也是红黑树
  2. 根节点必须是红黑树
  3. 我们可以设置一个“基准值”,基准值为红黑树一条路径中的黑色结点的个数。
  4. 遍历每条红黑树的路径,判断红黑树结点的个数,是否与基准值相等。
  5. 除此之外,出现连续两个红色结点则返回false
// 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测
template<class K, class V>
bool  RBTree<K, V>::IsValidRBTRee() {
	if (_root == nullptr)	//空树也是红黑树
		return true;

	if (_root->_Col != BLACK)	//根节点必须是红黑树
	{
		return false;
	}

	// 基准值
	int pathBlack = 0;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_Col == BLACK)
			++pathBlack;
		cur = cur->_left;
	}
	_IsValidRBTRee(_root, 0, pathBlack);
}


template<class K, class V>
bool RBTree<K, V>::_IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack) {


	if (pRoot == nullptr)
	{
		if (blackCount != pathBlack)		//一条路径走到底,也就是走到叶子结点以后,判断这条路径上的黑色结点个数(blackCount)是否与 设定的黑色结点个数相同(pathBlack)
			return false;
		return true;
	}


	if (pRoot->_Col == BLACK)
	{
		++blackCount;
	}


	if (pRoot->_Col == RED && pRoot->_parent && pRoot->_parent->_Col == RED)
	{
		cout << _root->_kv.first << "出现连续红色节点" << endl;
		return false;
	}

	//递归访问左右子树
	return _IsValidRBTRee(pRoot->_left, blackCount, pathBlack)
		&& _IsValidRBTRee(pRoot->_right, blackCount, pathBlack);
}

2.25 获取根节点

	template<class K, class V>
	typename RBTree<K, V>::Node*& RBTree<K, V>::GetRoot() {
		return _root;
	}

2.25 获取红黑树的高度

	template<class K, class V>
	int RBTree<K, V>::Height()
	{
		return Height(_root);
	}
	template<class K, class V>
	int RBTree<K, V>::Height(typename RBTree<K, V>::Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = Height(root->_left);	//计算左子树的高度
		int rightHeight = Height(root->_right);	//计算右子树的高度
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

2.26 find函数

template<class K, class T,>
typename RBTree<class K, class T, class Of_T>::Node* RBTree<class K, class T, class Of_T>::Find(const T& data) {
	Node* cur = _root;
	while (cur)
	{
		if (data > cur->_data)
		{
			cur = cur->_right;
		}
		else if (data < cur->_data)
		{
			cur = cur->_left;
		}
		else return cur;
	}
	return nullptr;  // 找不到目标元素时返回nullptr
}

三、结语:

看完本篇文章,我们不难知道,对于插入操作,无论是红黑树还是avl树,要维持对应的“平衡”,会进行沿路径的更新,其中涉及大量的旋转操作,而红黑树较于avl树那种严格的高度差在-11之间,红黑树的平衡条件相对宽松,这也就大大减少了的为了维持平衡的大量旋转操作,而且还能保证效率在log(N),这也就是为啥说红黑树较于avl树更加优秀。
你赞同这个观点吗?
在这里插入图片描述

后续牛牛会模拟实现mapset,会在那篇文章封装红黑树,对红黑树进行改造,增加迭代器等功能。帮助友友们更加深入理解mapset容器。

在这里插入图片描述

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

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

相关文章

【C语言】InfiniBand 驱动mlx4_ib_init和mlx4_ib_cleanup

一、中文讲解 这两个函数是Linux内核模块中对于Mellanox InfiniBand 驱动程序初始化和清理的函数。 mlx4_ib_init()函数是模块初始化函数&#xff0c;使用__init宏标注&#xff0c;表示该函数只在模块加载时运行一次。 函数执行的步骤如下&#xff1a; 1. 通过alloc_ordered_w…

sklearn.preprocessing.RobustScaler(解释和原理,分位数,四分位差)

提示&#xff1a;sklearn.preprocessing.RobustScaler&#xff08;解释和原理&#xff0c;分位数&#xff0c;四分位差&#xff09; 文章目录 [TOC](文章目录) 一、RobustScaler 是什么&#xff1f;二、代码1.代码2.输出结果 总结 提示&#xff1a;以下是本篇文章正文内容&…

计算机网络|Socket

文章目录 Socket并发socket Socket Socket是一种工作在TCP/IP协议栈上的API。 端口用于区分不同应用&#xff0c;IP地址用于区分不同主机。 以下是某一个服务器的socket代码。 其中with是python中的一个语法糖&#xff0c;代表当代码块离开with时&#xff0c;自动对s进行销毁…

[VulnHub靶机渗透] CONNECT THE DOTS

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

miniconda3彻底删除虚拟环境

退出虚拟环境&#xff1a;确保您不在要删除的虚拟环境中。如果在&#xff0c;使用命令 conda deactivate 来退出当前激活的虚拟环境。查看虚拟环境列表&#xff1a;运行命令 conda env list 或 conda info -e 来查看所有存在的虚拟环境及其路径。删除虚拟环境&#xff1a;使用命…

HTTP Cookie 你了解多少?

Cookie是什么&#xff1f; 先给大家举个例子&#xff0c;F12 打开浏览器的页面之后&#xff0c;我们能在 Response Headers 的字段里面看到一个header 叫做 Set-Cookie&#xff0c;如下所示 图中包含的 Set-Cookie 为 Set-Cookie:uuid_tt_dd10_20293537580-1709432565344-232…

Maven(黑马学习笔记)

初识Maven 什么是Maven Maven是Apache旗下的一个开源项目&#xff0c;是一款用于管理和构建java项目的工具。 官网&#xff1a;https://maven.apache.org/ Apache 软件基金会&#xff0c;成立于1999年7月&#xff0c;是目前世界上最大的最受欢迎的开源软件基金会&#xff0…

Sqli-labs靶场第15关详解[Sqli-labs-less-15]自动化注入-SQLmap工具注入

Sqli-labs-Less-15 #自动化注入-SQLmap工具注入 SQLmap用户手册&#xff1a;文档介绍 - sqlmap 用户手册 由于这题是post请求&#xff0c;所以先使用burp进行抓包&#xff0c;然后将数据包存入txt文件中打包 用-r 选择目标txt文件 python sqlmap.py -r data.txt -current-db…

JavaScript之数据类型

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 数据类型   Java…

2023天津公租房网上登记流程图,注册到信息填写

2023年天津市公共租赁住房网上登记流程图 小编为大家整理了天津市公共租赁住房网上登记流程&#xff0c;从登记到填写信息。 想要体验的朋友请看一下。 申请天津公共租赁住房时拒绝申报家庭情况会怎样&#xff1f; 天津市住房保障家庭在享受住房保障期间&#xff0c;如在应申…

力扣 第 125 场双周赛 解题报告 | 珂学家 | 树形DP + 组合数学

前言 整体评价 T4感觉有简单的方法&#xff0c;无奈树形DP一条路上走到黑了&#xff0c;这场还是有难度的。 T1. 超过阈值的最少操作数 I 思路: 模拟 class Solution {public int minOperations(int[] nums, int k) {return (int)Arrays.stream(nums).filter(x -> x <…

Windows上构建一个和Linux类似的Terminal

preview 目的是在Windows上构建一个和Linux类似的Terminal&#xff0c;让Windows炼丹和Linux一样舒适&#xff0c;同是让Terminal取代Xshell完成远程链接。 预览如下图 在Linux下我们使用zsh和oh-my-zsh结合&#xff0c;Windows下我们使用powershell7和oh-my-posh结合。 前提…

力扣● 1049. 最后一块石头的重量 II ● 494. 目标和 ● 474.一和零

● 1049. 最后一块石头的重量 II 题目要把石头分成两堆&#xff0c;这两堆的重量差值最小。相撞之后剩下的石头重量就最小。其实就是要尽量把石头分为差不多重量的两堆&#xff0c;和昨天的● 416. 分割等和子集相似&#xff0c;这样就转换成了01背包问题。 和416题一样&…

【字符串相加】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 字符串相加 方法一&#xff1a; 方法二&#xff1a; 总结 前言 世上有两种耀眼的光芒&#xff0c;一种是正在升起的太阳&#xff0c;一种是正在努力学习编程的…

腾讯云幻兽帕鲁游戏存档迁移教程,本地单人房迁移/四人世界怎么迁移存档?

腾讯云幻兽帕鲁游戏存档迁移的方法主要包括以下几个步骤&#xff1a; 登录轻量云控制台&#xff1a;首先&#xff0c;需要登录到轻量云控制台&#xff0c;这是进行存档迁移的前提条件。在轻量云控制台中&#xff0c;可以找到接收存档的服务器卡片&#xff0c;并点击进入实例详情…

LeetCode 2368.受限条件下可到达节点的数目:搜索 + 哈希表

【LetMeFly】2368.受限条件下可到达节点的数目&#xff1a;搜索 哈希表 力扣题目链接&#xff1a;https://leetcode.cn/problems/reachable-nodes-with-restrictions/ 现有一棵由 n 个节点组成的无向树&#xff0c;节点编号从 0 到 n - 1 &#xff0c;共有 n - 1 条边。 给…

ecmascript 6+(2)

引用数据类型&#xff1a; Object, Array, RegExp, Date等 包装类型&#xff1a;&#xff08;底层数据类型会将简单数据类型包装为对象&#xff09; String, Number, Boolean等&#xff08;都是基本数据类型的构造函数&#xff09; Object Object.keys(对象) 返回数组&…

ctf_show笔记篇(web入门---php特性)

目录 php特性 89&#xff1a;直接数组绕过preg_match当遇到数组时会直接报错输出0 90&#xff1a;这里利用了intval的特性 91&#xff1a;这里需要细节一点 92-93&#xff1a;这两题的方法很多可以发散思维 94&#xff1a;还是利用小数绕过例如4476.0 95&#xff1a;这里…

spring boot 修复 Spring Framework URL解析不当漏洞(CVE-2024-22243)

漏洞描述 当应用程序使用UriComponentsBuilder来解析外部提供的URL&#xff08;如通过查询参数&#xff09;并对解析的URL的主机执行验证检查时可能容易受到Open重定向攻击和SSRF攻击&#xff0c;导致网络钓鱼和内部网络探测等。 受影响产品或系统 6.1.0 < Spring Framew…

【bioinformation 2】生物数据库

&#x1f31e;欢迎来到AI医学的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f4c6;首发时间&#xff1a;&#x1f339;2024年3月3日&…