高级数据结构 | 创建二叉树 —递归与非递归实现:先序中序创建、中序后序创建 ...

对于使用二叉链表结构存储的二叉树,我们通常使用“递归—先序”的方式创建,并且在输入树中结点的数据时需要人为的加入‘#’以表示叶子节点(度为0的点)。

而我们知道,给定一个二叉树的中序遍历序列和其他任意一种优先深度遍历方式的序列都可以得到其完整的二叉树结构,那么我们能不能使用计算机来完成这一转化呢?

换言之,我们本章重点为:

  1. 使用先序和中序创建二叉树
  2. 使用中序和后序创建二叉树


一、使用递归创建二叉树
先序中序创建二叉树

通过先序的第一个元素可知整棵树的根结点。而对于中序遍历来说,根结点在其序列中间位置。

  • 因此我们可以通过一个根节点将中序划分为两个部分,根节点之前,跟结点之后。
    例如:对于序列 ABDECF(先)、DBEACF(中)来说
    先:ABDECF 得到根结点 ⇒ A
    中:DBEACF
    对于结点A来说,这是一个树的根,也是一个树分叉的位置。
  • 如果我们利用分治的思想,把左边和右边分别再次使用相同的方法递归下去。那么最终就可以完整复原树的每一个结点和上面的分叉。

实现如下:

/* 找“根”结点的下标 */
int FindIndex(const ElemType* istr, int n, ElemType x)
{
	int pos = -1;
	for (int i = 0; i < n; ++i)
	{
		if (istr[i] == x)
		{
			pos = i;
			break;
		}
	}
	return pos;
}
struct BtNode* CreatePI(const char* pstr, const char* istr,int n)
{
	struct BtNode* s = NULL;
	if (n > 0)
	{
		s = Buynode();
		s->data = *pstr;
		int pos = FindIndex(istr,n,*pstr);	// 找当前规模的“根”的下标
		if (pos == -1) exit(0);
		s->leftchild = CreatePI(pstr + 1, istr, pos);//存储左边部分
		s->rightchild = CreatePI(pstr + pos + 1, istr + pos + 1, n - pos - 1);//存储右边部分
	}
	return s;
}
struct BtNode* CreateTreePI(const char *pstr,const char *istr,int n)
{
	if (NULL == pstr || NULL == istr || n < 1) return NULL;
	return CreatePI(pstr, istr, n);
}
中序后序创建二叉树

先序和后序是两种相反的遍历方式,先序:根左右,后序:左右根。

有趣的是,我们可以通过同样的算法完成中序、后序创建二叉树的过程,只需要在把后序颠倒过来。

那么,按照后序遍历的最后一个元素我们可以得到根结点,按照根结点可以把中序划分为两段。同样的我们使用分治的思想可以很方便的完成对二叉树的创建。

实现如下:

struct BtNode* CreateIL(const char* istr, const char* lstr, int n)
{
	struct BtNode* s = NULL;
	if (n > 0)
	{
		s = Buynode();
		s->data = lstr[n - 1];
		int pos = FindIndex(istr, n, lstr[n - 1]);	// 找叶子结点下标
		if (pos == -1) exit(0);
		s->leftchild = CreateIL(istr , lstr, pos);
		s->rightchild = CreateIL(istr + pos + 1, lstr + pos, n - pos - 1);
	}
	return s;
}
struct BtNode* CreateTreeIL(const char* istr, const char* lstr, int n)
{
	if (NULL == istr || NULL == lstr || n < 1) return NULL;
	return CreateIL(istr, lstr, n);
}
二、非递归创建二叉树

使用递归的好处在于递归可以自动回退回上一层的状态,而我们一般把递归改非递归的思路都是引入一种新的数据结构模拟这种回退的功能。

  • 先序遍历其实每一步都找的是根节点,只是第一个元素是整棵树的根节点,第二个元素是左子树的根节点,第三个元素是左子树的左子树的根节点。

  • 中序遍历可以看做是每一步都在遍历叶子结点,第一个元素是最左边子树的叶子结点,第二个元素是上一层子树的叶子结点… …

先序中序遍历思路:
因此,我们可以使用先序遍历,每次沿着“根”的左子树向下遍历,直到遇到叶子结点,也就是第一个中序遍历的元素。然后我们回退至上一层,回退至与中序的第二个元素结点相同处,找到一个有分叉的“根”节点,再继续以此根的右子树向下遍历,如此循环下去最终遍历完整个树。

在此遍历的过程中,我们已经完成了对二叉树的建立。在此过程中通过保存每次遍历的结点的值与中序序列进行比较,我们可以准确的回退至上一层的根结点处,继续沿着根的另一边子树遍历。

中序后序遍历思路:
后序遍历倒置过来后,首位元素即是我们的根节点元素,第二位元素即是此“根”下右子树的最右边的叶子结点。先序在首次遍历时从左子树最左边的叶子结点处开始,刚好与后序相反。

综上,相对于先序中序的遍历,我们只需做一些小小的改动即可。从后续序列末端开始遍历,与中序序列的末端进行比较,建立的二叉树的顺序是先右子树,再左子树。

先序中序创建二叉树

非递归的先序中序创建二叉树,实现如下:

#include <stack>		// 使用C++库中提供的数据结构——栈
/* 非递归 --先序中序 */
struct BtNode* NiceCreatePI(const char* pstr, const char* istr, int n)
{
	std::stack<struct BtNode*> st;
	bool flag = false;
	int i = 0, j = 0;		// i 指向先序,j 指向中序
	struct BtNode* root = Buynode();
	root->data = pstr[0];
	st.push(root);
	for (i = 1; i < n; ++i)
	{
		struct BtNode* s = Buynode();
		s->data = pstr[i];

		struct BtNode* tmp = NULL;
		while (j < n - 1 && st.top()->data == istr[j])	// 判断是否遇到分叉(“根”)
		{
			while (!st.empty() && st.top()->data == istr[j])	// 向上找有分叉的“根”
			{
				tmp = st.top();		// 该结点(“根”)有右子树
				st.pop();
				++j;
			}
			if (tmp != NULL)
			{
				tmp->rightchild = s;	// 把该结点添加至右子树
				if (i == n - 1) { flag = true; break; }
				st.push(s);				// 入栈	
				s = Buynode();
				s->data = pstr[++i];
			}
		}

		if (flag)	break;	// 创建完成,退出
		st.top()->leftchild = s;
		st.push(s);		// 入栈


	}
	return root;
}
struct BtNode* NiceCreateTreePI(const char* pstr, const char* istr, int n)
{
	if (NULL == pstr || NULL == istr || n < 1) return NULL;
	return NiceCreatePI(pstr, istr, n);
}
中序后序创建二叉树

非递归的中序后序创建二叉树,实现如下:

#include <stack>		// 使用C++库中提供的数据结构——栈
/* 非递归 --中序后序 */
struct BtNode* NiceCreateIL(const char* istr, const char* lstr, int n)
{
	std::stack<struct BtNode*> st;
	bool flag = false;
	int i = n, j = n;		// i 指向后序,j 指向中序
	struct BtNode* root = Buynode();
	root->data = lstr[n];
	st.push(root);
	for (i = n - 1; i >= 0; --i)
	{
		struct BtNode* s = Buynode();
		s->data = lstr[i];

		struct BtNode* tmp = NULL;
		while (j > 0 && st.top()->data == istr[j])	// 判断是否遇到分叉(“根”)
		{
			while (!st.empty() && st.top()->data == istr[j])	// 向上找有分叉的“根”
			{
				tmp = st.top();		// 该结点(“根”)有左子树
				st.pop();
				--j;
			}
			if (tmp != NULL)
			{
				tmp->leftchild = s;	// 把该结点添加至左子树
				if (i == 0) { flag = true; break; }
				st.push(s);				// 入栈
				s = Buynode();
				s->data = lstr[--i];

			}
		}

		if (flag) break;
		st.top()->rightchild = s;
		st.push(s);		// 入栈

	}
	return root;
}

struct BtNode* NiceCreateTreeIL(const char* istr, const char* lstr, int n)
{
	if (NULL == istr || NULL == lstr || n < 1) return NULL;
	return NiceCreateIL(istr, lstr, n - 1);
}
三、测试用例

输出形如 ABC##DE##F##G#H# 格式的先序序列字符串。

void strPreOrder(struct BtNode* p)
{
	if (NULL != p)
	{
		printf("%c", p->data);
		strPreOrder(p->leftchild);
		strPreOrder(p->rightchild);
	}
	else printf("#");
}

这里主要测试五种类型的二叉树,分别是普通的二叉树、只有左子树的二叉树、只有右子树的二叉树,根节点没有右子树的二叉树、根节点左子树的二叉树。


	//const char pstr[] = "ABCDEFGH"; //先序序列   		//   	     A
	//const char istr[] = "CBEDFAGH"; //中序序列   		//        B    G
	//const char lstr[] = "CEFDBHGA"; //后序序列   		//     C   D     H
														//        E  F 



		//左单 链表
	//const char pstr[] = "ABCDEFGH"; //先序序列   		//          A
	//const char istr[] = "HGFEDCBA"; //中序序列   		//         B
	//const char lstr[] = "HGFEDCBA"; //后序序列  		//        C
														//       D
														//      E
														//     F
														//    G
														//   H



		//右单 链表
	//const char pstr[] = "ABCDEFGH"; //先序序列   	   //     A
	//const char istr[] = "ABCDEFGH"; //中序序列   	   //      B
	//const char lstr[] = "HGFEDCBA"; //后序序列   	   //      	C
													   //      	 D
													   //      	  E
													   //      	   F
													   //      	    G
													   //      	     H



	//const char pstr[] = "ABCDEFGH"; //先序序列   	    //             A
	//const char istr[] = "DCFEGHBA"; //中序序列   	    //           B
	//const char lstr[] = "DFHGECBA"; //后序序列  	    //         C
													    //      D     E
													    //	        F    G
													    //		           H





	//const char pstr[] = "ABCDFE"; //先序序列			 //			A
	//const char istr[] = "ABFDCE"; //中序序列			 //			 B
	//const char lstr[] = "FDECBA"; //后序序列			 //			  C
														 //			D   E
														 //		 F 
	

	// {先序,中序,后序}
	const char*str[][20] = { 
	{"ABCDEFGH","CBEDFAGH","CEFDBHGA"},			// 普通二叉树
	{"ABCDEFGH","HGFEDCBA","HGFEDCBA"},			// 只有左子树,单链表结构
	{"ABCDEFGH","ABCDEFGH","HGFEDCBA"},			// 只有右子树,单链表结构
	{"ABCDEFGH","DCFEGHBA","DFHGECBA"},			// 根节点没有右子树
	{"ABCDFE","ABFDCE","FDECBA"}				// 根节点没有左子树
	};



全部代码:

#include <iostream>
#include <vector>


using namespace std;

typedef char ElemType;
typedef struct BtNode
{
	struct BtNode* leftchild;
	struct BtNode* rightchild;
	ElemType data;
}BtNode, * BinaryTree;

/* 购买结点,便于后期维护有关结点的申请的问题 */
struct BtNode* Buynode()
{
	struct BtNode* s = (struct BtNode*)malloc(sizeof(struct BtNode));
	if (NULL == s)	exit(1);
	memset(s, 0, sizeof(struct BtNode));
	return s;
}

void Freenode(struct BtNode* p)
{
	free(p);
}

/* 先序遍历 */
void PreOrder(struct BtNode* p)
{
	if (NULL != p)
	{
		printf("%c ", p->data);
		PreOrder(p->leftchild);
		PreOrder(p->rightchild);
	}
}


/* 中序遍历 */
void InOrder(struct BtNode* p)
{
	if (NULL != p)
	{
		InOrder(p->leftchild);
		printf("%c ", p->data);
		InOrder(p->rightchild);
	}
}


/* 后序遍历 */
void PastOrder(struct BtNode* p)
{
	if (NULL != p)
	{
		PastOrder(p->leftchild);
		PastOrder(p->rightchild);
		printf("%c ", p->data);
	}
}




#include <stack>		// 使用C++库中提供的数据结构——栈
/* 非递归 --先序中序 */
struct BtNode* NiceCreatePI(const char* pstr, const char* istr, int n)
{
	std::stack<struct BtNode*> st;
	bool flag = false;
	int i = 0, j = 0;		// i 指向先序,j 指向中序
	struct BtNode* root = Buynode();
	root->data = pstr[0];
	st.push(root);
	for (i = 1; i < n; ++i)
	{
		struct BtNode* s = Buynode();
		s->data = pstr[i];

		struct BtNode* tmp = NULL;
		while (j < n - 1 && st.top()->data == istr[j])	// 判断是否遇到分叉(“根”)
		{
			while (!st.empty() && st.top()->data == istr[j])	// 向上找有分叉的“根”
			{
				tmp = st.top();		// 该结点(“根”)有右子树
				st.pop();
				++j;
			}
			if (tmp != NULL)
			{
				tmp->rightchild = s;	// 把该结点添加至右子树
				if (i == n - 1) { flag = true; break; }
				st.push(s);				// 入栈	
				s = Buynode();
				s->data = pstr[++i];
			}
		}

		if (flag)	break;	// 创建完成,退出
		st.top()->leftchild = s;
		st.push(s);		// 入栈


	}
	return root;
}
struct BtNode* NiceCreateTreePI(const char* pstr, const char* istr, int n)
{
	if (NULL == pstr || NULL == istr || n < 1) return NULL;
	return NiceCreatePI(pstr, istr, n);
}


#include <stack>		// 使用C++库中提供的数据结构——栈
/* 非递归 --中序后序 */
struct BtNode* NiceCreateIL(const char* istr, const char* lstr, int n)
{
	std::stack<struct BtNode*> st;
	bool flag = false;
	int i = n, j = n;		// i 指向后序,j 指向中序
	struct BtNode* root = Buynode();
	root->data = lstr[n];
	st.push(root);
	for (i = n - 1; i >= 0; --i)
	{
		struct BtNode* s = Buynode();
		s->data = lstr[i];

		struct BtNode* tmp = NULL;
		while (j > 0 && st.top()->data == istr[j])	// 判断是否遇到分叉(“根”)
		{
			while (!st.empty() && st.top()->data == istr[j])	// 向上找有分叉的“根”
			{
				tmp = st.top();		// 该结点(“根”)有左子树
				st.pop();
				--j;
			}
			if (tmp != NULL)
			{
				tmp->leftchild = s;	// 把该结点添加至左子树
				if (i == 0) { flag = true; break; }
				st.push(s);				// 入栈
				s = Buynode();
				s->data = lstr[--i];

			}
		}

		if (flag) break;
		st.top()->rightchild = s;
		st.push(s);		// 入栈

	}
	return root;
}

struct BtNode* NiceCreateTreeIL(const char* istr, const char* lstr, int n)
{
	if (NULL == istr || NULL == lstr || n < 1) return NULL;
	return NiceCreateIL(istr, lstr, n - 1);
}


void strPreOrder(struct BtNode* p)
{
	if (NULL != p)
	{
		printf("%c", p->data);
		strPreOrder(p->leftchild);
		strPreOrder(p->rightchild);
	}
	else printf("#");
}

int main()
{
	// {先序,中序,后序}
	const char* str[][20] = {
	{"ABCDEFGH","CBEDFAGH","CEFDBHGA"},			// 普通二叉树
	{"ABCDEFGH","HGFEDCBA","HGFEDCBA"},			// 只有左子树,单链表结构
	{"ABCDEFGH","ABCDEFGH","HGFEDCBA"},			// 只有右子树,单链表结构
	{"ABCDEFGH","DCFEGHBA","DFHGECBA"},			// 根节点没有右子树
	{"ABCDFE","ABFDCE","FDECBA"}				// 根节点没有左子树
	};

	BinaryTree rootPI = NULL;		// 递归先序、中序
	BinaryTree rootIL = NULL;		// 递归中序、后序
	BinaryTree rootPI_r = NULL;		// 非递归先序、中序
	BinaryTree rootIL_r = NULL;		// 非递归中序、后序
	int len = sizeof(str) / sizeof(str[0]);
	while (len--)
	{
		int n = strlen(*str[len]);
		printf("●--第%d组测试用例:\n", len + 1);

		const char* pstr = str[len][0];
		const char* istr = str[len][1];
		const char* lstr = str[len][2];

		printf("使用‘#’表示:\n");
		rootPI = NiceCreateTreePI(pstr, istr, n); // 使用先序和中序创建二叉树 
		strPreOrder(rootPI);  printf("\n");
		rootIL = NiceCreateTreeIL(istr, lstr, n); // 使用中序和后序创建二叉树 
		strPreOrder(rootIL);  printf("\n");
		rootPI_r = NiceCreateTreePI(pstr, istr, n);
		strPreOrder(rootPI_r); printf("\n");
		rootIL_r = NiceCreateTreeIL(istr, lstr, n);
		strPreOrder(rootIL_r); printf("\n\n");

		printf("前序遍历:\t", ""); printf("%*s中序遍历:\t", n, ""); printf("%*s后序遍历:\n", n, "");
		PreOrder(rootPI); printf("\t");		 InOrder(rootPI); printf("\t");		  PastOrder(rootPI); printf("\n");
		PreOrder(rootIL); printf("\t");		 InOrder(rootIL); printf("\t");		  PastOrder(rootIL); printf("\n");
		PreOrder(rootPI_r); printf("\t");	 InOrder(rootPI_r); printf("\t");	  PastOrder(rootPI_r); printf("\n");
		PreOrder(rootIL_r); printf("\t");	 InOrder(rootIL_r); printf("\t");	  PastOrder(rootIL_r); printf("\n");


		puts("");
	}


	return 0;
}

/*		输出:
●--第5组测试用例:
使用‘#’表示:
A#B#CDF###E##
A#B#CDF###E##
A#B#CDF###E##
A#B#CDF###E##

前序遍历:            中序遍历:              后序遍历:
A B C D F E     A B F D C E     F D E C B A
A B C D F E     A B F D C E     F D E C B A
A B C D F E     A B F D C E     F D E C B A
A B C D F E     A B F D C E     F D E C B A

●--第4组测试用例:
使用‘#’表示:
ABCD##EF##G#H####
ABCD##EF##G#H####
ABCD##EF##G#H####
ABCD##EF##G#H####

前序遍历:              中序遍历:              后序遍历:
A B C D E F G H         D C F E G H B A         D F H G E C B A
A B C D E F G H         D C F E G H B A         D F H G E C B A
A B C D E F G H         D C F E G H B A         D F H G E C B A
A B C D E F G H         D C F E G H B A         D F H G E C B A

●--第3组测试用例:
使用‘#’表示:
A#B#C#D#E#F#G#H##
A#B#C#D#E#F#G#H##
A#B#C#D#E#F#G#H##
A#B#C#D#E#F#G#H##

前序遍历:              中序遍历:              后序遍历:
A B C D E F G H         A B C D E F G H         H G F E D C B A
A B C D E F G H         A B C D E F G H         H G F E D C B A
A B C D E F G H         A B C D E F G H         H G F E D C B A
A B C D E F G H         A B C D E F G H         H G F E D C B A

●--第2组测试用例:
使用‘#’表示:
ABCDEFGH#########
ABCDEFGH#########
ABCDEFGH#########
ABCDEFGH#########

前序遍历:              中序遍历:              后序遍历:
A B C D E F G H         H G F E D C B A         H G F E D C B A
A B C D E F G H         H G F E D C B A         H G F E D C B A
A B C D E F G H         H G F E D C B A         H G F E D C B A
A B C D E F G H         H G F E D C B A         H G F E D C B A

●--第1组测试用例:
使用‘#’表示:
ABC##DE##F##G#H##
ABC##DE##F##G#H##
ABC##DE##F##G#H##
ABC##DE##F##G#H##

前序遍历:              中序遍历:              后序遍历:
A B C D E F G H         C B E D F A G H         C E F D B H G A
A B C D E F G H         C B E D F A G H         C E F D B H G A
A B C D E F G H         C B E D F A G H         C E F D B H G A
A B C D E F G H         C B E D F A G H         C E F D B H G A

*/

2022.1.7更新…

四、力扣刷题

106. 从中序与后序遍历序列构造二叉树

[力扣LC]关于中序后序构造二叉树

在这里插入图片描述
思路:通过后缀式的特点,定位每个区间内根节点的位置。例如,通过后序最后一位可以确定树的根节点为3,从而将中序序列分成三部分。

接下来,分别对左区间和有区间做相同的操作即可。唯一的难点在于确定左区间根的位置与确定右区间根的位置。

对于左区间而言
1. 根据后序特点:[左]-[右]-[根] 。我们通过中缀式计算得到 根右边节点个数
2. 通过 当前根的位置 - 右节点个数 = 左结点位置
3. 而该左节位置的值即为左区间的根节点值

在这里插入图片描述
对于右区间而言

  1. 根据后序特点:[左]-[右]-[根] 。
  2. [左]-[[左]-[右]-[根]]-[根] 。因此,对于右区间而言,只需将 当前根位置减一,即可得到右区间的根位置。
    在这里插入图片描述
    全部代码参考:
class Solution {
    // 在中序遍历中找根位置
    int find_root(vector<int>& inorder, int begin, int end, int r_val)
    {
        int pos = -1;
        for ( ; begin <= end; ++begin)
        {
            if (inorder[begin] == r_val)
            {
                pos = begin;
                break;
            }
        }
        return pos;
    }
    TreeNode* createTree(vector<int>& inorder, int begin, int end, vector<int>& postorder, int n)
    {
        TreeNode* proot = nullptr;
        if (n >= 0 && begin<=end)  
        {
            int r_val = postorder[n]; // 获得根节点元素
            proot = new TreeNode(r_val);
            int pos = find_root(inorder, begin, end, r_val);    // 根节点在中序串的位置
            proot->left = createTree(inorder, begin, pos - 1, postorder, n - (end - pos) - 1); // 左区间 [begin,pos-1]
            proot->right = createTree(inorder, pos + 1, end, postorder, n - 1);	// 右区间 [pos+1,end]
        }
        return proot;
    }
    
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.empty() || postorder.empty() || inorder.size() != postorder.size())
            return nullptr;
        int n = postorder.size();
        return createTree(inorder, 0, n-1, postorder, n-1);
    }
};
[力扣LC]关于先序中序构造二叉树

同理可以参考中序+后序方式。

先序的特点:首位是树的根节点,则可以根据先序首位将中序分成三部分 (左子树,根,右子树)。
然后分别在左子树、右子树中通过上述方式继续相同的步骤,这里可以采用递归的方式进行。

而递归的出口条件是,当我们已经找到先序中的最后一个根,即先序序列的最后一位。(n < preorder.size())。

全部代码参考:

class Solution {
    // 在中序遍历中找根位置
    int find_root(vector<int>& inorder, int begin, int end, int r_val)
    {
        int pos = -1;
        for ( ; begin <= end; ++begin)
        {
            if (inorder[begin] == r_val)
            {
                pos = begin;
                break;
            }
        }
        return pos;
    }
    TreeNode* createTree(vector<int>& inorder, int begin, int end, vector<int>& preorder, int n)
    {
        TreeNode* proot = nullptr;
        if (n <= preorder.size() && begin <= end) 
        {
            int r_val = preorder[n]; // 获得根节点元素
            proot = new TreeNode(r_val);
            int pos = find_root(inorder, begin, end, r_val);    // 根节点在中序串的位置
            
            proot->left = createTree(inorder, begin, pos - 1, preorder, n + 1);
            proot->right = createTree(inorder, pos + 1, end, preorder,  n + (pos - begin) +1);
        }
        return proot;
    }
    
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.empty() || inorder.empty() || preorder.size() != inorder.size())
            return nullptr;
        int n = preorder.size();
        return createTree(inorder, 0, n-1, preorder, 0);
    }
};
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我叫RT

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

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

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

打赏作者

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

抵扣说明:

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

余额充值