<C++> 红黑树封装map/set

目录

一、map和set类模板​​​​​​

二、红黑树节点定义

1.红黑树的节点修改 

2.仿函数 

(1)节点比较大小时存在的问题 

(2)解决不同类型的 key 获取问题

三、红黑树迭代器

1、移动操作

2、解决 set 迭代器的非法操作

四、RBTree完整代码

五、map模拟实现

六、set模拟实现


一、map和set类模板​​​​​​

在同时封装 set 和 map 时,面临第一个问题:两者参数不匹配

  • set 只需要 key
  • map 则需要 key 和 value

红黑树同时封装出set和map时,set传给value的是一个value,map传给value的是一个pair,set和map传给红黑树的value决定了这棵树里面存的节点值类型。上层容器不同,底层红黑树的Key和T也不同。

参考库中的解决方案:管你是 k 还是 k/v,我都看作 value_type,获取 key 值时再另想其他方法解决

rb_tree 的参数3是获取 key 的方式(后续介绍),参数4是比较方式,参数5是空间配置器

能否省略 参数1 key_type ?

  • 对于 set 来说,可以,因为冗余了
  • 但对于 map 来说,不行,因为 map 中的函数参数类型为 key_type,省略后就无法确定参数类型了,比如 FindErase 中都需要 key_type 这个类型

在上层容器set中,K和T都代表Key,底层红黑树节点当中存储K和T都是一样的;map中,K代表键值Key,T代表由Key和Value构成的键值对,底层红黑树中只能存储T。所以红黑树为了满足同时支持set和map,节点当中存储T。

这就要对红黑树进行改动。

二、红黑树节点定义

1.红黑树的节点修改 

template<class K,class V>

修改为

template<class T>

那么节点定义就修改为

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;//节点的值,_data里面存的是K就传K,存的是pair就传pair

	Colour _col;

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

2.仿函数 

(1)节点比较大小时存在的问题 

红黑树插入节点时,需要比较节点的大小,kv需要改成_data:

pair<iterator, bool> Insert(const T& data) { //... }

但是以上代码在插入新节和查找节点时,当和当前节点比较大小时,Key可以比较,但是pair比较不了,也就是set可以比较,但是map比较不了。这就需要写一个仿函数,如果是map就取_data里面的first也就是Key进行比较,通过泛型解决红黑树里面存的是什么。所以上层容器map需要向底层的红黑树提供仿函数来获取T里面的Key,这样无论上层容器是set还是map,都可以用统一的方式进行比较了。
 

Find函数也有相同问题

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (kot(cur->_data) > key)
				cur = cur->_left;
			else if (kot(cur->_data) < key)
				cur = cur->_right;
			else
				return cur;
		}
		return nullptr;
	}

可以看到,Find() 的参数类型为 K

此时面临着一个尴尬的问题:当 T 为 key 时,_data 不是 pair,自然没有 first 和 second,程序也就无法跑起来。

凡是涉及获取 key 的地方都有这个问题,因为此时的 _data 是不确定的,对于这种不确定的类型,一般使用 仿函数 解决。

(2)解决不同类型的 key 获取问题

现在可以看看库中 rb_tree 的参数3了,它是一个 函数对象,可以传递 仿函数,主要是用来从不同的 T 中获取 key 值

 set 中的 key 值就是 key,而 map 中的 key 值是 pair<K, V> 中的 first。

set有set的仿函数,map有map的仿函数,尽管set的仿函数看起来没有什么作用,但是,必须要把它传给底层红黑树,这样红黑树就能根据仿函数分别获取set的key和map的first。

①set的仿函数

	template<class K>
	class set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		pair<iterator, bool>  insert(const K& key)
		{
			return _t.Insert(key);
		}
	private:
		RBTree<K, K, SetKeyOfT>  _t;
	};

②map的仿函数

	template<class K, class V>
	class map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};
	private:
		RBTree<K, pair<const K, V>, MapKeyOfT>  _t;
	};

当我们得到不同的 key 值获取方式后,就可以更改 红黑树 中相应的代码了

比如:查找

	Node* Find(const K& key)
	{
		Node* cur = _root;
		KeyOfT kot;//创建一个对象,用来获取 key 值
		while (cur)
		{
			if (kot(cur->_data) > key)
				cur = cur->_left;
			else if (kot(cur->_data) < key)
				cur = cur->_right;
			else
				return cur;
		}
		return nullptr;
	}

三、红黑树迭代器

map和set的迭代器的实现其实本质上是红黑树迭代器的实现,迭代器的实现需要定义模板类型、模板类型引用、模板类型指针。 

将 红黑树 的节点再一次封装,构建一个单独的 迭代器 类

因为此时节点的模板参数有 K 和 V,所以 迭代器类 中也需要这两个参数

至于 迭代器类 设计时的精髓:不同类型的迭代器传递不同的参数 这里就不再展开叙述,简单来说,额外增加 Ref Ptr 的目的是为了让 普通迭代器 和 const 迭代器 能使用同一个 迭代器类
 

template<class T, class Ref, class Ptr>
struct __RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBTreeIterator<T, Ref, Ptr> Self;
	Node* _node;

	// 节点指针构造迭代器
	__RBTreeIterator(Node* node)
		:_node(node)
	{}

	//1、typedef __RBTreeIterator<T, T&, T*> iterator; // 拷贝构造
	// 
	// //支持普通迭代器构造const迭代器的构造函数
	//2、typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
	__RBTreeIterator(const __RBTreeIterator<T, T&, T*>& it)
		:_node(it._node)
	{}
private:
	Node* _node;
};

其中的 RefPtr 具体是什么类型,取决于调用方传递了什么

1、移动操作

迭代器 最重要的操作莫过于 移动红黑树 的迭代器是一个 双向迭代器,只支持 ++ 和 -- 操作

树形 结构的容器在进行遍历时,默认按 中序遍历 的顺序进行迭代器移动,因为这样遍历 二叉搜索树 后,结果为 有序

正向移动思路:

  1. 判断当前节点的右子树是否存在,如果存在,则移动至右子树中的最左节点
  2. 如果不存在,则移动至当前路径中 孩子节点为左孩子的父亲节点
  3. 如果父亲为空,则下一个节点就是空
  4. Self& operator++()
    	{
    		if (_node->_right)
    		{
    			// 1、右不为空,下一个就是右子树的最左节点
    			Node* childLeft = _node->_right;
    			while (childLeft->_left)
    			{
    				childLeft = childLeft->_left;
    			}
    			_node = childLeft;
    		}
    		else
    		{
    			// 2、右为空,沿着到根的路径,找孩子是父亲左的那个祖先
    			Node* cur = _node;
    			Node* parent = cur->_parent;
    			while (parent && cur == parent->_right)
    			{
    				cur = parent;
    				parent = parent->_parent;
    			}
    			_node = parent;
    		}
    		return *this;
    	}

    为什么右子树不为空时,要访问 右子树的最左节点

     ·  因为此时是正向移动,路径为 左根右,如果右边路径存在,就要从它的最左节点开始访问

为什么右子树为空时,要访问当前路径中 孩子节点为左孩子 的父亲节点       

 ·  因为 孩子节点为右孩子 的父亲节点已经被访问过了

 -- 啥都反过来就行

	Self& operator--()
	{
		if (_node->_left)
		{
			// 1、左不为空,下一个就是左子树的最左节点
			Node* childRight = _node->_left;
			while (childRight->_right)
			{
				childRight = childRight->_right;
			}
			_node = childRight;
		}
		else
		{
			// 2、左为空,沿着到根的路径,找孩子是父亲右的那个祖先
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}

2、解决 set 迭代器的非法操作

此时的代码仍然存在问题:set 中只有 keykey 是不能修改的,但此时 set 中的 key 可以被修改!

void SetAndMapTest3()
{
	vector<int> arr = { 8,6,3,2,1 };
	set<int> s;
	for (auto e : arr)
		s.Insert(e);

	cout << "修改前: ";
	for (auto& e : s)
	{
		cout << e << " ";
		e = 1;
	}
	cout << endl;

	cout << "修改后: ";
	for (auto& e : s)
		cout << e << " ";
	cout << endl;
}

此时居然能将 set 中的 key 进行修改!? 这是非常不合理的

库中给出的解决方案:对于 set 来说,无论是否为 const 迭代器,都使用 红黑树中的 const 迭代器进行适配

也就是说,锁死了 set 中迭代器的修改权限,此时自然无法修改 key 值

  • 此时迭代器类中的 Ref 和 Ptr 都是 const 版本
set.h

//迭代器
typedef typename Tree::const_iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::const_reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;

修改完成,VS 启动,代码,运行

 

结果:出现了一个编译错误

注意: 先要把修改相关的代码屏蔽,否则会导致这个错误无法出现

出现错误的原因

  • 在 set 中,普通对象调用 begin() 或 end() 时,返回的是 普通迭代器,但此时的 iterator 是 const 迭代器,这就涉及一个类型转换问题了,其中的 RefPtr 类型不匹配!

解决方案:在 红黑树迭代器类 中新增一个特殊的构造函数

  • 当类模板实例化为 普通迭代器 时,就是一个普通的 拷贝构造 函数
  • 当类模板实例化为 const 迭代器 时,则是一个特殊的构造函数 -> 将普通的迭代器对象 -> 构造为 const 迭代器
    	//1、typedef __RBTreeIterator<T, T&, T*> iterator; // 拷贝构造
    	// 
    	// //支持普通迭代器构造const迭代器的构造函数
    	//2、typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
    	__RBTreeIterator(const __RBTreeIterator<T, T&, T*>& it)
    		:_node(it._node)
    	{}

    如何做到的?

  • 当创建 set(普通对象) 中的普通迭代器时,因为此时是普通对象,所以 红黑树 底层会返回一个 普通迭代器,但对于 set 来说,无论是否为 const 对象,它要返回的都是 const 迭代器,于是它会把 红黑树返回的普通迭代器 -> 借助特殊的构造函数 -> 构造为 const 迭代器

  • 如果 set 是 const 对象,那么 红黑树 返回的就是 const 迭代器,都不用进行类型转换了

这种写法对于 map 是否有影响?

  • 没有影响,对于 map 来说,普通对象对应的就是普通迭代器,不存在 普通迭代器 转为 const 迭代器 这种情况

新增这个特殊的构造函数后,能正常编译,将 e = 1 这条赋值语句取消注释,再编译,可以发现出现了预料中的报错信息:不能给常量对象赋值

 注意: set 中的普通对象对应的也是 const 迭代器,但底层 红黑树 仍然是普通对象,返回的普通迭代器无法转换为 set 中的 const 迭代器,需要通过特殊构造函数解决;不能单纯的通过 const 修饰迭代器暴力解决问题,因为这样会出现 const const 的问题

四、RBTree完整代码

#pragma once
#include <iostream>
#include <assert.h>
#include <time.h>
using namespace std;


// 红黑树封装map、set所需的红黑树

enum Colour
{
	RED,
	BLACK,
};

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;//节点的值,_data里面存的是K就传K,存的是pair就传pair

	Colour _col;

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

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

	// 节点指针构造迭代器
	__RBTreeIterator(Node* node)
		:_node(node)
	{}

	//1、typedef __RBTreeIterator<T, T&, T*> iterator; // 拷贝构造
	// 
	// //支持普通迭代器构造const迭代器的构造函数
	//2、typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
	__RBTreeIterator(const __RBTreeIterator<T, T&, T*>& it)
		:_node(it._node)
	{}

	Ref operator*()
	{
		return _node->_data; 
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	Self& operator++()
	{
		if (_node->_right)
		{
			// 1、右不为空,下一个就是右子树的最左节点
			Node* childLeft = _node->_right;
			while (childLeft->_left)
			{
				childLeft = childLeft->_left;
			}
			_node = childLeft;
		}
		else
		{
			// 2、右为空,沿着到根的路径,找孩子是父亲左的那个祖先
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}

	Self& operator--()
	{
		if (_node->_left)
		{
			// 1、左不为空,下一个就是左子树的最左节点
			Node* childRight = _node->_left;
			while (childRight->_right)
			{
				childRight = childRight->_right;
			}
			_node = childRight;
		}
		else
		{
			// 2、左为空,沿着到根的路径,找孩子是父亲右的那个祖先
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}
private:
	Node* _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*> const_iterator;

	iterator begin()
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}

		return iterator(cur);
	}

	iterator end()
	{
		return iterator(nullptr);
	}


	~RBTree()
	{
		_Destroy(_root);
		_root = nullptr;
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		KeyOfT kot;//创建一个对象,用来获取 key 值
		while (cur)
		{
			if (kot(cur->_data) > key)
				cur = cur->_left;
			else if (kot(cur->_data) < key)
				cur = cur->_right;
			else
				return cur;
		}
		return nullptr;
	}

pair<iterator, bool> Insert(const T& data)
{
	// 空树
	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_col = BLACK;
		return make_pair(iterator(_root), true);
	}

	KeyOfT kot;
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (kot(cur->_data) > kot(data))// 往左找 
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kot(cur->_data) < kot(data))// 往右找
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return make_pair(iterator(cur), false); // 值相等
		}
	}

	//2.走到这里,说明kv在树中不存在,需要插入data,并且cur已经为空,parent已经是叶子节点了
	cur = new Node(data);
	Node* newnode = cur;

	if (parent->_data > data)
		parent->_left = cur;
	else
		parent->_right = cur;

	cur->_parent = parent; // 反向链接父亲


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

			// 情况1:uncle存在且为红,变色处理,并继续往上调整	
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续往上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2+3:uncle不存在/uncle存在且为黑,旋转+变色 	
			{
				//       g
				//    p     u
				//  c
				if (cur == parent->_left)
				{
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					//       g
					//    p     u
					//		 c
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					//parent->_col = RED;
					grandfather->_col = RED;
				}
				break;
			}
		}
		else // (grandfather->_right == parent)
		{
			//       g
			//    u     p
			//        c   (c)
			Node* uncle = grandfather->_left;

			// 情况1:uncle存在且为红,变色处理,并继续往上调整	
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续往上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2+3:uncle不存在/uncle存在且为黑,旋转+变色 	
			{
				//       g
				//    u     p
				//            c
				if (cur == parent->_right)
				{
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					//       g
					//    u     p
					//		 c
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;
			}
		}
	}
	_root->_col = BLACK;
	return make_pair(iterator(newnode), true);
}

	bool Isbalance()
	{
		if (_root && _root->_col == RED)
		{
			cout << "根节点颜色是红色" << endl;
			return false;
		}

		int benchmark = 0;// 基准值
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				++benchmark;
			cur = cur->_left;
		}

		// 连续红色节点
		return _Check(_root, 0, benchmark);
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	int Height()
	{
		return _Height(_root);
	}
private:
	void _Destroy(Node* root)
	{
		if (root == nullptr)
			return;

		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
	}

	bool _Check(Node* root, int blackNum, int benchmark)
	{
		if (root == nullptr)
		{
			if (benchmark != blackNum)
			{
				cout << "某条路径黑色节点的数量不相等" << endl;
				return false;
			}
			return true;
		}

		if (root->_col == BLACK)
			++blackNum;

		if (root->_col == RED
			&& root->_parent
			&& root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}

		return _Check(root->_left, blackNum, benchmark)
			&& _Check(root->_right, blackNum, benchmark);
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);

		return leftH > rightH ? leftH + 1 : rightH + 1;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}


	// 左单旋
	void RotateL(Node* parent)
	{
		Node* childR = parent->_right;
		Node* childRL = childR->_left;


		parent->_right = childRL;
		if (childRL)
			childRL->_parent = parent; // 更新父亲

		Node* ppnode = parent->_parent;

		childR->_left = parent;
		parent->_parent = childR; // 更新父亲

		// 是一颗单独的树
		if (ppnode == nullptr)
		{
			_root = childR;
			_root->_parent = nullptr;
		}
		else
		{
			// 是一颗子树,判断是左子树还是右子树
			if (ppnode->_left == parent)
			{
				ppnode->_left = childR;
			}
			else
			{
				ppnode->_right = childR;
			}

			childR->_parent = ppnode;// 更新父亲
		}
	}

	// 右单旋
	void RotateR(Node* parent)
	{
		Node* childL = parent->_left;
		Node* childLR = childL->_right;


		parent->_left = childLR;
		if (childLR)
			childLR->_parent = parent; // 更新父亲

		Node* ppnode = parent->_parent;

		childL->_right = parent;
		parent->_parent = childL; // 更新父亲

		// 是一颗单独的树
		if (parent == _root)
		{
			_root = childL;
			_root->_parent = nullptr;
		}
		else
		{
			// 是一颗子树,判断是左子树还是右子树
			if (ppnode->_left == parent)
			{
				ppnode->_left = childL;
			}
			else
			{
				ppnode->_right = childL;
			}

			childL->_parent = ppnode;// 更新父亲
		}
	}

	Node* _root = nullptr;
};

五、map模拟实现

调用红黑树对应接口实现map,插入和查找函数返回值当中的节点指针改为迭代器,增加operator[ ]的重载:

#pragma once
#include "RBTree.h"

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

		iterator begin()
		{
			return _t.begin();
		}
		iterator end()
		{
			return _t.end();
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));
			return ret.first->second;
		}

		pair<iterator, bool> insert(const pair<const K, V>& kv)
		{
			return _t.Insert(kv);
		}
	private:
		RBTree<K, pair<const K, V>, MapKeyOfT>  _t;
	};

	void test_map1()
	{
		map<string, string> dict;
		dict.insert(make_pair("sort", "排序"));
		dict.insert(make_pair("string", "字符串"));
		dict.insert(make_pair("count", "计数"));
		dict.insert(make_pair("stirng", "(字符串)"));

		map<string, string>::iterator it = dict.begin();
		while (it != dict.end())
		{
			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;

		for (auto& kv : dict)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
			
	}

	void test_map2()
	{
		string arr[] = { "西瓜","西瓜","苹果","桃子","西瓜", "李子","李子" };
		map<string, int> countMap;

		for (auto& e : arr)
		{
			countMap[e]++;
		}

		for (auto& kv : countMap)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;

	}
}

六、set模拟实现

调用红黑树对应接口实现set,插入和查找函数返回值当中的节点指针改为迭代器:

#pragma once
#include "RBTree.h"

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

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

	void test_set1()
	{
		set<int> s;

		int a[] = { 4,25,6,1,3,5,15,7,16,14 };
		for (auto e : a)
		{
			s.insert(e);
		}

		set<int>::iterator it = s.begin();
		cout << "修改前: ";
		while (it != s.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		cout << "修改后: ";
		for (auto& e : s)
		{
			//e = 1;
			cout << e << " ";
		}
		cout << endl;

	}
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值