AVL树的实现及平衡旋转

简介:这篇文章虽说是AVL树的实现,但其重点还是放在插入结点时,其结点平衡旋转四种情况的判定及如何去实现四种平衡旋转,对于其它的接口,比如求二叉树结点的个数,二叉树结点的高度,中序遍历还会在初阶数据结构中进行补充。测试程序可以帮忙检测写的平衡旋转是否正确,至于AVL树的删除比插入更加的复杂,需要慢慢研究

在看这篇文章之前可以去看看下面两篇文章有助于帮助理解AVL树的实现:其中容器介绍了pair<K, V>类,二叉搜索树介绍了插入与查找的具体实现过程

容器map和set的使用
二叉搜索树(key/value) 的介绍与模拟实现

AVL的概念

  1. AVL树是一个平衡二叉搜索树,它的左右子树的是AVL树,且左右子树的高度差要么是0,1,-1,它是通过控制高度差去控制平衡的。
  2. AVL树这里引进了一个平衡因子的概念,每个结点都有一个平衡因子,它是右子树的高度减去左子树的高度,所以它的值只可能是0,-1,1,如果出现2的情况就会用到旋转去平衡高度差
  3. AVL树是不可能实现高度差为零的,比如存在两个结点无论如何都会形成两层
  4. AVL树的高度可以控制在logN层,那它的增删改的效率也能做到O(logN),而二叉搜索树它可能存在左边或右边特别高的情况,那它的效率就会达到O(N)

AVL树的结点结构

// AVL树结点的结构
template<class K, class V>
struct AVLTreeNode
{
	// 默认构造
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}

	// 成员变量
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf; // 平衡因子
};

AVL树的大致框架

// AVL树大大致框架
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	// 插入,查找.....

private:
	Node* _root = nullptr;
};

AVL树的查找

Node* find(const K& key)
{
	Node* pcur = _root;
	while (pcur)
	{
		if (pcur->_kv.first > key)
			pcur = pcur->_left;
		else if (pcur->_kv.first < key)
			pcur = pcur->_right;
		else
			return pcur;
	}
	return nullptr;
}

插入

插入的大致框架

插入的过程跟二叉搜索树中的插入是相似的,只不过AVL树的插入包括旋转平衡,另外就是这个_parent指针的更新

bool insert(const pair<K, V>& kv)
{
	// 插入,要先找到插入结点的位置
	// 先判读根是否为空
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	Node* pcur = _root;
	Node* parent = nullptr;
	while (pcur)
	{
		if (pcur->_kv.first > kv.first)
		{
			parent = pcur;
			pcur = pcur->_left;
		}
		else if (pcur->_kv.first < kv.first)
		{
			parent = pcur;
			pcur = pcur->_right;
		}
		else
		{
			return false;
		}
	}

	// 走到这里,说明pcur为空,但插到左结点还是右结点不清楚
	pcur = new Node(kv);

	if (parent->_kv.first > kv.first)
	{
		parent->_left = pcur;
	}
	else
	{
		parent->_right = pcur;
	}
	// 记得将pcur的_parent指向parent;
	pcur->_parent = parent;
	// 更新平衡因子...

	return true;
}

平衡因子更新原则

1.结点的平衡因子 = 右子树的高度 - 左子树的高度
2. 只要子树的高度变化,一定会影响到当前结点的平衡因子
3. 如果新增结点在parent的左子树,则parent的pf++,若在右子树则parent的pf- -
4. 其次下面的三种情况说明了,当前结点所在子树的高度是否变化决定了是否会继续向上更新

更新平衡因子

插入结点后,其parent结点的平衡因子变化可能会影响其祖先结点的平衡因子,祖先结点的平衡因子可能也会变化,因此得在外层去套一个while(parent),最差的情况可能会更新到根结点,其次根据平衡因子更新原则,如果新增结点在parent的左子树,pf--,新增结点在parent的右子树,pf++,但是要注意其parent祖先结点的平衡因子并不也是pf++,pf- -如此简单,得按照下面三种情况分别讨论

if (parent->_left == pcur)
	parent->pf--;
else
	parent->pf++;

parent的 _bf = 1 或 _bf= -1 → _bf = 0

在这里插入图片描述

如果parent结点的平衡因子变为零,它是一定不会影响到其祖先结点的平衡因子的,根据平衡因子更新原则,平衡因子 = 右子树的高度 - 左子树的高度,更新之前其祖先结点的平衡因子可能为 1,-1,0,现在其中一课子树的根结点平衡因子变化了,但其高度不变,所以无需向上更新祖先结点的平衡因子

// parent的平衡因子变化成0
if (parent->_bf == 0)
{
	break;
}

parent的 _bf = 0 → _bf = 1 或 _bf = -1

在这里插入图片描述

parent祖先结点的平衡因子原本为0,1,-1。就是左子树的高度 - 右子树的高度,现在有棵子树的高度增加了1,那有没有可能其parent祖先结点的平衡因子变为2, -2的可能,因此得向上更新平衡因子,看看其平衡因子到底是多少。注意:只要子树的高度变化,其根结点的平衡因子是一定会变化的

// parent的平衡因子变化成1或-1
else if (parent->_bf == 1 || parent->_bf == -1)
{
	pcur = parent;
	parent = parent->_parent;
}

parent的 _bf = 1 或 _bf = -1 → _bf = 2 或 _bf = -2

到了这种情况了,说啥也没有用了,直接旋转, 平衡高度

// parent的平衡因子变化成2或-2
else if (parent->_bf == 2 || parent->_bf == -2)
{
	// 旋转操作
	break;
}

整体代码(不包含旋转)

// 更新平衡因子...
while (parent)
{
	if (parent->_left == pcur)
		parent->_bf--;
	else
		parent->_bf++;

	// parent的平衡因子变化成0
	if (parent->_bf == 0)
	{
		break;
	}

	// parent的平衡因子变化成1或-1
	else if (parent->_bf == 1 || parent->_bf == -1)
	{
		pcur = parent;
		parent = parent->_parent;
	}

	// parent的平衡因子变化成2或-2
	else if (parent->_bf == 2 || parent->_bf == -2)
	{
		// 旋转操作
		break;
	}
	else
	{
		assert(false);
	}

旋转

在这里我会把下面四种情况以图形 + 文字 + 代码的形式去阐述,但是要注意的一点,左单旋与右单旋,左右双旋与右左双旋,它们的细节与过程是一致的,因此其实弄清楚右单旋与左右双旋就能写出左单旋与右左双旋

在这里插入图片描述

  1. 上面的图片看不懂也没啥事,我想阐述的是,当高度 h = 2时,这棵树引发结点10旋转(单旋)的情况有36种,当高度 h = 3时,情况就变成了5400种了,当然新增结点得在a子树中,在b子树中就是双旋了
  2. 那么这么多种情况,总不可能一种一种去分析,因此直接把子树a,b,c抽象成高度为h的长方形,后续分析四种旋转,把长方形当成树作为一个整体去移动,树肯定是平衡的,树要是不平衡的话就轮不到结点10先旋转了,肯定在树内部先旋转,那上面的结点10旋转何尝不是一颗子树的根结点旋转呢?
  3. 对不平衡子树旋转,旋转后降低了子树的高度,是不会再影响上一层的,因为没插入之前就是平衡的,插入之后虽说不平衡,但旋转不弥补了升高的高度吗。因此旋转后,插入结束

右单旋

在这里插入图片描述
在这里插入图片描述

  1. 要注意一点:无论再怎么旋转都必须符合二叉树的搜索性质,即左子树元素的key永远大于根结点key,右子树元素的key永远小于根结点key
  2. 要把握好parent,subL,subLR的位置
  3. 要更改这些结点指针的指向:parent->_left,parent->_parent,parent的父结点(_left 或 _right),subLR->_parent,subL->_right,subL->_parent,同时你得去考虑parent是否为_rootsubLR是否为空在改变parent->_parent指针之前得保存它的父结点若不为_root,subL在左结点,还是右结点
  4. 在旋转后,千万不要忘记更新parent与subL的平衡因子
// 1.右单旋,纯粹左边高,旋到右边
if (parent->_bf == -2 && pcur->_bf == -1)
	RotateR(parent);
void RotateR(Node* parent)
{
	// 先记录下三个结点
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;
	
	subL->_right = parent;

	// 再将parent的_parent指向subL之前,先保存下来
	Node* ppNode = parent->_parent;
	parent->_parent = subL;

	// parent的可能是根结点,得提前判断一下
	if (parent == _root)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		// 看subL是ppNode的左结点还是右结点
		if (ppNode->_left == parent)
			ppNode->_left = subL;
		else
			ppNode->_right = subL;
		
		// 记得改变subL的_parent指向
		subL->_parent = ppNode;
	}
	// 也不要忘记将parent与subL的平衡因子置为零
	parent->_bf = subL->_bf = 0;
}

左单旋

在这里插入图片描述
在这里插入图片描述
左单旋与右单旋的思路是一致的,只不过右边高了,得旋一部分到左边,细节与过程都是一样的

// 2.左单旋,纯粹右边高,旋到左边
else if (parent->_bf == 2 && pcur->_bf == 1)
	RotateL(parent);
void RotateL(Node* parent)
{
	// 先记录三个结点,parent也算是一个吧
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	subR->_left = parent;

	// 先将parent的_parent给保存下来
	Node* ppNode = parent->_parent;
	parent->_parent = subR;

	// 判断是不是根结点
	if (parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		//判断是ppNode的左结点还是右结点
		if (ppNode->_left == parent)
			ppNode->_left = subR;
		else
			ppNode->_right = subR;
		subR->_parent = ppNode;
	}
	// 别忘记更新subR与parent的平衡因子
	subR->_bf = parent->_bf = 0;
}

左右双旋

在这里插入图片描述
在这里插入图片描述

  1. 左右双旋其实就是旋转两次,左单旋 + 右单旋,只不过旋转的结点不同,注意:之前插入结点要么在最左边子树,要么在最右边子树,而左右双旋插入的是b子树(可以说是中间子树)
  2. 在b子树插入结点可能插入到根结点的左子树或又子树导致根结点的平衡因子- - 或 ++,将subL,parent,subLR左右双旋后其平衡因子分别有两种情况(注意:过程是不变的,左单旋 + 右单旋,也是同一结点旋转
  3. 要特别注意树的高度h = 0 的情况,旋转后它们的平衡因子又与上面两种情况不同
  4. 要分清楚上面三种情况来更新平衡因子,就得在旋转之前保存b子树根结点的平衡因子
// 3.左右双旋,旋转两次,左单旋 + 右单旋
else if (parent->_bf == -2 && pcur->_bf == 1)
	RotateLR(parent);
void RotateLR(Node* parent)
{
	// 仍然先记录下三个结点
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	// 将插入结点后,subLR的_bf保存下来
	int bf = subLR->_bf;

	RotateL(parent->_left);
	RotateR(parent);

	// 更新平衡因子--是看subLR的原_bf
	if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

右左双旋

在这里插入图片描述
右左双旋的过程与细节也是跟左右双旋一致

// 4.右左双旋,旋转两次,右单旋 + 左单旋
else if (parent->_bf == 2 && pcur->_bf == -1)
	RotateRL(parent);
	void RotateRL(Node* parent)
	{
		// 记录结点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		// 保存原subRL的平衡因子
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		// 更新平衡因子
		if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

旋转情况判定汇总

// parent的平衡因子变化成2或-2
else if (parent->_bf == 2 || parent->_bf == -2)
{
	// 旋转操作

	// 1.右单旋,纯粹左边高,旋到右边
	if (parent->_bf == -2 && pcur->_bf == -1)
		RotateR(parent);
	// 2.左单旋,纯粹右边高,旋到左边
	else if (parent->_bf == 2 && pcur->_bf == 1)
		RotateL(parent);
	// 3.左右双旋,旋转两次,左单旋 + 右单旋
	else if (parent->_bf == -2 && pcur->_bf == 1)
		RotateLR(parent);
	// 4.右左双旋,旋转两次,右单旋 + 左单旋
	else if (parent->_bf == 2 && pcur->_bf == -1)
		RotateRL(parent);
	else
	{
		assert(false);
	}
	break;
}

AVL.h

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

// AVL树结点的结构
template<class K, class V>
struct AVLTreeNode
{
	// 默认构造
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}

	// 成员变量
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf; // 平衡因子
};

// AVL树大大致框架
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	// 插入,查找.....
	bool insert(const pair<K, V>& kv)
	{
		// 插入,要先找到插入结点的位置
		// 先判读根是否为空
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* pcur = _root;
		Node* parent = nullptr;
		while (pcur)
		{
			if (pcur->_kv.first > kv.first)
			{
				parent = pcur;
				pcur = pcur->_left;
			}
			else if (pcur->_kv.first < kv.first)
			{
				parent = pcur;
				pcur = pcur->_right;
			}
			else
			{
				return false;
			}
		}

		// 走到这里,说明pcur为空,但插到左结点还是右结点不清楚
		pcur = new Node(kv);

		if (parent->_kv.first > kv.first)
		{
			parent->_left = pcur;
		}
		else
		{
			parent->_right = pcur;
		}
		// 记得将pcur的_parent指向parent;
		pcur->_parent = parent;
		// 更新平衡因子...
		while (parent)
		{
			if (parent->_left == pcur)
				parent->_bf--;
			else
				parent->_bf++;

			// parent的平衡因子变化成0
			if (parent->_bf == 0)
			{
				break;
			}

			// parent的平衡因子变化成1或-1
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				pcur = parent;
				parent = parent->_parent;
			}

			// parent的平衡因子变化成2或-2
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 旋转操作

				// 1.右单旋,纯粹左边高,旋到右边
				if (parent->_bf == -2 && pcur->_bf == -1)
					RotateR(parent);
				// 2.左单旋,纯粹右边高,旋到左边
				else if (parent->_bf == 2 && pcur->_bf == 1)
					RotateL(parent);
				// 3.左右双旋,旋转两次,左单旋 + 右单旋
				else if (parent->_bf == -2 && pcur->_bf == 1)
					RotateLR(parent);
				// 4.右左双旋,旋转两次,右单旋 + 左单旋
				else if (parent->_bf == 2 && pcur->_bf == -1)
					RotateRL(parent);
				else
				{
					assert(false);
				}
				break;
			}
			else
			{
				break;
			}
		}

		return true;
	}
	void RotateR(Node* parent)
	{
		// 先记录下三个结点
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		
		subL->_right = parent;

		// 再将parent的_parent指向subL之前,先保存下来
		Node* ppNode = parent->_parent;
		parent->_parent = subL;

		// parent的可能是根结点,得提前判断一下
		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			// 看subL是ppNode的左结点还是右结点
			if (ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;
			
			// 记得改变subL的_parent指向
			subL->_parent = ppNode;
		}
		// 也不要忘记将parent与subL的平衡因子置为零
		parent->_bf = subL->_bf = 0;
	}
	void RotateL(Node* parent)
	{
		// 先记录三个结点,parent也算是一个吧
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;

		// 先将parent的_parent给保存下来
		Node* ppNode = parent->_parent;
		parent->_parent = subR;

		// 判断是不是根结点
		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			//判断是ppNode的左结点还是右结点
			if (ppNode->_left == parent)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;
			subR->_parent = ppNode;
		}
		// 别忘记更新subR与parent的平衡因子
		subR->_bf = parent->_bf = 0;
	}

	void RotateLR(Node* parent)
	{
		// 仍然先记录下三个结点
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		// 将插入结点后,subLR的_bf保存下来
		int bf = subLR->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		// 更新平衡因子--是看subLR的原_bf
		if (bf == -1)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void RotateRL(Node* parent)
	{
		// 记录结点
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		// 保存原subRL的平衡因子
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		// 更新平衡因子
		if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	Node* find(const K& key)
	{
		Node* pcur = _root;
		while (pcur)
		{
			if (pcur->_kv.first > key)
				pcur = pcur->_left;
			else if (pcur->_kv.first < key)
				pcur = pcur->_right;
			else
				return pcur;
		}
		return nullptr;
	}

	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	int Height()
	{
		return _Height(_root);
	}

	size_t Size()
	{
		return _Size(_root);
	}
private:

	size_t _Size(Node* root)
	{
		if (root == nullptr)
			return 0;
		return 1 + _Size(root->_left) + _Size(root->_right);
	}

	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 _IsBalanceTree(Node* root)
	{
		// 空树也是AVL树
		if (nullptr == root)
			return true;

		// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int diff = rightHeight - leftHeight;

		// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
		// pRoot平衡因子的绝对值超过1,则一定不是AVL树
		if (abs(diff) >= 2)
		{
			cout << root->_kv.first << "高度差异常" << endl;
			return false;
		}

		if (root->_bf != diff)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		// pRoot的左和右如果都是AVL树,则该树一定是AVL树
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_kv.first << ' ';
		_InOrder(root->_right);
	}
	Node* _root = nullptr;
};

测试程序

#include"AVL.h"
#include<vector>
// 测试代码
void TestAVLTree1()
{
	AVLTree<int, int> t;
	// 常规的测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		/*if (e == 14)
		{
			int x = 0;
		}*/

		t.insert({ e, e });
		cout << "Insert:" << e << "->";
		cout << t.IsBalanceTree() << endl;
	}

	t.InOrder();
	cout << t.IsBalanceTree() << endl;
}

// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestAVLTree2()
{
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}

	size_t begin2 = clock();
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.insert(make_pair(e, e));
	}
	size_t end2 = clock();

	cout << t.IsBalanceTree() << endl;

	cout << "Insert:" << end2 - begin2 << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;

	size_t begin1 = clock();
	// 确定在的值
	/*for (auto e : v)
	{
		t.Find(e);
	}*/
	// 随机值
	for (size_t i = 0; i < N; i++)
	{
		t.find((rand() + i));
	}

	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}
int main()
{
    // 特殊的带有双旋场景的测试⽤例
    // TestAVLTree1();
	TestAVLTree2();
	return 0;
}

总结

写AVL树的实现时,对于四种旋转一定一定要去画图分析,不然它的三个结点指向加上它的平衡因子更新,加上四种旋转情况,我觉得能把人给绕得云里雾里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值