C++之二叉树进阶

片头

嗨!小伙伴们,大家好~ 今天我们来学习二叉树进阶相关知识,准备好了吗?咱们开始咯~


一、前言

在前面的章节中,我们学习过二叉树:数据结构之二叉树

后面学习map和set的特性时,需要先铺垫二叉搜索树,二叉搜索树也是一种树形结构。

对二叉搜索树的特性了解,有助于更好的理解map和set的特性。

有些OJ题使用C语言实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

这一章中,我们学习二叉搜索树,对二叉树部分进行收尾总结。


二、二叉搜索树

2.1 二叉搜索树概念

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

(1)若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

(2)若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

(3)它的左右子树也分别为二叉搜索树

搜索二叉树也称为排序二叉树。如果对搜索二叉树进行中序遍历,那么自然就拍成了升序。

	int a[] = { 8,3,1,10,6,4,7,14,13 };

中序遍历结果如下:1   3   4   6   7   8   10   13   14

看,是不是恰好有序啦?

这就是利用了二叉搜索树的特性:左子树的所有节点值比跟节点小;右子树的所有节点值比根节点大


2.2 实现二叉搜索树

我们先创建 SearchBinaryTree.h 头文件,用来声明和定义二叉搜索树;接着创建 test.cpp 源文件,用来测试二叉搜索树是否合法。

(1)定义节点
template<class T>
struct BSTreeNode
{
	//构造函数
	BSTreeNode(const T& val)
		:_data(val)
		,_left(nullptr)
		,_right(nullptr)
	{ }

	T _data;				//节点的值
	BSTreeNode<T>* _left;	//左孩子
	BSTreeNode<T>* _right;	//右孩子
};

(2)定义SearchBinaryTree类

在SearchBinaryTree.h 头文件中,定义SearchBinaryTree类,重命名 BSTreeNode<T> 为 Node。定义私有成员变量_root。

template<class T>
class BSTree
{
	typedef BSTreeNode<T> Node;
public:
    //成员函数
private:
    //成员变量
	Node* _root = nullptr;	//根节点,初始化为空
};

(3)查找节点

a、从根开始查找,将节点的值和查找的数进行比较;比根大则往右边查找,比根小则往左边走查找

b、最多查找高度次,如果走到空,还没有找到,则这个节点不存在。

//查找这个值是否存在
    bool Find(const T& Key)
	{
		Node* cur = _root;	//定义变量cur,
							//用来循环遍历搜索二叉树的每一个节点
		while (cur)
		{
			if (cur->_data > Key)		//若Key比当前节点的值小,走左子树部分
			{
				cur = cur->_left;
			}
			else if (cur->_data < Key)	//若Key比当前节点的值大,走右子树部分
			{
				cur = cur->_right;
			}
			else
			{
				return true;			//找到了,返回true
			}
		}

		return false;		//没有找到,返回false
	}

(4)插入节点

a、树为空,则直接新增节点,赋值给root指针

b、树不为空,按二叉树的性质查找插入位置,插入新节点

	//插入新节点
	bool Insert(const T& Key)
	{
		if (_root == nullptr)		//若当前根节点为空
		{
			_root = new Node(Key);	//将新节点直接赋给_root
			return true;
		}


		Node* Parent = nullptr;				//假设父节点初始值为空
		Node* cur = _root;					//从根节点开始遍历
		while (cur)
		{
			if (cur->_data > Key)			//若Key值比当前节点的值小
			{
				Parent = cur;
				cur = cur->_left;			//走左子树部分
			}
			else if (cur->_data < Key)		//若Key值比当前节点的值大
			{
				Parent = cur;
				cur = cur->_right;			//走右子树部分
			}
			else
			{
				return false;				//找到了一个和Key值相等的,返回false
			}
		}

		//找到合适的位置,插入新节点
		cur = new Node(Key);
		if (Parent->_data < Key)		//若当前父节点的值比Key值小
		{
			Parent->_right = cur;		//将新节点放到父节点的右子树上
		}
		else                            //当前父节点的值比Key值大
		{
			Parent->_left = cur;		//将新节点放到父节点的左子树上
		}
		return true;		//插入完毕后,返回true
	}

(5)中序遍历

提到中序遍历,代码很简单,按照左节点 - 根 - 右节点的方式来递归实现

	//中序遍历
	void InOrder(Node* root)
	{
		if (root == nullptr)	//若当前节点为空,返回
		{
			return;
		}

		InOrder(root->_left);		//左节点
		cout << root->_data << " "; //值
		InOrder(root->_right);		//右节点
	}

我们可以在test.cpp里面试一下:

那怎么办呢?别着急,咱们可以把中序遍历的核心代码设定为子函数_InOrder并且私有化,用成员函数InOrder调用子函数即可。

template<class T>
class BSTree
{
	typedef BSTreeNode<T> Node;
public:
	void InOrder()
	{
		_InOrder(_root);	//虽然成员变量私有化了,
							//但是类中的成员函数依然可以使用
		cout << endl;
	}

private:
	void _InOrder(Node* root)	//将中序遍历设定为子函数即可
	{
		if (root == nullptr)    //如果当前节点为空,直接返回
		{
			return;
		}

		_InOrder(root->_left);            //左节点
		cout << root->_data << " ";       //根
		_InOrder(root->_right);           //右节点
	}

	Node* _root = nullptr;	//根节点,初始化为空
};

我们再试试:

#include"SearchBinaryTree.h"
int main()
{
	int a[] = { 8,3,1,10,6,4,7,14,13 };
	BSTree<int> t;
	for (auto& e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	return 0;
}


(6)二叉树的删除

首先,咱们先列一个大致框架,和前面Find函数类似。

如果Key比当前节点的值大,那么往右查找;若Key比当前节点的值小,那么往左查找。

	//删除节点
	bool erase(const T& Key)
	{
		Node* parent = nullptr;
		Node* cur = _root;			//初始时,从根节点开始遍历
		while (cur)
		{
			if (cur->_data < Key)
			{
				parent = cur;		//将子节点赋给父节点
				cur = cur->_right;
			}
			else if (cur->_data > Key)
			{
				parent = cur;		//将子节点赋给父节点
				cur = cur->_left;
			}
			else
			{
				//删除节点
			}
		}
		return false;	//没有找到,返回false
	}

讲解删除部分稍微有点复杂,我们具体可以分为以下3种情况:

其中,第1种情况和第2种情况可以合并,因为没有孩子(叶子节点)的情况包括了左孩子为空 / 右孩子为空。

所以,当删除节点的时候,一定要看清楚被删除节点在父节点的左边还是右边

				//删除节点
				if (cur->_left == nullptr)		//0~1个孩子
				{
					if (parent->_left == cur)	//若被删除的节点在父节点的左边
					{
						parent->_left = cur->_right;
					}
					else						//若被删除的节点在父节点的右边
					{						
						parent->_right = cur->_right;
					}
					delete cur;
					return true;
				}
				else if (cur->_right == nullptr)		//0~1个孩子
				{
					if (parent->_left == cur)			//如果被删除的节点在父节点的左边
					{
						parent->_left = cur->_left;
					}
					else                                 //若被删除的节点在父节点的右边
					{
						parent->_right = cur->_left;
					}
					delete cur;
					return true;
				}
				else
				{
					//被删除的节点有2个孩子的情况
				}
    

现在,被删除的节点没有孩子、有1个孩子的情况已经解决了。那么,如果被删除的节点有2个孩子,怎么办呢?

					//被删除的节点有2个孩子的情况
					//右子树的最小值 
					// --> 右子树的最左边的节点 --> 这个节点没有左孩子,可能有右孩子

					Node* rightMinP = nullptr;
					Node* rightMin = cur->_right;	//找右子树

					while (rightMin->_left)			//寻找右子树中最左边的节点
					{
						rightMinP = rightMin;
						rightMin = rightMin->_left;
					}

					cur->_data = rightMin->_data;	//将数据替换
					if (rightMinP->_left == rightMin)		//如果被删除的节点为父节点的左孩子
					{
						rightMinP->_left = rightMin->_right;		//将父节点的左指针指向被删除节点的右孩子
					}
					else									//如果被删除的节点为父节点的右孩子
					{
						rightMinP->_right = rightMin->_right;		//将父节点的右指针指向被删除节点的右孩子
					}
					delete rightMin;	//替换完成,将这个节点删除

大致基本上完成了,但还有2个小问题:

Q1:关于rightMinP初始化的问题

					Node* rightMinP = cur;			//rightMinP初始化为被删除的节点cur

现在,我们将所有节点依次删除,运行一下程序:

#include"SearchBinaryTree.h"
int main()
{
	int a[] = { 8,3,1,10,6,4,7,14,13 };
	BSTree<int> t;
	for (auto& e : a)
	{
		t.Insert(e);
	}
	t.InOrder();

	for (auto& e : a)
	{
		t.erase(e);
		t.InOrder();
	}

	return 0;
}

其实,还有1种情况没有考虑到:

完整代码如下:(有一点点长,不过,其实也不算太难)

	//删除节点
	bool erase(const T& Key)
	{
		Node* parent = nullptr;
		Node* cur = _root;			//初始时,从根节点开始遍历
		while (cur)
		{
			if (cur->_data < Key)
			{
				parent = cur;		//将子节点赋给父节点
				cur = cur->_right;
			}
			else if (cur->_data > Key)
			{
				parent = cur;		//将子节点赋给父节点
				cur = cur->_left;
			}
			else
			{
				//删除节点
				if (cur->_left == nullptr)		//0~1个孩子
				{
					if (parent == nullptr)		//若被删除的节点cur为根节点
					{
						_root = cur->_right;	//_root更新为cur的右孩子
					}
					else {
						if (parent->_left == cur)	//若被删除的节点在父节点的左边
							parent->_left = cur->_right;
						else						//若被删除的节点在父节点的右边
							parent->_right = cur->_right;
					}
					delete cur;
					return true;
				}
				else if (cur->_right == nullptr)		//0~1个孩子
				{
					if (parent == nullptr)				//若被删除的节点cur为根节点
					{
						_root = cur->_left;				//_root更新为cur的左孩子
					}
					else {
						if (parent->_left == cur)		//若被删除的节点在父节点的左边
							parent->_left = cur->_left;
						else                            //若被删除的节点在父节点的右边
							parent->_right = cur->_left;
					}
					delete cur;
					return true;
				}
				else
				{
					//被删除的节点有2个孩子的情况
					//右子树的最小值 
					// --> 右子树的最左边的节点 --> 这个节点没有左孩子,可能有右孩子

					Node* rightMinP = cur;			//rightMinP初始化为被删除的节点cur
					Node* rightMin = cur->_right;	//找右子树

					while (rightMin->_left)			//寻找右子树中最左边的节点
					{
						rightMinP = rightMin;
						rightMin = rightMin->_left;
					}

					cur->_data = rightMin->_data;	//将数据替换
					if (rightMinP->_left == rightMin)		//如果被删除的节点为父节点的左孩子
					{
						rightMinP->_left = rightMin->_right;		//将父节点的左指针指向被删除节点的右孩子
					}
					else									//如果被删除的节点为父节点的右孩子
					{
						rightMinP->_right = rightMin->_right;		//将父节点的右指针指向被删除节点的右孩子
					}
					delete rightMin;	//替换完成,将这个节点删除
					return true;		
				}
			}
		}
		return false;	//没有找到,返回false
	}

运行一下:


(7)查找节点(递归版)

二叉搜索树的递归查找很简单,因为外面不能传根,这里像InOrder一样封装起来:

	//查找(递归版)
	bool FindR(const T& Key)
	{
		return _FindR(_root, Key);
	}
	bool _FindR(Node* root, const T& Key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_data < Key)
		{
			_FindR(root->_right, Key);
		}
		else if (root->_data > Key)
		{
			_FindR(root->_left, Key);
		}
		else
		{
			return true;
		}
	}

(8)插入节点(递归版)
	//插入(递归)
	bool InsertR(const T& Key)
	{
		return _InsertR(_root, Key);
	}
	bool _InsertR(Node* root, const T& Key)
	{
		if (root == nullptr)
		{
			root = new Node(Key);
			return true;
		}

		if(root->_data < Key)
		{
			return _InsertR(root->_right, Key);
		}
		else if (root->_data > Key)
		{
			return _InsertR(root->_left, Key);
		}
		else
		{
			return false;
		}
	}

因为递归到最后1步的时候,root只是一个局部变量,根本插入不了数据。

可以一步一步的把父亲传下来,但这里有一个关键点:加引用

	//插入(递归)
	bool InsertR(const T& Key)
	{
		return _InsertR(_root, Key);
	}
	bool _InsertR(Node*& root, const T& Key)
	{
		if (root == nullptr)
		{
			root = new Node(Key);
			return true;
		}

		if(root->_data < Key)
		{
			return _InsertR(root->_right, Key);
		}
		else if (root->_data > Key)
		{
			return _InsertR(root->_left, Key);
		}
		else
		{
			return false;
		}
	}

这里的引用最后1步才起作用,它是空,但它也是上一层传下来的别名。

赋给root,就把父亲链接起来了,可以用二级指针,但传引用就很方便。


(9)二叉树的删除(递归版)

这里的递归删除和上面的递归插入一样,也用到了非常巧妙的引用:

	//二叉树节点的删除(递归版)
	bool EraseR(const T& key) {
		return _EraseR(_root, key);
	}
	bool _EraseR(Node*& root, const T& Key)
	{
		if (root == nullptr)
		{
			return false;
		}

		if (root->_data < Key) 
		{
			return _EraseR(root->_right, Key);
		}
		else if (root->_data > 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* MinNode = root->_right;
				while (MinNode->_left)
				{
					MinNode = MinNode->_left;
				}
				//root->_data = MinNode->_data; //错误
				swap(root->_data, MinNode->_data);
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;
		}
	}

现在,我们测试一下:

test.cpp

#include"SearchBinaryTree.h"

void test1()
{
	BSTree<int> t;
	int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.EraseR(8);
	t.EraseR(3);
	t.InOrder();

	for (const auto& e : arr)
	{
		t.EraseR(e);
	}
	t.InOrder();
}


void test2()
{
	BSTree<int> t;
	int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	t.EraseR(8);
	t.EraseR(3);
	t.EraseR(1);
	t.InOrder();

	cout << t.FindR(10) << endl;
	cout << t.FindR(100) << endl;
}

int main()
{
	test1();
	cout << "=======================" << endl;
	test2();

	return 0;
}


(10)析构、拷贝构造和赋值

test.cpp默认生成的:

void test3()
{
	BSTree<int> t;
	int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();
}

int main()
{
	test3();
	return 0;
}

这里也是浅拷贝问题,指向的是同一颗树,编译器没有崩溃,只是没写析构,写一下析构函数:

	//析构函数
    ~BSTree()
	{
		_Destroy(_root);
	}

private:

	void _Destroy(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
		root = nullptr;
	}

再测试一下:

这时,我们就应该自己写拷贝构造了:

	//拷贝构造
    BSTree(const BSTree<K,V>& t)
	{
		_root = _Copy(t._root);
	}

private:

	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_data);		//中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

此时,运行就会报错:

错误(活动)   E0291  类 "BSTree<int>" 不存在默认拷贝构造函数

我们再写一个默认的拷贝构造:

	//拷贝构造
    BSTree(const BSTree<K,V>& t)
	{
		_root = _Copy(t._root);
	}

	//BSTree()	//这样写也可以,但下面是C++11的用法
	//{ }

	BSTree() = default;		//C++11关键字,强制编译器生成默认的构造

private:

	Node* _Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}

		Node* CopyRoot = new Node(root->_data);		//中序递归链接左右子树
		CopyRoot->_left = _Copy(root->_left);
		CopyRoot->_right = _Copy(root->_right);
		return CopyRoot;
	}

运行程序:

写了拷贝构造,我们就可以直接用现代写法写一个赋值:

	//赋值重载
	BSTree<k> operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}

测试一下:

void test4()
{
	BSTree<int> t;
	int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
	for (const auto& e : arr)
	{
		t.InsertR(e);
	}
	t.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();

	BSTree<int> copy = t;
	copy.InOrder();

	BSTree<int> t2;
	t2.InsertR(3);
	t2.InsertR(5);
	t2.InsertR(4);
	copy = t2;
	t2.InOrder();
	copy.InOrder();
}

int main()
{
	test4();
	return 0;
}


三、搜索二叉树的应用

3.1 K 模型

K模型,即只有 key 作为关键码,我们上面写的就是 K 模型,

结构中只需存储 key 即可,关键码就是需要搜索到的值。

举个例子:对于单词 word,我们需要判断该单词是否拼写正确

以单词集合中的每个单词作为key,构建一个搜索二叉树。

在二叉树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。


3.2 KV 模型

KV模型,每一个关键码 key,都有与之相对应的值 Value,即 <Key, Value> 的键值对。

这就像 python 中的 dict 字典类型一样,key 和 value 对应。

这在生活中也是非常常见的,比如英汉词典就是英文和中文的对应关系,通过英文可以快速检索到对应的中文,英文单词也可以与其对应的中文构建出一种键值对:

<string, string> 即 <word, chinese>

再比如统计水果次数,就构建出了一种键值对:

<string, int> 即 <水果, count>

直接放用于测试的代码:

BinarySearchTree.h

//二叉搜索树的KV结构
namespace keyValue
{
	//定义2个类模板参数K、V
	template<class K,class V>
	struct BSTreeNode
	{
		K _key;		//存放了2个类型的数据,相比较于K类型
		V _value;
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;

		BSTreeNode(const K& key,const V& value)
			:_key(key)
			,_value(value)
			,_left(nullptr)
			,_right(nullptr)
		{ }
	};

	//同样的,定义2个类模板参数K、V
	//搜索二叉树依旧是按照K的数据进行排序,与V无关
	template<class K,class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:

		//查找只和数据_key有关,与数据_value无关
		Node* find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}
			return nullptr;
		}

		//插入新节点和原来的代码逻辑相似
		//只是多加了1个value数据
		bool insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}

			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}

			//插入新节点
			cur = new Node(key,value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}

		//删除数据只和数据_key有关,与数据_value无关
		bool erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;

			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					//删除操作
					if (cur->_left == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_right;
							else
								parent->_right = cur->_right;
						}
						delete cur;
						return true;
					}
					else if (cur->_right == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_left;
							else
								parent->_right = cur->_left;
						}
						delete cur;
						return true;
					}
					else 
					{
						//被删除的当前节点有2个孩子
						//右子树的最小节点作为替代节点
						Node* rightMinP = cur;
						Node* rightMin = cur->_right;

						while (rightMin->_left)
						{
							rightMinP = rightMin;
							rightMin = rightMin->_left;
						}

						cur->_key = rightMin->_key;
						if (rightMinP->_left == rightMin)
							rightMinP->_left = rightMin->_right;
						else
							rightMinP->_right = rightMin->_right;

						delete rightMin;
						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->_value << endl;
			_InOrder(root->_right);
		}
		Node* _root = nullptr;
	};
}

test.cpp

#include"BinarySearchTree.h"
void test2()
{
	keyValue::BSTree<string, string> dict;	//字典树,如果所有单词都在里面就能很准确查找
	dict.insert("up", "上边");
	dict.insert("down", "下边");
	dict.insert("left", "左边");
	dict.insert("right", "右边");

	string str;
	while (cin >> str)	//Ctrl+z+换行结束或者Ctrl+c结束
	{
		keyValue::BSTreeNode<string, string>* ret = dict.find(str);
		if (ret)
		{
			cout << "对应的中文: " << ret->_value << endl;
		}
		else
		{
			cout << "无此单词,请重新输入!" << endl;
		}
	}
}

int main()
{
	test2();

	return 0;
}

再来一个例子:

#include"BinarySearchTree.h"
void test3()	//统计水果出现的次数
{
	string arr[] = { "苹果","西瓜","葡萄","香蕉","西瓜",
					"西瓜","苹果","葡萄","苹果","西瓜" };

	keyValue::BSTree<string, int> CountTree;
	for (auto& str : arr)
	{
		//先查找水果在不在搜索树中
		//1、不在,说明水果第1次出现,则插入<水果,1>
		//2、在,则查找到的节点中水果对应的次数++
		//keyValue::BSTreeNode<string, int>* ret = CountTree.find(str);
		auto ret = CountTree.find(str);
		if (ret == nullptr)
		{
			CountTree.insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	CountTree.InOrder();
}

int main()
{
	test3();

	return 0;
}


片尾

今天我们学习了二叉搜索树,这是学习map和set的必备知识,希望看完这篇文章对友友们有所帮助!!!

点赞收藏加关注!!!

谢谢大家!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值