【C++ STL】-- 红黑树的插入实现

目录

红黑树的概念

二叉树搜索树的应用

红黑树节点的定义

红黑树结构

insert

需调整的多情况的核心思维:

需调整的多情况分类讲解:

情况一:

情况二:

情况三:

总结:

代码实现:

对于红黑树是否建立成功的检查

升序打印

检查是否为红黑树

执行

代码汇总


红黑树的概念

        红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的:
  • 每个结点不是红色就是黑色
  • 根节点是黑色的 
  • 如果一个节点是红色的,则它的两个孩子结点是黑色的 
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 
  • 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

        所以通过两个颜色,以红色不能相挨为核心,打造出一条路径最多为黑-红-黑-红,叶节点NIL为黑且是空。而另一条路径为全黑,这样正好是,满足上面的性质,且红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍。

二叉树搜索树的应用

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树。
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:

比如:英汉词典就是英文与中文的对应关系,具体方式如下:

  • 通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对。
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
  • 使用一个结构体pair(键值对):

pair的相关文件

红黑树节点的定义

        对于红黑树的实现,以K模型做讲解。

// 节点颜色
enum Color{RED, BLACK};

// 红黑树节点的定义
template<class ValueType>
struct RBTreeNode {
	RBTreeNode<ValueType>* _Right;  // 节点的右孩子
	RBTreeNode<ValueType>* _Left;   // 节点的左孩子
	RBTreeNode<ValueType>* _Parent; // 节点的双亲

	ValueType _Data; // 节点的值
	Color _Color;

	// 构造函数
	RBTreeNode(const ValueType& data = ValueType(), Color color = RED)
		:_Right(nullptr), _Left(nullptr), _Parent(nullptr)
		,_Data(data), _Color(color)
	{}
};

红黑树结构

        首先,由于红黑树符合其最长路径中黑色节点个数不会超过最短路径黑色节点个数的两倍。那么对于一个数据的插入,就会考虑为红色的,因为插入黑色面临黑色节点的个数改变,调整更难,于是考虑简单的红色,因为其不会考虑个数,自会考虑是否由两个红节点相连。

        (由于红黑树的模型主要关注的是节点颜色,所以篇博客的图以三角形代表抽象的红黑树结构)

insert

        对于insert最友好的情况就是插入节点的父节点就是黑色、插入节点就是根节点,对于节点的父节点就是黑色,且并未干扰到路径的黑色节点个数的同时,也不会有两个红色节点相挨:

        插入节点就是根节点:

// 插入的位置是根节点
if (_root == nullptr)
{
	_root = new Node(data);
	_root->_color = BLACK;
	return true;
}

需调整的多情况的核心思维:

  • cur(插入)为红,parent为红,grandfather为黑

         由于我们插入的颜色为红色,所以主要的调整就是颜色调整,而我们插入的环境是红黑树之下,所以处于颜色的不平衡只会是:插入的节点为红,其父节点为红,父节点的父节点为黑,这个是一定的。

(因为颜色不成立一定是两个红色相挨了,而相挨的只能是插入的与其的父节点,否则原来必定不是红黑树结构)

  • insert的多情况由于uncle(父节点的兄弟节点)

        我们的插入的节点的干扰到的颜色分布一定是grandfather节点的子树的黑节点个数平衡,而又影响到grandfather节点的parent节点的左右子树,以此向上,一直到根节点,最终就是一节颗树都不符合。所以重点就是左右子树,也就是parent与uncle节点颜色的问题。

        而如果uncle为红还好,可以直接与father一起变黑色保持左右黑节点个数的同时解决掉cur(插入)为红,parent为红的情况,随后grandfather节点变为新的插入(具体实现后面细讲)。如此渐渐向上,保持左右子树黑节点个数平衡颜色达标。此外,如果uncle不在呢?uncle为黑色呢,所以,这就是多情况的原因。

需调整的多情况分类讲解:

  • 情况一:

    • cur为红,p为红,g为黑,u存在且为红

          由于a、b、c、d、e的情况有很多很多,所以我们就以简易的具象图结构讲解,原理与复杂结构是一样的:

一.  a、c、d、e是NIL

二. a、b、c、d、e是最简单的子树结构

        对此由于插入的节点一定是叶子节点,所以不可能由插入在中间的情况,需要分析:

        所以,实际上对于此种情况转换过来时的情况为:

         随后:

情况一总结:

  • 固定情况:
    • u存在且为红
  • 固定处理:
    • p、u变黑、g变红。继续把g当成cur,g不是根继续往上处理,g是根在其变为红色的情况下变为黑色。

  • 情况二:

    • cur为红,p为红,g为黑,u不存在/u存在且为黑(单旋)

        由于u不存在/u存在且为黑可以更加详细的画出:

1. u存在且为黑的情况具像图举例:

        cur其实原来是黑色的,是由情况一变化而来的。


处理的核心:

        因为红黑树是是一个标准的搜索树,所以一定符合左子树 < 根 < 右子树,那么对于路径长度的变换,是不能通过简单的平移节点所能达成的:

左旋:

        根 < 左子树是关键。

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

	// 将subR连接到parent的右
	parent->_right = subRL;
	if (subRL) // 可能subR的左子树不存在
		subRL->_parent = parent;

	Node* pparent = parent->_parent;

	// 将parent连接到subR的左节点上,成为subR的左子树
	subR->_left = parent;
	parent->_parent = subR;

	// 将parent的父节点状态给予subR
	if (_root == parent) // parent是根
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else // parent不是根
	{
		if (pparent->_left == parent)
			pparent->_left = subR;
		else
			pparent->_right = subR;
		subR->_parent = pparent;
	}
}

右旋:

        根 > 右子树是关键。

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

	// 将subL连接到parent的左
	parent->_left = subLR;
	if (subLR) // 可能subL的右子树不存在
		subLR->_parent = parent;

	Node* pparent = parent->_parent;

	// 将parent连接到subL的右节点上,成为subL的右子树
	subL->_right = parent;
	parent->_parent = subL;

	// 将parent的父节点状态给予subL
	if (_root == parent) // parent是根
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else // parent不是根
	{
		if (pparent->_left == parent)
			pparent->_left = subL;
		else
			pparent->_right = subL;
		subL->_parent = pparent;
	}
}

对于u不存在的处理方式(右单旋):

        以g为轴点右单旋。然后p变黑、g变红。

对于u存在且为黑的处理方式(右单旋):

        与u不存在时同理,以g为轴点右单旋。然后p变黑、g变红。


        上述是右单旋转的情况,如果p和u的位置换了换,就是左单旋然后p变黑、g变红。

对于u不存在的处理方式(左单旋):

        以g为轴点左单旋。然后p变黑、g变红。

对于u存在且为黑的处理方式(左单旋):

       与u不存在时同理,以g为轴点左单旋。然后p变黑、g变红。

情况二总结:

  • 固定情况:
    • u不存在/u存在且为黑
  • 固定处理:(单旋变色)
    • 单选后,p变黑、g变红
      • a、 p 为 g 的左孩子,cur 为 p 的左孩子,则进行右单旋转p变黑、g变红
        b、 p 为 g 的右孩子,cur 为 p 的右孩子,则进行左单旋转,p变黑、g变红

  • 情况三:

    • cur为红,p为红,g为黑,u不存在/u存在且为黑(双旋)

         由于u不存在/u存在且为黑可以更加详细的画出:

 1. u存在且为黑的情况具像图举例:

        cur其实原来是黑色的,是由情况一变化而来的。


对于u不存在的处理方式(右单旋):

        以p为轴点右单旋。则转换成了情况二。

对于u存在且为黑的处理方式(右单旋):

        与u不存在时同理,以p为轴点右单旋。则转换成了情况二。

对于u不存在的处理方式(左单旋):

        以p为轴点左单旋。则转换成了情况二。

 对于u存在且为黑的处理方式(左单旋):

        与u不存在时同理,以p为轴点左单旋。则转换成了情况二。

情况三总结:

  • 固定情况:
    • u不存在/u存在且为黑
  • 固定处理:(单旋变色)
    • 单选后,则转换成了情况二
      • a、 p g 的左孩子, cur p 的右孩子,则针对 p做左单旋转,则转换成了情况二
        b、 p g 的右孩子, cur p 的左孩子,则针对 p做右单旋转。则转换成了情况

总结:

Note:红黑树的关键是叔叔节点

  • u存在且为红,变色继续往上处理。
  • u不存在或存在且为黑,旋转 + 变色
    • 单旋 + 变色
    • 双旋 + 变色

代码实现:

核心实现:

        对于情况一:问题不大,因为没有左右旋。所以,对于parent处于grandfather左右节点位置,只会是uncle与father,处于位置的变化。

        对于情况二、三:uncle与father,处于位置的须注意的同时。又由于需要左右旋,所以,对于parent处于grandfather左右节点位置很重要的,那么就可以以(情况二:单旋+变色     情况三:单旋+情况二)的第一个操作分为一起

bool insert(const ValueType& data)
{
	// 插入的位置是根节点
	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_color = BLACK;
		return true;
	}

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

	cur = new Node(data);
	cur->_color = RED;

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

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

			//情况一:uncle存在且为红,p、u变黑,g变红。继续向上
			if (uncle && uncle->_color == RED)
			{
				parent->_color = uncle->_color = BLACK;
				grandfather->_color = RED;
				//继续向上
				cur = grandfather;
				parent = cur->_parent;
			}
			else// 情况二+三:uncle不存在 + 存在且为黑
			{
				//情况二:右旋 + p变黑,g变红
				//      g
				//   p     u
				// c
				if (cur == parent->_left)
				{
					RotateR(grandfather);
					parent->_color = BLACK;
					grandfather->_color = RED;
				}
				//情况三:左旋 + 情况二(右旋 + p变黑,g变红)
				//     g
				//  p     u
				//    c
				else
				{
					RotateL(parent);
					RotateR(grandfather);
					cur->_color = BLACK;
					grandfather->_color = RED;
				}
				break;
			}
		}
		else
		{
			Node* uncle = grandfather->_left;

			//情况一:uncle存在且为红,p、u变黑,g变红。继续向上
			if (uncle && uncle->_color == RED)
			{
				parent->_color = uncle->_color = BLACK;
				grandfather->_color = RED;
				//继续向上
				cur = grandfather;
				parent = cur->_parent;
			}
			else// 情况二+三:uncle不存在 + 存在且为黑
			{
				//情况二:左旋 + p变黑,g变红
				//      g
				//   u     p
				//           c
				if (cur == parent->_right)
				{
					RotateL(grandfather);
					parent->_color = BLACK;
					grandfather->_color = RED;
				}
				//情况三:右旋 + 情况二(左旋 + p变黑,g变红)
				//      g
				//   u     p
				//       c
				else
				{
					RotateR(parent);
					RotateL(grandfather);
					cur->_color = BLACK;
					grandfather->_color = RED;
				}
				break;
			}
		}
	}
	_root->_color = BLACK;
	return true;
}

对于红黑树是否建立成功的检查

升序打印

        利用中序递归实现,升序打印,因为this指针没法递归,所以需写一个函数传入根节点。

// 利用递归中序按升序打印红黑树
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}

// 中序递归
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;

	_InOrder(root->_left);
	cout << root->_data << " ";
	_InOrder(root->_right);
}

检查是否为红黑树

  • 是红黑树的重点:
    • 颜色分布(无红色节点相连)
    • 每条路径的黑色节点个数相同
    • 最长路径最多为最短路径的2倍
    • 根节点存在即为黑色

        对于根节点存在即为黑色颜色分布(无红色节点相连),第一个:if判断即可,第二个:节点与其的父节点不同时为红色即可。

        对于最长路径最多为最短路径的2倍每条路径的黑色节点个数相同。如果从 " 最长路径最多为最短路径的2倍入手 " ,即以高度入手,是可以保证最长路径最多为最短路径的2倍,但是是没有办法保证颜色的分布,而,如果以 " 每条路径的黑色节点个数相同 " 入手,利用无红色节点相连就可以保证,在每条路径的黑色节点个数相同的前提下,最长路径最多为最短路径的2倍。

// 检测是否符合红黑树
bool IsBalance()
{
	if (_root == nullptr)
	{
		return true;
	}

	// 判断根节点是否为黑色
	if (_root->_color == RED)
	{
		cout << "根节点不黑色" << endl;
		return false;
	}

	int benchMark = 0; //某路径的黑节点个数,作为基准值
	return PrevCheck(_root, 0, benchMark);
}

// 利用深度优先
bool PrevCheck(Node* root, int blackNum, int& benchMark) //利用引用保存基准值
{
	if (root == nullptr)
	{
		if (benchMark == 0) //将第一个路径的黑节点个数给benchMark,作为基准值
			benchMark = blackNum;
		else if (blackNum != benchMark)
			return false;
		return true;
	}

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

	// 检查是否有连续的红色节点
	if (root->_color == RED && root->_parent->_color == RED)
	{
		cout << "存在连续的红节点" << endl;
		return false;
	}

	return PrevCheck(root->_left, blackNum, benchMark)
		&& PrevCheck(root->_right, blackNum, benchMark);
}

执行

void TestRBTree()
{
	size_t N = 100;
	srand(time(0));
	RBTree<int> t;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand();
		t.insert(x);
	}

	t.InOrder();

	if (t.IsBalance())
		cout << "是红黑树" << endl;
	else
		cout << "不是红黑树" << endl;
}

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

代码汇总

#include<iostream>
#include<assert.h>
using namespace std;
// 节点颜色
enum Color{RED, BLACK};

// 红黑树节点的定义
template<class ValueType>
struct RBTreeNode {
	RBTreeNode<ValueType>* _right;  // 节点的右孩子
	RBTreeNode<ValueType>* _left;   // 节点的左孩子
	RBTreeNode<ValueType>* _parent; // 节点的双亲

	ValueType _data; // 节点的值
	Color _color;

	// 构造函数
	RBTreeNode(const ValueType& data = ValueType(), Color color = RED)
		:_right(nullptr), _left(nullptr), _parent(nullptr)
		,_data(data), _color(color)
	{}
};

template<class ValueType>
class RBTree {
	typedef RBTreeNode<ValueType> Node;
public:
	bool insert(const ValueType& data)
	{
		// 插入的位置是根节点
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_color = BLACK;
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
        // 查找cur插入的位置
		while (cur)
		{
			if (data > cur->_data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (data < cur->_data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}

		cur = new Node(data);
		cur->_color = RED;

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

		while (parent && parent->_color == RED)
		{
			Node* grandfather = parent->_parent;
			assert(grandfather);
			assert(grandfather->_color == BLACK);
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				
				//情况一:uncle存在且为红,p、u变黑,g变红。继续向上
				if (uncle && uncle->_color == RED)
				{
					parent->_color = uncle->_color = BLACK;
					grandfather->_color = RED;
					//继续向上
					cur = grandfather;
					parent = cur->_parent;
				}
				else// 情况二+三:uncle不存在 + 存在且为黑
				{
					//情况二:右旋 + p变黑,g变红
					//      g
					//   p     u
					// c
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_color = BLACK;
						grandfather->_color = RED;
					}
					//情况三:左旋 + 情况二(右旋 + p变黑,g变红)
				    //     g
				    //  p     u
				    //    c
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_color = BLACK;
						grandfather->_color = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;

				//情况一:uncle存在且为红,p、u变黑,g变红。继续向上
				if (uncle && uncle->_color == RED)
				{
					parent->_color = uncle->_color = BLACK;
					grandfather->_color = RED;
					//继续向上
					cur = grandfather;
					parent = cur->_parent;
				}
				else// 情况二+三:uncle不存在 + 存在且为黑
				{
					//情况二:左旋 + p变黑,g变红
					//      g
					//   u     p
					//           c
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_color = BLACK;
						grandfather->_color = RED;
					}
					//情况三:右旋 + 情况二(左旋 + p变黑,g变红)
					//      g
					//   u     p
					//       c
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_color = BLACK;
						grandfather->_color = RED;
					}
					break;
				}
			}
		}
		_root->_color = BLACK;
		return true;
	}

	// 利用递归中序按升序打印红黑树
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 检测是否符合红黑树
	bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}

		// 判断根节点是否为黑色
		if (_root->_color == RED)
		{
			cout << "根节点不黑色" << endl;
			return false;
		}

		int benchMark = 0; //某路径的黑节点个数,作为基准值
		return PrevCheck(_root, 0, benchMark);
	}

private:
	// 利用深度优先
	bool PrevCheck(Node* root, int blackNum, int& benchMark) //利用引用保存基准值
	{
		if (root == nullptr)
		{
			if (benchMark == 0) //将第一个路径的黑节点个数给benchMark,作为基准值
				benchMark = blackNum;
			else if(blackNum != benchMark)
				return false;
			return true;
		}

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

		// 检查是否有连续的红色节点
		if (root->_color == RED && root->_parent->_color == RED)
		{
			cout << "存在连续的红节点" << endl;
				return false;
		}

		return PrevCheck(root->_left, blackNum, benchMark)
			&& PrevCheck(root->_right, blackNum, benchMark);
	}

	// 中序递归
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}

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

		// 将subL连接到parent的左
		parent->_left = subLR;
		if (subLR) // 可能subL的右子树不存在
			subLR->_parent = parent;

		Node* pparent = parent->_parent;

		// 将parent连接到subL的右节点上,成为subL的右子树
		subL->_right = parent;
		parent->_parent = subL;

		// 将parent的父节点状态给予subL
		if (_root == parent) // parent是根
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else // parent不是根
		{
			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;

		// 将subR连接到parent的右
		parent->_right = subRL;
		if (subRL) // 可能subR的左子树不存在
			subRL->_parent = parent;

		Node* pparent = parent->_parent;

		// 将parent连接到subR的左节点上,成为subR的左子树
		subR->_left = parent;
		parent->_parent = subR;

		// 将parent的父节点状态给予subR
		if (_root == parent) // parent是根
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else // parent不是根
		{
			if (pparent->_left == parent)
				pparent->_left = subR;
			else
				pparent->_right = subR;
			subR->_parent = pparent;
		}
	}

	Node* _root = nullptr;
};

void TestRBTree()
{
	size_t N = 100;
	srand(time(0));
	RBTree<int> t;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand();
		t.insert(x);
	}

	t.InOrder();

	if (t.IsBalance())
		cout << "是红黑树" << endl;
	else
		cout << "不是红黑树" << endl;
}

int main()
{
	TestRBTree();
	return 0;
}
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
C++ STL中,红黑树实现被封装在`std::map`和`std::set`这两个容器类中。这两个容器类都是基于红黑树实现的,它们提供了高效的查找、插入和删除操作,保证了元素的有序性。 STL中的红黑树实现与你提供的C++代码略有不同。STL中的红黑树使用节点颜色(红色或黑色)和节点指针(parent、left、right)来表示树的结构,而你提供的代码使用了模板和节点对象来实现。 在STL中,红黑树插入和删除操作已经被封装在`std::map`和`std::set`中,使用起来非常简单。你只需要包含相应的头文件`<map>`或`<set>`,并使用`std::map`或`std::set`类来定义变量,就可以直接使用红黑树的功能了。 以下是使用STL红黑树的简单示例: ```cpp #include <map> int main() { std::map<int, std::string> myMap; // 插入元素 myMap.insert(std::make_pair(1, "one")); myMap = "two"; // 查找元素 auto it = myMap.find(1); if (it != myMap.end()) { std::cout << it->second << std::endl; // 输出 "one" } // 删除元素 myMap.erase(2); return 0; } ``` 在上面的示例中,我们使用`std::map`来创建一个键-值对的红黑树。我们使用`insert`函数插入元素,使用`find`函数查找元素,使用`erase`函数删除元素。 总结一下,C++ STL中的红黑树实现被封装在`std::map`和`std::set`中,使用起来非常方便。你可以直接包含相应的头文件,并使用这些类来实现红黑树的功能。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川入

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值