AVL树的介绍与模拟实现(insert+旋转调平精美图示超详解哦)

12 篇文章 0 订阅

引言

在之前的文章中,介绍了二叉搜索树,它可以实现最快O(logN)的搜索时间复杂度:
戳我看二叉搜索树详解哦
但是当二叉搜索树的左右子树高度不平衡时,如果要查找的数据在较高的子树中,那么查找的效率就会降低。在极端情况下就像是在单链表中查找,搜索的时间复杂度就成了O(N)

如果能有某种特性的存在,使得二叉搜索树的左右子树始终是平衡的,即左右子树几乎等高。那么在搜索时的效率就会稳定在接近O(logN)的值上。具有这样的某种特性的二叉搜索树称为平衡二叉树

AVL树就是平衡二叉树的一种:

AVL树介绍

AVL树在满足二叉搜索树条件的基础上,给每个结点中增加了一个平衡因子(这个平衡因子的值是该结点左右子树的高度差);
AVL树要求平衡因子的绝对值小于等于1,即任意一个结点左右子树的高度差不超过1。 通过降低树的高度,从而达到减少平均搜索时间的效果(在这里做例子的平衡二叉树是按照大小次序依次插入的结果)

在这里插入图片描述

AVL树的左右子树的高度差不超过1,是一种高度平衡的二叉树,平均搜索时间可以达到O(logN)

Key与Key-Value

根据搜索时的形式的不同可以分为KK-V两种:

K模式中,只有K这一种类型的数据,在建树时根据key的大小来创建,搜索时也根据key的大小来搜索,搜索到时就获取key这一种数据。适用于判断某段数据中存不存在某个值:

K-V模式中,有两种类型的数据:建树与搜索时以key为依据,通过搜索到的key可以访问到与其在一起存储的值value相当于keyvalue之间建立有一个映射关系。应用范围广泛,例如字典中,首先搜索输入的单词(key)在字典中是否存在,再通过找到的单词访问到其对应的翻译(value):

AVL树模拟实现

对于AVL树的实现,与之前的二叉搜索树不同的就是增加了平衡因子。需要在插入的过程中保持平衡因子的合适,就需要对树的形状做出调整。在本篇文章中介绍的是K-V类型的模式(使用非递归方式):
为防止命名冲突,将实现放在我们的命名空间qqq中:

AVLTree是一个类模板,有两个模板参数,即KV,表示key的类型与value的类型;
成员变量就是根结点的指针

namespace qqq
{
	template<class K, class V>
	class AVLTree
	{
	
	protected:
		Node* _root = nullptr; //Node是根节点类型typedefe来的
	};
}

结点类

接下来来对结点的类型进行实现:

AVLTreeNode是也是一个类模板,模板参数为KV
在结点中储存数据的结构为pair,其中firstK类型,secondV类型;

成员变量有数据_kv
指向父结点的指针_parent
指向左右孩子结点的指针_left_right
以及平衡因子_bf
(由于在AVL树中需要经常访问结点的成员变量,所以结点类使用struct类,其成员默认为public

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _parent;   
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	int _bf;
};

结点类的构造函数

结点类构造函数的参数类型为const pair<K, V>,缺省参数为其默认构造的匿名对象;
在初始化列表中对各个成员变量置空即可:

AVLTreeNode(const pair<K, V> kv = pair<K, V>())
	: _kv(kv)
	, _left(nullptr)
	, _right(nullptr)
	, _parent(nullptr)
	, _bf(0)
{}

insert

在AVL树中插入时:

  • 首先自上而下搜索插入位置;
  • 找到插入位置后将新结点链接到树上;
  • 然后自下而上修改平衡因子;
  • 如果遇到不平衡的情况就进行旋转调平衡:

搜索插入位置

  • 搜索插入位置的部分与二叉搜索树时类似,向下搜索:
  • 首先定义一个结点指针cur向下遍历,另外再使用一个结点指针parent来记录它的父结点,方便找到位置后插入;
  • while循环向下遍历,当cur为空时,即找到插入位置,此时将新结点链接到parent下即可:

在这里插入图片描述

// 部分代码 :向下搜索插入位置//
while (cur != nullptr) //搜索
{
	if (newnode->_kv.first > cur->_kv.first)
	{
		parent = cur;
		cur = parent->_right;
	}
	else if (newnode->_kv.first < cur->_kv.first)
	{
		parent = cur;
		cur = parent->_left;
	}
	else  //相等即插入失败
	{
		return false;
	}
}

当找到位置后,还需要另作判断

  • parent为空时,说明当前插入的结点是树的第一个结点,直接将newnode赋值给_root即可;
  • 若不为空,还需要通过比较parent->firstnewnode->first的值来确定要链接parent的左结点还是右结点
  • 链接即使parent的左或右指针指向newnodenewnode的父指针指向parent
  • 最后令cur等于newnode

在这里插入图片描述

// 部分代码 :判断newnode要链接parent的那个结点 //
if (parent == nullptr) //插入
{
	_root = newnode;
}
else if (newnode->_kv.first < parent->_kv.first)
{
	parent->_left = newnode;
	newnode->_parent = parent;
	cur = newnode;
}
else
{
	parent->_right = newnode;
	newnode->_parent = parent;
	cur = newnode;
}

然后就需要对平衡因子进行修改

  • 现在新结点即cur的平衡因子自然是0,所以直接parent开始循环向上修改
  • 判断,cur插入在了parent的左边,parent的平衡因子 -1,反之 +1 :
// 部分代码 :进行当前位置的修改平衡因子 //
if (cur == parent->_left) //更新parent的平衡因子
{
	--parent->_bf;
}
else
{
	++parent->_bf;
}

修改一次后,对 parent 的平衡因子进行判断

  • parent的平衡因子为0,说明parent这棵子树已经平衡,不会再对上面的结点有所影响,所以直接break
  • parent的平衡因子为-1或1,说明还不平衡,将curparent都向上移动继续修改平衡因子;
  • parent的平衡因子为-2或2,说明这棵树的平衡因子已经不合理,需要进行旋转调平衡;

调平后直接break即可:

在这里插入图片描述

// 部分代码 :对修改后的parent进行判断 //
if (parent->_bf == 0) //parent平衡因子为0,即向上调整结束
{
	break;
}
else if (parent->_bf == 1 || parent->_bf == -1) //继续向上调整
{
	cur = parent;
	parent = cur->_parent;
}
else if (parent->_bf == -2 || parent->_bf == 2) //旋转降低平衡因子
{
	//不合理,进行旋转调平
	break;
}
else
{
	assert(0);
}
  • 最后,当parent的值为空时,即整棵树的平衡因子都已经修改完成,退出循环即可。

在这里插入图片描述

insert整体代码

对于调整平衡这部分代码,在下面的旋转部分会详细介绍

bool insert(const pair<K, V>& kv)
{
	//先插入(搜索插入)
	Node* newnode = new Node(kv);
	Node* parent = nullptr;
	Node* cur = _root;

	while (cur != nullptr) //搜索
	{
		if (newnode->_kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = parent->_right;
		}
		else if (newnode->_kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = parent->_left;
		}
		else  //相等即插入失败
		{
			return false;
		}
	}
	if (parent == nullptr) //插入
	{
		_root = newnode;
	}
	else if (newnode->_kv.first < parent->_kv.first)
	{
		parent->_left = newnode;
		newnode->_parent = parent;
		cur = newnode;
	}
	else
	{
		parent->_right = newnode;
		newnode->_parent = parent;
		cur = newnode;
	}

	//向上调整平衡因子
	while (parent != nullptr)
	{
		if (cur == parent->_left) //更新parent的平衡因子
		{
			--parent->_bf;
		}
		else
		{
			++parent->_bf;
		}

		if (parent->_bf == 0) //parent平衡因子为0,即向上调整结束
		{
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1) //继续向上调整
		{
			cur = parent;
			parent = cur->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2) //旋转降低平衡因子
		{
			if (parent->_bf == 2 && cur->_bf == 1) //右右——单次左旋parent
			{
				RotateL(parent);
			}
			else if (parent->_bf == -2 && cur->_bf == -1) //左左——单次右旋parent
			{
				RotateR(parent);
			}
			else if (parent->_bf == 2 && cur->_bf == -1) //右左——先右旋cur再左旋parent
			{
				RotateRL(parent);
			}
			else if (parent->_bf == -2 && cur->_bf == 1)//左右——先左旋cur再右旋parent
			{
				RotateLR(parent);
			}
			else //兜底
			{
				assert(0);
			}
			break;
		}
		else
		{
			assert(0);
		}
	}
	return true;
}

旋转调平衡

旋转调平衡既要保证调整之后树平衡,有要保证搜索树的特征不变。根本目的就是要降低树的整体高度
在调平衡的情况中,共有四种情况:

右右——左旋

第一种情况为:parent的右子树高度比左子树高,在其右子树中又有右子树较高。我们用gparentparentcurcurleft来记录这些结点:
在这里插入图片描述
需要注意的是,当parent不平衡为右右时,一定存在上面的关系:cur的右子树高度为h + 1cur左子树的高度为hparent左子树的高度也一定是h
cur右子树的高度大于h + 1,则不平衡的结点应该为cur
cur右子树的高度为h, 则cur平衡因子为0,parent就不会不平衡;
parent左子树的高度大于h,则parent就不会不平衡;
parent左子树的高度小于h,则在插入之前此树就已经不平衡了。

在这种情况下,parent的平衡因子为2,cur的平衡因子为1(该结论用于在insert中快速判断为哪种情况)

这种不平衡情况的解决方式为单次左旋:

  • 首先将curleftparent的右指针建立链接curleft一定比parent大;
  • 然后将parentcur的左指针建立链接parentcurleft一定比cur小;
  • 最后再将curparent的父结点建立链接(当然,这需要判断cur应该为gparent的左或右子树),若parent就是_rootgparent就是nullptr,这步就变成了将cur赋值给_root

在这里插入图片描述

  • 调平后,将parentcur 的平衡因子全部修改为0即可:
void RotateL(Node* parent)
{
	Node* gparent = parent->_parent;
	Node* cur = parent->_right;
	Node* curleft = cur->_left;

	parent->_right = curleft;
	if (curleft != nullptr)
	{
		curleft->_parent = parent;
	}

	cur->_left = parent;
	parent->_parent = cur;

	cur->_parent = gparent;
	if (gparent == nullptr)
	{
		_root = cur;
	}
	else
	{
		if (cur->_kv.first < gparent->_kv.first)
		{
			gparent->_left = cur;
		}
		else
		{
			gparent->_right = cur;
		}
	}

	parent->_bf = cur->_bf = 0;
}

左左——右旋

第二种情况为:parent的左子树高度比右子树高,在其左子树中又有左子树较高。我们用gparentparentcurcurright来记录这些结点:
在这里插入图片描述
与上面的情况正好相反,这种左左的情况也一定满足上图的关系:cur的左子树高度为h + 1cur右子树的高度为hparent右子树的高度也一定是h。这里就不赘述了。

在这种情况下,parent的平衡因子为 -2,cur的平衡因子为 -1

解决这种不平衡的方法为单次右旋(与单次左旋相反):

  • 首先将currightparent的左指针建立链接curright一定比parent大;
  • 然后将parentcur的右指针建立链接parentcurright一定比cur大;
  • 最后再将curparent的父结点建立链接(当然,这需要判断cur应该为gparent的左或右子树),若parent就是_rootgparent就是nullptr,这步就变成了将cur赋值给_root

在这里插入图片描述

  • 调平后,将parentcur 的平衡因子全部修改为0即可:
void RotateR(Node* parent)
{
	Node* gparent = parent->_parent;
	Node* cur = parent->_left;
	Node* curright = cur->_right;

	parent->_left = curright;
	if (curright != nullptr)
	{
		curright->_parent = parent;
	}

	cur->_right = parent;
	parent->_parent = cur;

	cur->_parent = gparent;
	if (gparent == nullptr)
	{
		_root = cur;
	}
	else
	{
		if (cur->_kv.first < gparent->_kv.first)
		{
			gparent->_left = cur;
		}
		else
		{
			gparent->_right = cur;
		}
	}

	parent->_bf = cur->_bf = 0;
}

右左——右左双旋

第三种情况为:parent的右子树高度比左子树高,在其右子树中又有左子树较高。我们用gparentparentcurcurleft来记录这些结点:
在这里插入图片描述

在这种情况中,树的高度一定满足图示的关系:cur的左子树高度为h + 1cur右子树的高度为hparent左子树的高度也一定是h
cur左子树的高度大于h + 1,就应该是cur不平衡;
cur左子树的高度等于hparent就不会不平衡,平衡因子为0;
parent左子树的高度大于hparent就不会不平衡;
parent左子树的高度小于h,则在插入之前此树就已经不平衡了。

在这种情况下,parent的平衡因子为2,cur的平衡因子为 -1

单次旋转已经不足以将这种情况调整平衡了,需要进行两次旋转,即左右双旋
首先认为cur不平衡,cur进行一次右单旋。但是在进行右单旋时,curleft的左右子树高度不确定,可能有三种情况:

在这里插入图片描述
在对cur进行右单旋后的状态就类似于前面的右右,通过一次左单旋即可平衡;

然后再parent进行一次左单旋(具体单旋的步骤就不赘述了):
在这里插入图片描述

旋转调平后,我们再来总结规律
右左双旋最后的结果为:curleft做根结点,parent做左子结点,cur做右子节点。curleft的左子树做parent的右子树,curleft的右子树做cur的左子树

而上面提到的,curleft的左右子树高度包含三种情况(h,h)(h-1,h)(h,h-1)这三种情况也决定着右左旋转后parentcur的平衡因子的值curleft的平衡因子为0是一定的):分别对应(0,0)(-1,0)(0,1)。需要在旋转后进行判断赋值:

void RotateRL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;
	int bf = curleft->_bf;

	RotateR(cur);
	RotateL(parent);

	//根据curleft的平衡因子对parent与cur的平衡因子进行赋值
	if (bf == 0)
	{
		curleft->_bf = 0;
		cur->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		curleft->_bf = 0;
		cur->_bf = 1;
		parent->_bf = 0;
	}
	else if(bf == 1)
	{
		curleft->_bf = 0;
		cur->_bf = 0;
		parent->_bf = -1;
	}
	else
	{
		assert(0);
	}
}

左右——左右双旋

第四种情况为:parent的左子树高度比右子树高,在其左子树中又有右子树较高。我们用gparentparentcurcurright来记录这些结点:
在这里插入图片描述
与上面右左的情况相反,这种情况下一定满足上图关系:cur的右子树高度为h + 1cur左子树的高度为hparent右子树的高度也一定是h,这里就不赘述了。

在这种情况下,parent的平衡因子为 -2,cur的平衡因子为 1

与上面情况相反,需要进行左右双旋来调整平衡
首先认为cur不平衡,cur进行一次左单旋。当然,这里的左单旋也有三种情况:
在这里插入图片描述
然后再parent进行一次右单旋
在这里插入图片描述

左右双旋总结
与右左双旋相反,左右双旋最后的结果为:curright做根结点,parent做右子结点,cur做左子节点。curright的右子树做parent的左子树,curright的左子树做cur的右子树

同时,curright的左右子树高度包含三种情况(h,h)(h-1,h)(h,h-1)这三种情况也决定着左右旋转后curparent的平衡因子的值curright的平衡因子为0是一定的):分别对应(0,0)(-1,0)(0,1)。需要在旋转后进行判断赋值:

void RotateLR(Node* parent)
{
	Node* cur = parent->_left;
	Node* curright = cur->_right;
	int bf = curright->_bf;

	RotateL(cur);
	RotateR(parent);

	if (bf == 0)
	{
		curright->_bf = 0;
		cur->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		curright->_bf = 0;
		cur->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		curright->_bf = 0;
		cur->_bf = -1;
		parent->_bf = 0;
	}
	else
	{
		assert(0);
	}
}

在完全了解不平衡的条件与解决方案后,我们就可以补全上面insert的不平衡时的种类判断以及旋转调平衡的代码了(在前面insert部分以及后面的源码部分都有展示,这里就不再写了)。

源码

namespace qqq
{

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _parent;   
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	int _bf;

	AVLTreeNode(const pair<K, V> kv = pair<K, V>())
		: _kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

	template<class K, class V>
	class AVLTree
	{
		typedef AVLTreeNode<K, V> Node;
	public:

		bool insert(const pair<K, V>& kv)
		{
			//先插入(搜索插入)
			Node* newnode = new Node(kv);
			Node* parent = nullptr;
			Node* cur = _root;

			while (cur != nullptr) //搜索
			{
				if (newnode->_kv.first > cur->_kv.first)
				{
					parent = cur;
					cur = parent->_right;
				}
				else if (newnode->_kv.first < cur->_kv.first)
				{
					parent = cur;
					cur = parent->_left;
				}
				else  //相等即插入失败
				{
					return false;
				}
			}
			if (parent == nullptr) //插入
			{
				_root = newnode;
			}
			else if (newnode->_kv.first < parent->_kv.first)
			{
				parent->_left = newnode;
				newnode->_parent = parent;
				cur = newnode;
			}
			else
			{
				parent->_right = newnode;
				newnode->_parent = parent;
				cur = newnode;
			}

			//向上调整平衡因子
			while (parent != nullptr)
			{
				if (cur == parent->_left) //更新parent的平衡因子
				{
					--parent->_bf;
				}
				else
				{
					++parent->_bf;
				}

				if (parent->_bf == 0) //parent平衡因子为0,即向上调整结束
				{
					break;
				}
				else if (parent->_bf == 1 || parent->_bf == -1) //继续向上调整
				{
					cur = parent;
					parent = cur->_parent;
				}
				else if (parent->_bf == -2 || parent->_bf == 2) //旋转降低平衡因子
				{
					if (parent->_bf == 2 && cur->_bf == 1) //右右——单次左旋parent
					{
						RotateL(parent);
					}
					else if (parent->_bf == -2 && cur->_bf == -1) //左左——单次右旋parent
					{
						RotateR(parent);
					}
					else if (parent->_bf == 2 && cur->_bf == -1) //右左——先右旋cur再左旋parent
					{
						RotateRL(parent);
					}
					else if (parent->_bf == -2 && cur->_bf == 1)//左右——先左旋cur再右旋parent
					{
						RotateLR(parent);
					}
					else //兜底
					{
						assert(0);
					}
					break;
				}
				else
				{
					assert(0);
				}
			}
			return true;
		}

		void RotateL(Node* parent)
		{
			Node* gparent = parent->_parent;
			Node* cur = parent->_right;
			Node* curleft = cur->_left;

			parent->_right = curleft;
			if (curleft != nullptr)
			{
				curleft->_parent = parent;
			}

			cur->_left = parent;
			parent->_parent = cur;

			cur->_parent = gparent;
			if (gparent == nullptr)
			{
				_root = cur;
			}
			else
			{
				if (cur->_kv.first < gparent->_kv.first)
				{
					gparent->_left = cur;
				}
				else
				{
					gparent->_right = cur;
				}
			}

			parent->_bf = cur->_bf = 0;
		}
		void RotateR(Node* parent)
		{
			Node* gparent = parent->_parent;
			Node* cur = parent->_left;
			Node* curright = cur->_right;

			parent->_left = curright;
			if (curright != nullptr)
			{
				curright->_parent = parent;
			}

			cur->_right = parent;
			parent->_parent = cur;

			cur->_parent = gparent;
			if (gparent == nullptr)
			{
				_root = cur;
			}
			else
			{
				if (cur->_kv.first < gparent->_kv.first)
				{
					gparent->_left = cur;
				}
				else
				{
					gparent->_right = cur;
				}
			}

			parent->_bf = cur->_bf = 0;
		}
		void RotateRL(Node* parent)
		{
			Node* cur = parent->_right;
			Node* curleft = cur->_left;
			int bf = curleft->_bf;

			RotateR(cur);
			RotateL(parent);

			if (bf == 0)
			{
				curleft->_bf = 0;
				cur->_bf = 0;
				parent->_bf = 0;
			}
			else if (bf == -1)
			{
				curleft->_bf = 0;
				cur->_bf = 1;
				parent->_bf = 0;
			}
			else if(bf == 1)
			{
				curleft->_bf = 0;
				cur->_bf = 0;
				parent->_bf = -1;
			}
			else
			{
				assert(0);
			}
		}
		void RotateLR(Node* parent)
		{
			Node* cur = parent->_left;
			Node* curright = cur->_right;
			int bf = curright->_bf;

			RotateL(cur);
			RotateR(parent);

			if (bf == 0)
			{
				curright->_bf = 0;
				cur->_bf = 0;
				parent->_bf = 0;
			}
			else if (bf == -1)
			{
				curright->_bf = 0;
				cur->_bf = 0;
				parent->_bf = 1;
			}
			else if (bf == 1)
			{
				curright->_bf = 0;
				cur->_bf = -1;
				parent->_bf = 0;
			}
			else
			{
				assert(0);
			}
		}

		bool isAVLTree()
		{
			return isBalance(_root);
		}

	protected:
		Node* _root = nullptr;

		//这两个函数将用于测试AVL树是否平衡
		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;
		}
		bool isBalance(Node* root)
		{
			if (root == nullptr)
			{
				return true;
			}

			int leftheight = height(root->_left);
			int rightheight = height(root->_right);

			return abs(rightheight - leftheight) < 2 && isBalance(root->_left) && isBalance(root->_right);
		}
	};
}

测试

最后,我们来测试一下我们的AVL树是否合格。
这时我们需要两段测试代码:height用于测量树的高度、isBalance用于判断树是否平衡。这两段代码都是递归实现的,体现了分治的思想,在前面C语言部分有介绍:戳我看二叉树练习题哦(二叉树的最大深度)

测试的代码在上面的源码部分有展示,这里就只演示一下效果:

namespace qqq
{
	void testAVLfunc() //测试用例,使用10000000个随机数
	{
		const int N = 10000000;
		std::vector<int> v;
		v.reserve(N);
		srand(time(nullptr));

		for (size_t i = 0; i < N; i++)
		{
			v.push_back(rand()); //生成随机数
		}

		AVLTree<int, int> avl;
		for (auto e : v)
		{
			avl.insert(make_pair(e, e));
			cout << "Insert:" << e << "->" << avl.isAVLTree() << endl;
		}
		cout << avl.isAVLTree() << endl;
	}
}
int main()
{
	qqq::testAVLfunc();
	return 0;
}

这个代码需要跑很久,结果当然是没问题的:
在这里插入图片描述

总结

到此,关于AVL树的介绍就结束了
AVL树是高度平衡的,这就导致在插入的时候,会有大量的时间用在了调平衡上,有些得不偿失。在下一篇文章中将介绍另一种平衡二叉树——红黑树。它的平衡度没有那么高,带来了一些效率的提升

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

  • 34
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿qiu不熬夜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值