[C++进阶&数据结构]二叉搜索树

多态讲完了,我们来讲点轻松的(也许)。

我们之前讲过二叉树,而二叉树中,又有一种特殊的树称之为二叉搜索树。

一、二叉搜索树的概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

例如下图,左边的是一颗二叉搜索树,右边不是:

为什么这样的树称作二叉搜索树呢?因为这树天生就十分适合搜索。我们可以这么想使用左边的树,如果我们想找到22的话,首先22比25小,所以一定在左子树,而22大于20,所以一定在右子树。然后我们就可以找到了。我们可以发现,这种树的效率天生就很高,这棵树最多找高度次。也就是说最多找N次(注意不是logN次,因为我们可能碰到下图中右边的树),下图中的两棵树都是二叉搜素树。也就是说时间复杂度为O(N)

普通的二叉搜索树的时间复杂度为O(N),但是我们可以对其进行一些特殊变换,使之成为AVL树,红黑树之类的(之后我们再讲)。这样时间复杂度就可以降为了O(logN)了。

这个树还有另一个名字是二叉排序树,这是因为当我们对这棵树进行中序遍历的时候,结果是升序的。

二、二叉搜索树的实现

1.二叉树的结点定义

对于二叉树的结点,我们类似于list的结点定义一样,使用一个struct类。

template<class K>
struct BSTreeNode
{
	BSTreeNode(const K& val = K())
		:_key(val)
		,_left(nullptr)
		,_right(nullptr)
	{}
	K _key;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
};

2.二叉搜索树的结构

为了方便我们后续的结点定义,我们将结点给typedef一下。然后我们的成员变量其实也就这一个根节点。只要有根节点就可以构建出二叉搜索树了。

private:
	typedef BSTreeNode<K> Node;
	Node* _root;

3.二叉搜索树的构造函数

public:
	BSTree()
		:_root(nullptr)
	{}

如上代码所示,对于构造函数其实是比较简答的,我们只需要将二叉树的根结点初始化为空即可

4.二叉搜索树的插入


对于插入接口,我们需要考虑的情况有如下两种:

  1. 二叉树为空的时候,直接new一个结点插入上去即可
  2. 如果二叉树不为空,那么我们就需要进行比较

例如上面的例子,我们此时插入一个16

16小于25,向左走。

16小于20,向左走。

16大于10,向右走,并插入空

注意:如果恰好等于当前结点的值,那么我们可以认为插入失败了,因为此时已经不满足二叉搜索树的定义了。

代码实现:

我们现在想的方法是对二叉树进行迭代,迭代到空new一个新节点插入,但是如果我们只是对二叉树进行迭代最终我们会发现如此迭代下去,最终的当前结点一定会为空指针,这并非我们所期望的。我们肯定是还需要它的父节点的,这样才能让我目标值插入进去。所以我们还需要一个值来记录当前所迭代位置的父节点。即我们下面写的parent。

有了parent以后,我们现在就是new一个新的结点,然后直接parent链接起来。不过此时又分为两种情形,是左孩子还是右孩子,其实并不好确定。于是我们可以使用parent当前的值与目标值进行比对从而判断出是插入左孩子还是右孩子。

public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		else
		{
			Node* parent = _root;
			Node* cur = _root;
			while (cur != nullptr)
			{
				if (cur->_key == key)
				{
					return false;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					parent = cur;
					cur = cur->_right;
				}
			}
			cur = new Node(key);
			if (parent->_key > key)
			{
				parent->_left = cur;
			}
			else if (parent->_key < key)
			{
				parent->_right = cur;
			}
			return true;
		}
	}

5.二叉搜索树的中序遍历


插入了以后,我们可以使用中序遍历对其进行输出,由于中序遍历就是排序后的情形,所以我们可以很方便的看到我们当前二叉树的结果。

对于排序,我们最好写一个子函数来实现,因为没有子函数的话,参数不好进行控制,会使得代码十分繁琐。

如下代码所示,对于中序遍历我们其实还是比较容易看懂的。无非就是先左子树,然后根,然后右子树即可。我们最好将子函数放在私有的部分中,因为在类外我们并不需要直接使用这个函数

public:
	void InOrder()
	{
		_InOrder(_root);
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

6.二叉搜索树的查找


对于查找的逻辑,是非常简单的,它与插入也是比较相似的,当我们当前结点的值小于目标的时候,迭代到右子树,当大于目标的时候,迭代到左子树。当相等的时候返回true即可。但若当cur都为nullptr了的时候还没有找到,那就是没有找到,返回false即可

public:
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key == key)
			{
				return true;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
		}
		return false;
	}

7.二叉搜索树的删除

对于二叉搜索树的删除操作,其实是比较困难的。

首先查找元素是否在二叉搜索树中,如果不存在就返回false

如果存在,那么此时要删除的节点可能分为以下四种情况:

  1. 要删除的节点无子节点
  2. 要删除的节点只有左子节点
  3. 要删除的节点只有右子节点
  4. 要删除的节点有左、右子节点

第一种情况是最好处理的,释放节点,直接把父节点指向改为空。

第二种情况,要删除的节点只有左子节点,我们还是用上面的例子,此时18就是一个只有左子节点的节点

如果我们要删除2,只需要将2的父节点和2的左子节点链接,然后直接删除2即可。

第三种情况,要删除的节点只有右子节点,此时13就是一个只有右子节点的节点

如果我们要删除13,只需要将13的父节点和13的右子节点链接,然后直接删除13即可

总结,第二种和第三种情况的删除方式其实也适用于第一种情况,因为无子节点,也就是左右都为空,此时父节点链接的是一个空指针,并删除目标节点即可。

第四种:要删除的节点有左、右节点,此时23符合要求

我们的目标是,在23的子树中寻找一个值,将23与这个值交换。这种方法叫做替换法。

要寻找的值必须满足交换后二叉搜索树的性质不变,所以我们需要在23的左子树找出一个最大值,或者在23的右子树找出一个最小值。

前面提到过对二叉搜索树中序遍历可以得到一个有序的序列,所以23的子树的最大值,也就是在左子树中做一次中序遍历下的最后一个数,也就是21。简单来说,我们只需要从23向左走一步,然后一直向右走,就可以找到左子树的最大值了

而23的右子树的最小值,也就是在右子树中做一次中序遍历下的第一个数,也就是26。和上面类似的,我们只需要从26向右走一步,然后一直向左走,就可以找到右子树的最小值了。

因此,我们可以将23和21替换,或者将23和26替换

这里就只展示23和21替换的情况,当学会规律后,另一种情况也就学会了。

替换后,还需要把23的父节点和23的子节点链接,这里又有两种情况需要区分,否则导致错误:

  1. 替换后23的父节点位于23原先的位置
  2. 替换后23的父节点不位于23原先的位置

可以看出,在这种情况下23的父节点位于23原先的位置,那么就要将此时23的左子节点赋值给父节点的left,然后就可以直接删除23了

我们改造一下这颗二叉搜索树.使父节点不位于原先的位置(另一种情况)

假设我们要删去12(改造后12的左子树的最大值为11)

此时,就需要将12的左子节点赋值给父节点的right,然后直接删除12。

这里是一个易错点,需要注意情况的判断。以上是对二叉搜索树的操作部分。

因此我们引出了两种写法,

以下是先考虑parent与cur这两代人之间的关系所写的代码

public:
	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				if (parent == nullptr)
				{
					if (cur->_left == nullptr)
					{
						_root = cur->_right;
						delete cur;
						return true;
					}
					else if (cur->_right == nullptr)
					{
						_root = cur->_left;
						delete cur;
						return true;
					}
					else
					{
						Node* leftMaxParent = cur;
						Node* leftMax = cur->_left;
						if (leftMax->_right == nullptr)
						{
							leftMax->_right = cur->_right;
							delete cur;
							_root = leftMax;
							return true;
						}
						while (leftMax->_right)
						{
							leftMaxParent = leftMax;
							leftMax = leftMax->_right;
						}
						std::swap(leftMax->_key, cur->_key);
						leftMaxParent->_right = leftMax->_left;
						delete leftMax;
						leftMax = nullptr;
						return true;
					}
				}
				if (parent->_left == cur)
				{
					if (cur->_left == nullptr)
					{
						parent->_left = cur->_right;
						delete cur;
						return true;
					}
					else if (cur->_right == nullptr)
					{
						parent->_left = cur->_left;
						delete cur;
						return true;
					}
					else 
					{
						Node* leftMaxParent = cur;
						Node* leftMax = cur->_left;
						if (leftMax->_right == nullptr)
						{
							leftMax->_right = cur->_right;
							delete cur;
							parent->_left = leftMax;
							return true;
						}
						while (leftMax->_right)
						{
							leftMaxParent = leftMax;
							leftMax = leftMax->_right;
						}
						std::swap(leftMax->_key, cur->_key);
						leftMaxParent->_right = leftMax->_left;
						delete leftMax;
						leftMax = nullptr;
						return true;
					}
				}
				else
				{
					if (cur->_left == nullptr)
					{
						parent->_right = cur->_right;
						delete cur;
						return true;
					}
					else if (cur->_right == nullptr)
					{
						parent->_right = cur->_left;
						delete cur;
						return true;
					}
					else
					{
						Node* leftMaxParent = cur;
						Node* leftMax = cur->_left;
						if (leftMax->_right == nullptr)
						{
							leftMax->_right = cur->_right;
							delete cur;
							parent->_right = leftMax;
							return true;
						}
						while (leftMax->_right)
						{
							leftMaxParent = leftMax;
							leftMax = leftMax->_right;
						}
						std::swap(leftMax->_key, cur->_key);
						leftMaxParent->_right = leftMax->_left;
						delete leftMax;
						leftMax = nullptr;
						return true;
					}
				}
			}
		}
		return false;
	}

以下是根据先考虑cur与其孩子之间的关系所写的代码

public:
	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				if (cur->_left == nullptr)
				{
					if (parent == nullptr)
					{
						_root = cur->_right;
					}
					else if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else if (parent->_right == cur)
					{
						parent->_right = cur->_right;
					}
				}
				else if (cur->_right == nullptr)
				{
					if (parent == nullptr)
					{
						_root = cur->_left;
					}
					else if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else if (parent->_right = cur)
					{
						parent->_right = cur->_left;
					}
				}
				else
				{
					Node* leftMax = cur->_left;
					Node* leftMaxParent = cur;
					while (leftMax->_right)
					{
						leftMaxParent = leftMax;
						leftMax = leftMax->_right;
					}
					std::swap(cur->_key, leftMax->_key);
					if (leftMaxParent->_left == leftMax)
					{
						leftMaxParent->_left = leftMax->_left;
					}
					else
					{
						leftMaxParent->_right = leftMax->_left;
					}

					cur = leftMax;
				}
				delete cur;
				return true;
			}
		}
	}

这两种方法都可以完成这个接口,但是代码量有显著的差异。这里推荐先考虑cur和其后代之间的关系。因为替代法这个出现于cur与其后代之间的关系之中。

8.二叉搜索树的查找(递归)

前面,都是讲的非递归,此处为补充(能用迭代还是用迭代去找)

二叉树其实本身还是比较适合递归的。为了可以控制参数,我们要写一个子函数去解决,当根节点为空的时候,直接返回false即可,当前的结点小于要查找的结点,那么去右子树找,当大于时候,去左子树找即可。

public:
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}
private:
	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_key == key)
		{
			return true;
		}
		else if (root->_key > key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return  _FindR(root->_right, key);
		}
	}

9.二叉搜索树的插入(递归)

大体思路同上,也是补充而已

public:
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
private:
	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (root->_key < key)
		{
			return _InsertR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
	}

10.二叉搜索树的删除(递归)

这个写的挺妙的

public:	
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
private:
	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		else
		{
			Node* del = root;
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else
			{
				Node* leftMax = root->_left;
				while (leftMax->_right)
				{
					leftMax = leftMax->_right;
				}
				std::swap(leftMax->_key, root->_key);
				return _EraseR(root->_left, key);
			}
			delete del;
			return true;
		}
	}

11.二叉搜索树的销毁

拉欧i据 直接按照后序的方式进行销毁即可。

public:
	~BSTree()
	{
		_Destory(_root);
	}
private:
	void _Destory(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
		root == nullptr;
	}

这里最好传引用,因为这样可以置空root指针

12.拷贝构造函数

拷贝构造函数,我们需要实现一个深拷贝,和之前的二叉树基本一样,我们讲deque时也做过了。

这里我们使用先序的方式进行构造:

public:
	BSTree(const BSTree<K>& t)
	{
		_root = Copy(t._root);
	}
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}
		Node* Copyroot = new Node(root->_key);
		Copyroot->_left = Copy(root->_left);
		Copyroot->_right = Copy(root->_right);

		return Copyroot;
	}

13.运算符重载

这里主要用的还是赋值运算符重载吧,

现代写法启动:

public:
	BSTree<K>& operator=(BSTree<K> t)
	{
		std::swap(_root, t._root);
		return *this;
	}

​

14.二叉搜索树的实现

template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;
 
	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};
 
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
 
	bool insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
 
		Node* prev = nullptr;
		Node* cur = _root;
 
		while (cur)
		{
			prev = cur;
			if (key > cur->_key)
				cur = cur->_right;
			else if (key < cur->_key)
				cur = cur->_left;
			else
				return false;
		}
 
		cur = new Node(key);
 
		if (key > prev->_key)
			prev->_right = cur;
		else
			prev->_left = cur;
		return true;
	}
 
	bool insertR(const K& key) //递归版本
	{
		return _insertR(_root, key);
	}
 
	bool find(const K& key)
	{
		Node* cur = _root;
 
		while (cur)
		{
			if (key > cur->_key)
				cur = cur->_right;
			else if (key < cur->_key)
				cur = cur->_left;
			else
				return true;
		}
 
		return false;
	}
 
	bool findR(const K& key) //递归版本
	{
		return _findR(_root, key);
	}
 
	bool erase(const K& key) //替换法:把目标替换成左子树的最大值或右子树的最小值
	{
		Node* parent = nullptr;
		Node* cur = _root;
 
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				if (cur->_left == nullptr) //左子树为空
				{
					if (cur == _root)
						_root = cur->_right;
					else
					{
						if (cur == parent->_right)
							parent->_right = cur->_right;
						else
							parent->_left = cur->_right;
					}
				}
				else if(cur->_right == nullptr) //右子树为空
				{
					if (cur == _root)
						_root = cur->_left;
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
				}
				else //左右子树都不为空
				{
					parent = cur;
					Node* leftmid = cur->_left;
					while (leftmid->_right)
					{
						parent = leftmid;
						leftmid = leftmid->_right;
					}
					swap(cur->_key, leftmid->_key);
					if (parent == cur)
						parent->_left = leftmid->_left;
					else
						parent->_right = leftmid->_left;
					cur = leftmid;
				}
 
				delete cur;
				return true;
			}
		}
		return false;
	}
 
	bool eraseR(const K& key) //递归版本
	{
		return _eraseR(_root, key);
	}
 
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
 
	BSTree() = default;
 
	BSTree(const BSTree<K>& t)
	{
		_root = copy(t._root);
	}
 
	~BSTree()
	{
		Destroy(_root);
	}
 
private:
	Node* copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
 
		Node* newroot = new Node(root->_key);
		newroot->_left = copy(root->_left);
		newroot->_right = copy(root->_right);
 
		return newroot;
	}
 
	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
 
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}
 
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
 
	bool _findR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;
		if (root->_key == key)
			return true;
 
		if (key > root->_key)
			return _findR(root->_right, key);
		else
			return _findR(root->_left, key);
	}
 
	bool _insertR(Node*& root, const K& key) //参数中指针一定要用引用,不然链接不上
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (key > root->_key)
			return _insertR(root->_right, key);
		else if (key < root->_key)
			return _insertR(root->_left, key);
		else
			return false;
	}
 
	bool _eraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
			return false;
 
		if (key > root->_key)
			return _eraseR(root->_right, key);
		else if (key < root->_key)
			return _eraseR(root->_left, key);
		else
		{
			if (root->_left == nullptr)
			{
				Node* tmp = root;
				root = root->_right; //由于引用,root是父节点子树的别名,所以可以直接修改
				delete tmp;
			}
			else if (root->_right == nullptr)
			{
				Node* tmp = root;
				root = root->_left;
				delete tmp;
			}
			else
			{
				Node* leftmid = root->_left;
				while (leftmid->_right)
					leftmid = leftmid->_right;
				swap(root->_key, leftmid->_key);
				return _eraseR(root->_left, key);
			}
			return true;
		}
	}
private:
	Node* _root = nullptr;
};

三、二叉搜索树的应用

1)key模型

即只有key作为关键码,只需要在二叉搜索树中存储一个key即可。例如我们将业主的信息作为key,将整个小区所有业主的key存储进二叉搜索树中,如果此时要查询一个人是否是小区的业主,只需要将他的key在二叉搜索树中查找一下就可以知道这个人是否是业主了。后面要学习的set,就是一个key模型

2)key/value模型

每一个关键码key,都有与之对应的值value,也就是要在二叉搜索树中存储<key, value>的键值对。例如字典就是一个key/value模型,英文单词作为key,中文作为value,通过key就可以查找到value。又例如哈希表或者类似的用于统计某个物品出现次数的结构,通过查找该物品就可以快速得到其出现次数,这也是一个key/value模型。后面要学习的map,就是一个key/value模型我们可以将二叉搜索树改造为key/value的结构,让节点中多存储一个value,再进行一些修改即可

template<class K, class V>
struct BSTreeNode
{
	BSTreeNode<K, V>* _left;
	BSTreeNode<K, V>* _right;
	K _key;
	V _val;
 
	BSTreeNode(const K& key, const V& val)
		:_left(nullptr)
		, _right(nullptr)
		, _key(key)
		, _val(val)
	{}
};
 
template<class K, class V>
class BSTree
{
	typedef BSTreeNode<K, V> Node;
public:
	bool insert(const K& key, const V& val)
	{
		if (_root == nullptr)
		{
			_root = new Node(key, val);
			return true;
		}
 
		Node* prev = nullptr;
		Node* cur = _root;
 
		while (cur)
		{
			prev = cur;
			if (key > cur->_key)
				cur = cur->_right;
			else if (key < cur->_key)
				cur = cur->_left;
			else
				return false;
		}
 
		cur = new Node(key, val);
 
		if (key > prev->_key)
			prev->_right = cur;
		else
			prev->_left = cur;
		return true;
	}
 
	Node* find(const K& key)
	{
		Node* cur = _root;
 
		while (cur)
		{
			if (key > cur->_key)
				cur = cur->_right;
			else if (key < cur->_key)
				cur = cur->_left;
			else
				return cur;
		}
 
		return nullptr;
	}
 
	bool erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
 
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				if (cur->_left == nullptr)
				{
					if (cur == _root)
						_root = cur->_right;
					else
					{
						if (cur == parent->_right)
							parent->_right = cur->_right;
						else
							parent->_left = cur->_right;
					}
				}
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
						_root = cur->_left;
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
				}
				else
				{
					parent = cur;
					Node* leftmid = cur->_left;
					while (leftmid->_right)
					{
						parent = leftmid;
						leftmid = leftmid->_right;
					}
					swap(cur->_key, leftmid->_key);
					if (parent == cur)
						parent->_left = leftmid->_left;
					else
						parent->_right = leftmid->_left;
					cur = leftmid;
				}
 
				delete cur;
				return true;
			}
		}
		return false;
	}
 
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
 
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " : " << root->_val << endl;
		_InOrder(root->_right);
	}
 
private:
	Node* _root = nullptr;
};

本篇文章到此结束,如有错误感谢指正。

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值