【C++】红黑树

红黑树的介绍

概念

红黑树是一种自平衡的二叉查找树 ,它通过为每个节点附加颜色属性(红色或黑色),并强制满足一组约束条件,使得树在插入、删除操作后能自动恢复平衡,从而保证最坏情况下的查找、插入、删除时间复杂度均为 O(log n)

接下来我会从各个方面为你介绍红黑树相关信息。

规则

红黑树每个节点要么是红色 ,要么是黑色,且必须同时满足以下规则:

规则 说明
① 节点颜色 每个节点非红即黑。
② 根节点 根节点必须是黑色。
③ 叶子节点(NIL) 所有叶子节点(空节点,通常用 NIL 表示)都被视为黑色。
④ 红色节点的子节点 红色节点的两个子节点必须都是黑色(不允许连续两个红色节点相连)。
⑤ 任意节点到叶子的黑色节点的数目 从任一节点出发,到达其所有后代叶子节点的路径上,经过的黑色节点数目必须相同。

正是这五条性质(尤其是④和⑤),保证了最长路径不会超过最短路径的两倍,从而防止树退化成链表。

那么它是如何确保最长路径不超过最短路径的两倍呢?

我们这里举个例子:假设有一颗红黑树的每条路径有x个黑色节点,在极端情况下,它的最短路径为(这条路径全为黑色节点):x,它的最长路径为(路径的节点全为一红一黑组合):2x。因此在这些规则的规定下,红黑树的最长路径不会超过最短路径的两倍

红黑树的实现

在介绍完规则之后,我们来简单的实现一下红黑树的结构:

红黑树的结构

红黑树的结构与AVL树的结构相近,只是多了一个枚举类型来记录节点的颜色

复制代码
#pragma once
#include<iostream>

using namespace std;

enum Color
{
	BLACK,
	RED
};

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

	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	Color _col;

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

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
private:
	Node* _root = nullptr;
};

红黑树的插入

红黑树插入的大概过程

红黑树的插入大致分为两个流程:先按二叉搜索树的规则插入,而后对插入的节点进行调整。而我们的重点放在调整部分。

首先我们要明确:对于空树插入,新插入的节点一定是黑色节点(红黑树规则2:根节点一定是黑色)。如果是非空树插入,则新增节点一定是红色节点,如果是黑色节点就会使得这条路径上的黑色节点与其他路径不同,规则5被破坏。

在非空树的节点插入之后,我们还要观察它是否违背了其他规则:父节点是否为黑色。

若父节点为黑色,则未违反任何规则,插入结束。若父节点为红色,违反了规则4:红色节点的两个子节点必须都是黑色(不允许连续两个红色节点相连)。则我们要对其进行分析和讨论。

为了方便称呼,在这里统一一下称呼:新增结点标识为c(cur),c的父亲标识为p(parent),p的父亲标识为 g(grandfather),p的兄弟标识为u(uncle)。

情况1:变色

第一种情况为c、p都为红色,g为黑色,而u存在且为红。则我们需要做的就是把p和u变成黑色,而g的颜色变为红色。

pu 都变黑、g 变红的做法,相当于在左右子树各增加一个黑色节点后,再通过将爷爷变红把 g 所在子树的黑色节点总数拉回原状,从而在消除 cp 连续红色的同时维持黑高不变;处理完成后若 g 的父亲为黑色则立即终止,若父亲也是红色则需将 g 视为新的问题节点继续向上调整,而一旦 g 最终成为根节点就必须重新染回黑色。

情况2:单旋 + 变色

第二种情况为c、p都为红色,g为黑色,而u不存在或者u存在且为黑。

关于u与c的关系:u不存在,则c⼀定是新增结点,u存在且为黑,则 c⼀定不是新增,c之前是黑色的,是在c的子树中插⼊,符合情况1,变色将c从黑色变成红色,更新上来的。

当新插入的红色节点位于父节点的外侧且叔父为黑色时,冲突的根源在于红色的父子相连破坏了规则,而调整的方法是对爷爷节点执行一次朝向父亲方向的单旋转,让父亲取代爷爷的位置成为局部新根,接着将父亲重新染为黑色、爷爷染为红色;这样一来,原本违规的红色父子被拆散,父亲变黑后挡住了向下的红色蔓延,而爷爷变红又刚好抵消了因旋转带来的黑色节点数量变化,整棵子树的黑高维持原样,调整一步到位,无需继续向上追溯。

情况3:双旋 + 变色

第三种情况与第二章情况相似,它们的前提相同:c、p都为红色,g为黑色,而u不存在或者u存在且为黑。

当新插入的红色节点位于父节点的内侧 (即父为左子时新节点是右子,或父为右子时新节点是左子)且叔父为黑色时,直接单旋无法一步到位,因为此时新节点处在"拐弯"位置;处理方法是先对父节点 执行一次反向单旋,把新节点转到外侧,此时结构退化为情况二的"外侧直线"形态,再对爷爷节点 执行一次朝向新节点方向的单旋,最后将新节点染黑、爷爷染红即可。这整个过程包含两次旋转和两次变色,因此称为双旋加变色,目的是在维持黑高不变的前提下,将原本嵌在中间的红节点提为子树的黑色新根,从而彻底消除连续红色冲突。

完整代码
复制代码
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	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;

	while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;
		if (parent == grandfather->_right)
		{
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED)
			{
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (cur == parent->_right)
				{
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
			}
		}
		else
		{
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED)
			{
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (cur == parent->_left)
				{
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
			}
		}
	}
	_root->_col = BLACK;
	return true;
}
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

	Node* pParent = parent->_parent;
	subL->_right = parent;
	parent->_parent = subL;

	if (_root == parent)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subL;
		}
		else
		{
			pParent->_right = subL;
		}
		subL->_parent = pParent;
	}
}
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	Node* pParent = parent->_parent;
	subR->_left = parent;
	parent->_parent = subR;

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

红黑树的查找

按二叉搜索树逻辑实现即可,搜索效率为 O(logN)

复制代码
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < key)
		{
			cur = cur->_right;
		}
		else if (cur->_kv.first > key)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}
	return nullptr;

红黑树的验证

红黑树的验证,本质就是对照五条核心性质逐项检查,确认这棵树既是合法的二叉搜索树,又严格符合颜色与黑高约束。具体验证逻辑如下:

  1. 二叉搜索树性质

    先通过中序遍历确认节点值严格有序,确保树本身是合法的 BST。

  2. 根节点为黑

    直接检查根节点颜色,若为红则不合法。

  3. 无连续红色节点

    遍历每个红色节点,检查其父节点和子节点是否均为黑色(NIL 视为黑)。

  4. 黑高一致性

    从根节点出发,递归计算每条到 NIL 叶子路径上的黑色节点数,保证所有路径结果相等。

实际操作中,写一个递归函数,返回以当前节点为根的子树是否合法及其黑高,自底向上校验,能高效完成验证。

复制代码
bool IsBalance()
{
	if (_root == nullptr)
	{
		return true;
	}
	if (_root->_col == RED)
		return false;

	Node* cur = _root;
	int refNum = 0;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			refNum++;
		}
		cur = cur->_left;
	}

	return Check(_root, 0, refNum);
}
bool Check(Node* root, int blackNum, int refNum)
{
	if (root == nullptr)
	{
		if (blackNum != refNum)
		{
			cout << "有路径的黑色节点数错误" << endl;
			return false;
		}
		return true;
	}

	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << "存在两个相连的红色节点" << endl;
	}
	if (root->_col == BLACK)
	{
		blackNum++;
	}
	return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}

与AVL树的比较

对比维度 红黑树 AVL 树
平衡条件 宽松(最长路径 ≤ 2倍最短路径) 严格(左右子树高度差 ≤ 1)
高度 略高(但仍是 O(log n)) 更低,理论上查找更快
插入/删除效率 旋转次数少,更优 旋转次数可能更多,但查找略快
适用场景 写多读少 (如 Java TreeMap、C++ STL map 读多写少(如词典查找、数据库索引)

红黑树模拟实现STL中的set和map

在实现完红黑树的基本结构之后,我们要尝试用红黑树来封装set和map

改造红黑树

在标准库的实现中,map 和 set 底层通常共用同一套红黑树代码。为了适配这种复用,红黑树节点中存储的数据类型不再写死为 Kpair<const K, V>,而是定义为一个泛型参数 T

这样一来就产生了一个问题:当红黑树内部需要比较两个节点的大小(比如执行插入、查找时),它面对的只是泛型 T,并不知道应该比较什么字段。对于 setT 就是 K 本身,直接比较 K 即可;但对于 mapTpair<K, V>,而比较规则要求只看键 K,忽略值 Vpair 默认的比较运算符会先比较 K,相等时还会比较 V,这既不符合语义,也会引入不必要的开销。

解决这个矛盾的办法是将"如何从 T 中取出键"这一行为抽象成一个仿函数,由上层调用方提供。具体做法如下:

  • RBTree 模板参数中增加一个 KeyOfT 类型,它必须是一个可调用对象,负责从 T 类型的实例中提取出用于比较的键 K

  • map 的实现层,定义一个仿函数 MapKeyOfT,其 operator() 接收 pair<const K, V>,返回 pair.first

  • set 的实现层,定义一个仿函数 SetKeyOfT,其 operator() 接收 K,直接返回 K 自身。

  • 实例化红黑树时,mapMapKeyOfT 传入,setSetKeyOfT 传入。

之后,红黑树中任何需要比较两个节点的地方,都先调用 KeyOfT 取出各自的键,再对键进行大小比较。这样就将数据存储结构比较规则解耦,用一套红黑树代码干净地支撑了两种不同的容器。

红黑树的迭代器

因为map和set支持迭代器,同样的,我们需要在原本的结构上再加上迭代器。

红黑树的迭代器在整体设计框架上与链表的迭代器思路一致,都是封装一个指向树节点的指针,并通过重载 *->++-- 等运算符,使其具备类似原生指针的访问行为。这里的核心难点 在于**operator++** 和 **operator--**的实现,因为它们需要严格遵循二叉搜索树的中序遍历顺序------左子树、根节点、右子树。

具体来说,当迭代器指向某个节点 it 时,++it 的逻辑分为两种情况:

++ 时若当前节点有右子树,则下一节点为右子树的最左节点;若无右子树,则沿父指针向上回溯,直到找到某个作为父节点左孩子的祖先,该祖先即为下一节点,若一直未找到则迭代器置为 nullptr 表示 end()

operator-- 的整体逻辑与 ++ 完全对称,只是遍历顺序变为右子树、根节点、左子树。

还有一点需要注意为满足 map 可改值、set 不可改键的要求,底层红黑树模板参数对 setconst K,对 mappair<const K, V>

改造后的红黑树代码

复制代码
#pragma once
#include<iostream>

using namespace std;

enum Color
{
	BLACK,
	RED
};

template<class T>
struct RBTreeNode
{
	T _data;

	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;

	Color _col;

	RBTreeNode(const T& data)
		:_data(data)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{ }
};

template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T, Ref, Ptr> Self;

	Node* _node;
	Node* _root;

	RBTreeIterator(Node* node, Node* root)
		:_node(node)
		,_root(root)
	{ }

	Self& operator++()
	{
		if (_node->_right)
		{
			_node = _node->_right;
			while (_node->_left)
			{
				_node = _node->_left;
			}
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = cur->_parent;
			}
			_node = parent;
		}
		return *this;
	}
	Self& operator--()
	{
		if (_node == nullptr)
		{
			Node* cur = _root;
			while (cur && cur->_right)
			{
				cur = cur->_right;
			}
			_node = cur;
		}
		else if (_node->_left)
		{
			Node* cur = _node->_left;
			while (cur->_right)
			{
				cur = cur->_right;
			}
			_node = cur;
		}
		else
		{
			Node* parent = _node->_parent;
			Node* cur = _node;

			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = cur->_parent;
			}

			_node = parent;
		}
		return *this;
	}
	Ref operator*()
	{
		return _node->_data;
	}
	Ptr operator->()
	{
		return &_node->_data;
	}
	bool operator!=(const Self& s) const
	{
		return _node != s._node;
	}
	bool operator==(const Self& s) const
	{
		return _node == s._node;
	}
};

template<class K, class T, class KeyOfT>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef RBTreeIterator<T, T&, T*> Iterator;
	typedef RBTreeIterator<T, const T&, const T*> ConstIterator;

	Iterator Begin()
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		return Iterator( cur, _root );
	}
	Iterator End()
	{
		return Iterator(nullptr, _root);
	}
	ConstIterator Begin() const
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		return Iterator(cur, _root);
	}
	ConstIterator End() const
	{
		return Iterator(nullptr, _root);
	}
	pair<Iterator, bool> Insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return { Iterator(_root, _root), true };
		}
		KeyOfT kot;
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (kot(cur->_data) < kot(data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kot(cur->_data) > kot(data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return { Iterator(cur, _root), false };
			}
		}
		cur = new Node(data);
		cur->_col = RED;
		if (kot(parent->_data) < kot(data))
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		Node* newnode = cur;

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_right)
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
				}
			}
			else
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
				}
			}
		}
		_root->_col = BLACK;
		return { Iterator(newnode, _root), true };
	}
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

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

		Node* pParent = parent->_parent;
		subL->_right = parent;
		parent->_parent = subL;

		if (_root == parent)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (pParent->_left == parent)
			{
				pParent->_left = subL;
			}
			else
			{
				pParent->_right = subL;
			}
			subL->_parent = pParent;
		}
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		Node* pParent = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;

		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (pParent->_left == parent)
			{
				pParent->_left = subR;
			}
			else
			{
				pParent->_right = subR;
			}
			subR->_parent = pParent;
		}
	}
	Node* Find(const K& key)
	{
		KeyOfT kot;
		Node* cur = _root;
		while (cur)
		{
			if (kot(cur->_data) < key)
			{
				cur = cur->_right;
			}
			else if (kot(cur->_data) > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == RED)
			return false;

		Node* cur = _root;
		int refNum = 0;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				refNum++;
			}
			cur = cur->_left;
		}

		return Check(_root, 0, refNum);
	}
	void InOrder()
	{
		return _InOrder(_root);
	}
	int Height()
	{
		return _Height(_root);
	}
	int Size()
	{
		return _Size(_root);
	}
private:
	bool Check(Node* root, int blackNum, int refNum)
	{
		if (root == nullptr)
		{
			if (blackNum != refNum)
			{
				cout << "有路径的黑色节点数错误" << endl;
				return false;
			}
			return true;
		}

		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "存在两个相连的红色节点" << endl;
		}
		if (root->_col == BLACK)
		{
			blackNum++;
		}
		return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data.first << ":" << root->_data.second << endl;
		_InOrder(root->_right);
	}
	int _Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
	int _Size(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		return _Size(root->_left) + _Size(root->_right) + 1;
	}
	Node* _root = nullptr;
};

set的模拟实现

复制代码
#pragma once
#include"RBTree4.13.h"

namespace Rane
{
	template<class K>
	class set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
		typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;

		iterator begin()
		{
			return _t.Begin();
		}
		iterator end()
		{
			return _t.End();
		}
		const_iterator begin() const
		{
			return _t.Begin();
		}
		const_iterator end() const
		{
			return _t.End();
		}
		pair<iterator, bool> insert(const K& key)
		{
			return _t.Insert(key);
		}
	private:
		RBTree<K, const K, SetKeyOfT> _t;
	};
}

map的模拟实现

复制代码
#pragma once
#include"RBTree4.13.h"

namespace Rane
{
	template<class K, class V>
	class map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
		typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;

		iterator begin()
		{
			return _t.Begin();
		}
		iterator end()
		{
			return _t.End();
		}
		const_iterator begin() const
		{
			return _t.Begin();
		}
		const_iterator end() const
		{
			return _t.End();
		}
		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _t.Insert(kv);
		}
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert({ key, V() });
			return ret.first->second;
		}
	private:
		RBTree<K, pair<const K, V>, MapKeyOfT> _t;
	};
}
相关推荐
leaves falling2 小时前
深入浅出 C++ STL list:从入门到精通
开发语言·c++
饕餮争锋2 小时前
Bash 简介
开发语言·bash
吃不胖爹2 小时前
Sharding-JDBC只分表不分库
java
yaaakaaang2 小时前
十三、责任链模式
java·责任链模式
爱吃烤鸡翅的酸菜鱼2 小时前
【Java】封装位运算通用工具类——用一个整数字段替代几十个布尔列,极致节省存储空间
java·开发语言·设计模式·工具类·位运算·合成复用原则
xinhuanjieyi2 小时前
php给30支NBA球队添加logo图标,做好对应关系
android·开发语言·php
菜菜小狗的学习笔记2 小时前
八股(三)Java并发
java·开发语言
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【10】ReactAgent 工具加载和执行流程
java·人工智能·spring
lee_curry2 小时前
JUC第一章 java中基础概念和CompletableFuture
java·多线程·并发·juc