408复习笔记——数据结构(五):树和二叉树

408考研笔记系列(五)(PS:本人使用的是王道四本书和王道视频)


前言

至此,我们学习了线性表、栈和队列、串这几种逻辑结构为线性的数据结构,也了解了他们的不同存储结构实现以及在不同存储结构下各种基本操作的实现;自此,我们将开始学习非线性逻辑结构的数据结构,首先也就是今天这一章的主角——树,也就是树形逻辑结构,包括这种逻辑结构的各种树;我喜欢把这一章说成种树,因为我们可以在这一章里面种出各种各样的树(当然有些树还是很恶心人的);
在这里插入图片描述


一、简介

正如前言中提到的,这一章中主要便是在种各种各样的树,从最简单也是最常见的二叉树,再到二叉树的各种变种——完全二叉树、二叉排序树、平衡二叉树、哈夫曼树等等;再到由树变为多棵树组成的森林;这也预示着本章内容将会是非常多也是非常之重要的;需要引起高度重视,在考试中也是经常出现相关考题;

二、主要内容

1.树

首先,我们需要知道树是什么,其实跟我们常见的树形结构一样,有根结点,也有分支节点还有叶子结点,他们组成了一棵树的结构;就像下图一样
在这里插入图片描述
其中树的根结点是没有前驱的,除了根结点以外的所有结点都是有且只有一个前驱;树中一个结点的孩子个数称为该结点的度;树的路径长度是指树根到每个结点的路径长的总和,根到每个结点的路径长度的最大值应为树的高度减一;

树常见的考点有以下几个(这里不对以下结论做解释):

1. 结点数 = 总度数 + 1
2. 度为m的树是指各结点度的最大值为m;m叉树是指每个结点最多只能有m个孩子;前者是一定有,后者是可以有;
3. 度为m的树第i层至多有m^(i-1)个结点
4. 高度为h的m叉树至多有(m^h - 1)/(m - 1)个结点(等比数列求和)
5. 高为h的m叉树至少有h个结点;高为h的度为m的树至少有(h + m - 1)个结点(大概就是每一层只有一个结点的样子)
6. 具有n个结点的m叉树最小高度为log[n(m - 1)+1]

其中第一和第二个考点考察的频率相对较高

2.二叉树

二叉树便是一种逻辑结构为树结构的数据结构,正如他的名字一般,其特点便是每个结点至多只有两个子树,左子树或者右子树;就像这样:
在这里插入图片描述
二叉树有一些非常重要的性质,当然上面树的特点二叉树都是具备的,除此之外,还有一些二叉树特有的性质也是在考试中非常经常出现的考点:

非空二叉树的叶子结点n0等于度为2的结点数n2加1(n0 = n2 +1),这个性质的来源便是上述中树的第一个考点,因为二叉树只有度为0、1、2三种类型的结点,因此设度为0、1、2的结点数分别为n0、n1、n2,结合树的第一条性质得到等式:n0 + n1 + n2 = n1 + 2*n2 + 1,化简便得到n0 = n2 + 1;

以上适用于所有情况的二叉树,但是基于实际情况,就同栈和队列相较于线性表一般,二叉树也有一些特殊的二叉树——满二叉树、完全二叉树、二叉排序树以及平衡二叉树等,这里主要介绍满二叉树和完全二叉树,剩下的树会在接下来的章节继续介绍;

这里放一张满二叉树的图片:
在这里插入图片描述
没错,图如其名,满二叉树就是满的二叉树;

那么完全二叉树呢?这个感觉看名字就和满二叉树一样呀!
那这里放一张完全二叉树的图片:
在这里插入图片描述
是的,这两张图都是完全二叉树!大家肯定会发现第一张图这不就是满二叉树嘛,没错,满二叉树也是一种完全二叉树;完全二叉树的定义是这样的:高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中的1~n的结点一一相对应时,称为完全二叉树;
在这里插入图片描述

从上面这张图中,我们可以看到F所在的位置在右图是与满二叉树相对的,而左图是不对应的,右图正式我们的完全二叉树,大家可以结合下图看一下完全二叉树以及满二叉树的相同与不同之处

在这里插入图片描述
那完全二叉树和满二叉树的考点又是什么呢?

  1. 第一个考点就是二叉树的考点,不过会结合下面的考点一起考察
  2. 当完全二叉树按1、2、3……n依次编号,结点 i 的左孩子编号为2i ,右孩子编号为2i + 1(如果他们的左右孩子都存在的情况下),若已知孩子结点的编号,反之可以得到父亲结点的编号;
  3. 满二叉树第 k 层有2^(k-1)个结点
  4. 完全二叉树的度为1的结点个数为0或者1,因此结合第一个考点,在已知结点总数的情况下是可以得到完全二叉树的叶子结点和度为2的结点数量;

至此我们已经知道了二叉树和二叉树的特殊情况,那么按照数据结构的三个要素,接下来,我们需要看一下二叉树的存储结构,跟线性表一样,二叉树的存储结构也可以分为顺序存储结构和链式存储结构‘;

顺序存储结构的关键就在于我们在满二叉树中关于孩子与父亲之间的编号的关系,但是有一个很大的问题便是当我们存储一个普通二叉树时,如果树比较高,但是树中元素数量不多时,由于我们需要这个二叉树高度的满二叉树所有元素的存储空间,因此空间的利用率会十分低下,导致存储密度很低,浪费存储空间;如下图所示:
在这里插入图片描述
所以一般在二叉树存储时,我们都会采用链式存储的方式来存储二叉树,又称二叉链表;像这样:
在这里插入图片描述
我们的二叉树结点代码会这样写:

// 用于定义一个二叉树结点
typedef struct BiTNode{
	int data;
	struct BiTNode *lchild, *rchild;// 分别指向二叉树的左结点和右结点
}BiTNode,*BiTree;

有时为了实际需要,会加上一个指向头结点的指针,这里的二叉链表有一个非常重要的性质:
n个结点的二叉链表中,含有n + 1个空链域,就是指针指向为空;大家可以结合上面的二叉链表图片理解一下;

之后,我们便会开始我们的第三个要素:运算,展开新的篇章,也会给大家种新的树;

3.二叉树遍历

上面我们介绍了二叉树是一种逻辑结构,以及他的两种存储结构分别是顺序存储结构和链式存储结构的二叉链表,因为顺序存储结构会导致存储密度非常低,因此我们很多情况下会选择链式存储结构的方法实现二叉树;并且我们知道二叉链表会有两个指针分别指向左孩子和右孩子;

那么接下来呢!我们便来了解一下二叉树的运算,前面的运算我们主要包括创建、销毁、增删改查等;但是在这里呢,我们更加关注二叉树的一个遍历运算,就是如何输出我们的二叉树;因为在线性表时,我们知道数与数之间是呈线性的,而二叉树是一种树的结构这就导致我们在从根结点遍历输出我们的结点时,是有很多种方式的;以最简单的二叉树而言,我们便有前序遍历、中序遍历、后序遍历和层次遍历这四种;

那么,他们分别是怎样实现的呢?这也是我们在考试中经常会考察的点;其中前序、中序和后序他们的前中后指的都是图中父结点(我们通常称之为根节点)相较于他的左孩子和右孩子的位置;
在这里插入图片描述

前序遍历的过程大概是这样的:
在这里插入图片描述
中序遍历的过程大概是这样的:
在这里插入图片描述
后序遍历的过程大概是这样的:
在这里插入图片描述
图片中过程其实是前中后序代码实现的过程,我们会通过递归函数的方式实现我们的前中后序遍历,因为是由递归函数的方式实现,所以源代码是很简单的,代码如下所示:
前序遍历:

// 前序遍历
// 前序遍历的顺序是:根左右
void PreOrder(pNode_t root)
{
	if (root != NULL)
	{
		printf("%3c", root->c);
		PreOrder(root->pleft);
		PreOrder(root->pright);
	}
}

中序遍历:

// 中序遍历
// 中序遍历的顺序:左根右
void InOrder(pNode_t root)
{
	if (root != NULL)
	{
		InOrder(root->pleft);
		printf("%3c",root->c);
		InOrder(root->pright);
	}
}

后序遍历:

// 后序遍历
// 后序遍历的顺序:左右根
void PostOrder(pNode_t root)
{
	if (root != NULL)
	{
		PostOrder(root->pleft);
		PostOrder(root->pright);
		printf("%3c", root->c);
	}
}

通过图片结合代码的方式相信还是比较容易理解的;这里有一个代码考察的点是当我们用非递归方式实现后序遍历时,当访问一个结点P时,栈中结点恰好是P结点的所有祖先,也会在后面的习题中体现;

除了前中后序三种遍历的方法外,还有一种层次遍历的方式,这种方式非常容易理解,大家看一张图就知道了:
在这里插入图片描述
其实在队列的时候是由介绍过层次遍历的,没错,一般我们是通过辅助队列的方式实现二叉树的层次遍历的:
在这里插入图片描述

代码如下:

// 层序遍历
// 通过队列的方式实现层序遍历
void LevelOrder(pNode_t root)
{
	LinkQueue LQ;
	InitQueue(&LQ);
	EnQueue(&LQ, root);
	while (LQ.length != 0)
	{
		DeQueue(&LQ);
	}
}

这里可能和大家见到的代码不太一样,我是把队列的入队和出队稍微修改了一下,这里可以看一下新的入队和出队代码:

// 从队尾的入队操作
void EnQueue(LinkQueue * LQ, pNode_t x)
{
	//在链式队列中,不会存在队满的问题,因此不需要像顺序队列中判断队满的情况
	LinkNode *s;
	s = (LinkNode*)malloc(sizeof(LinkNode));
	s->data = x;
	// 这里在链式队列的头指针出入队元素
	s->next = NULL;
	(*LQ).rear->next = s;
	(*LQ).rear = s;
	(*LQ).length++;
}
// 出队操作
void DeQueue(LinkQueue *LQ)
{
	//出队时需要判断是否已经队空,若队空则不要出队
	if ((*LQ).length == 0)
	{
		printf("Queue is empty");
	}
	LinkNode *p;
	p = (*LQ).front->next;
	pNode_t x = p->data;
	printf("%3c", x->c);
	(*LQ).front->next = p->next;
	(*LQ).length--;
	if ((*LQ).length == 0)
	{
		// 最后需要判断是为只有最后一个元素,若只有一个元素,那么需要在free前将尾指针指向头指针
		(*LQ).rear = (*LQ).front;
	}
	free(p);
	if (x->pleft != NULL)
	{
		EnQueue(LQ, x->pleft);
	}
	if (x->pright != NULL)
	{
		EnQueue(LQ, x->pright);
	}
}

其实入队也没什么变化哈!!!

至此便介绍了四种遍历算法,这四种遍历方式还有一个非常常见的考点就是,根据其中两个来搭建一颗二叉树,这里需要注意的是:只有中序遍历和其他三种遍历任意一种存在的情况下搭建的二叉树才是唯一的;这里的关键在于找他的根结点;

4.线索二叉树

在二叉树的链式存储结构那里我们知道:二叉树是由两个指针分别指向左孩子和右孩子,并且每个二叉树都有n + 1 个空链域;而我们的二叉树遍历又有四种不同的形式,当然最主要便是前序、中序和后序遍历啦!既然我们有那么多的空链域,那么我们为何不能把这些空指针给他利用起来呢?

于是乎,大佬们决定结合前序、中序、后序三种遍历方式将我们的n + 1个空指针利用起来,大佬之所以是大佬,是因为他们从来不会浪费,哪怕只有一个字节!就这样前序线索树、中序线索树、后序线索树便应运而生啦!这里需要注意的是:线索二叉树因为是计算机的一种存储结构,所以线索二叉树是一种物理结构;他们的使命就是把这些空指针便有用;

那么大佬们具体都拿这些指针干了什么呢?而且为什么会有三种线索树呢?其实结合他们的名字我们可以猜到,这些线索树肯定和三种遍历有关呀!三种遍历什么不一样呢?肯定是每种遍历后的结点输出顺序不一样呀!那么我们能用这些指针干些什么呢?没错,我们就是用这些指针指向三种遍历序列上的前驱和后继;

通俗而言,如果一个结点是叶子结点,他的左孩子指针和右孩子指针指向的都是NULL,那么当把他线索化为前序线索树后,我们就对照这棵树的前序序列,给这个叶子结点的左孩子指向他在前序序列里面前面的那个结点,而右孩子指针指的就是他在前序序列里面后面的那个结点;就如同下图:
在这里插入图片描述
但是这个时候问题又来了啊,你既然这样搞,那么我原来如果有右孩子的话,我怎么知道现在的还是不是我的右孩子呀?
为了解决这个问题,我们为每个结点添加了做标记和右标记用来记录这个指针指向的是右孩子还是前序序列的后继结点,像这样:

// 用于定义一个线索二叉树结点
// 线索二叉树的空指针域指向的是分别是改为指向其前驱或者后继的线索
typedef struct threadnode {
	char c;
	int ltag, rtag;//用于表示线索二叉树的左右指针指向的是其左右孩子还是线索前驱或者后驱
	struct threadnode *pleft;
	struct threadnode *pright;
}ThreadNode, *ThreadTree;

接下来,我们便要解决的是如何将二叉树进行三种线索化,既然线索化,那么我们依然可以通过之前的三种遍历手法,不过在每次访问结点时判断是否有指针为空,并通过设置全局变量的前驱结点pre的方式来修改指针,代码如下:

// 创建一个中序线索二叉树
ThreadNode CreatInThread(ThreadTree T)
{
	pre = NULL;
	if (T != NULL)
	{
		InThread(T);// 中序遍历二叉树,并将二叉树线索化;
		// 因为中序线索化最后visit的结点不会判断其右孩子的去向,因此线索化最后需要对最后一个结点的右结点进行判空
		// 但是中序遍历最后一次visit后会执行InThread(p->right),所以在中序时不加判断也是没有问题的
		if (pre->pright == NULL)
		{
			pre->rtag = 1;// 如果最后指向的右孩子为空,那么需要将标记设置为1,因为此时的右孩子结点已经指向后继结点
		}
	}
}

// 中序线索化二叉树
void InThread(ThreadTree T)
{
	// 整个过程其实就是二叉树的中序遍历,但是在中序遍历的过程中将空指针分别指向前驱结点和后继结点
	if (T != NULL)
	{
		InThread(T->pleft);
		visit(T);
		InThread(T->pright);
	}
}

// 访问该结点,并改变该结点的空指针域,使其中序线索化
void visit(ThreadNode *p)
{
	if (p->pleft == NULL)
	{
		p->pleft = pre;// 若该结点的左孩子为空,那么将他的左孩子指针指向其前驱结点
		p->ltag = 1;// 并将其左孩子的标记记为1
	}
	if (pre != NULL && pre->pright == NULL)
	{
		pre->pright = p;// 由于在访问该结点前,访问的结点一定是该结点中序遍历的前驱结点,因此将该结点设置为其前驱结点的后继
		pre->rtag = 1;
	}
	pre = p;
}

后序和中序是一样的,不过中序在 CreatInThread函数结尾的判断可有可无,因为在中序遍历最后还会有InThread(T->pright);但是后序遍历就必须要有这个判断才行;

这里就不放后序线索化的代码了,前序就和中序在遍历过程有较大的差别,因为前序是先visit结点才去ProThread(T->pleft)和ProThread(T->pright)的,而在visit函数中,左孩子指针和右孩子指针都有可能会发生变化的,因此我们往往会在前面加上一个判断,代码如下:

// 前序线索化遍历
void ProThread(ThreadTree T)
{
	if (T != NULL)
	{
		visit(T);
		if (T->ltag == 0)
		{
			// 因为在visit该结点后,若该结点左指针为空,会修改左指针,这样就会避免一直在原地转圈
			ProThread(T->pleft);
		}
		if (T->rtag == 0)
		{
			ProThread(T->pright);
		}
		//ProThread(T->pright);
	}
}

其他代码和中序一样;

最后在线索二叉树遍历这里需要注意后序线索树的遍历仍然需要栈的支持才行,就如同非递归方式实现二叉树后序遍历一样,线索二叉树的后序遍历也较为复杂;此外,还有一点需要注意的是后序线索二叉树不能有效解决求后序后继的问题,而前序线索二叉树不能有效解决求前驱结点的问题;这里的代码等有空再补上吧!今天下午调这个递归函数难受的不行啊,王道的代码还是有待加强的啊!

5.树和森林

至此,二叉树的存储结构和常用的运算都已经介绍完毕啦!我们了解到二叉树的存储结构一般会采用链式存储——二叉链表,后来因为二叉树链式存储中会有N+1个空链域,并且二叉树有前中后序遍历方法;于是通过利用空链域实现三种遍历形成了三种遍历的线索树;

那么这时我们就会想到,那普通的树呢?他们又是怎样解决存储问题的呢?这里给出了三种方法:双亲表示法、孩子表示法和孩子兄弟表示法;我们分别图片进行展示:
1、双亲表示法
在这里插入图片描述
为每一个结点增设一个伪指针,指示其双亲结点在数组中的位置;该方法如果想要得到结点的双亲结点十分简单,但是求结点的孩子则需要遍历整个结构;

2、孩子表示法:
在这里插入图片描述
这种存储方式显然是在寻找孩子时是非常方便的,但是寻找双亲时便需要遍历所有结点;

3、孩子兄弟表示法
在这里插入图片描述
这种方法和二叉链表一样,有两个指针,分别指向第一个孩子和结点的下一个兄弟结点;相比较于前面两种,我们更常见的便是我们的孩子兄弟表示法;

我们往往会将树和森林转换为我们的用孩子兄弟表示法时的二叉树,而根据我们的左孩子右兄弟原则,这种转换往往是非常容易的,但是针对这种转换,也会有一些常见的考点——二叉树右指针为空问题;这个问题指的是我们将森林(树)转换为二叉树的过程中,因为森林中每棵树的根结点从第二个开始依次连接到前一棵树的根的右孩子,因此最后一棵树的根结点的右指针为空,另外每一个非终端结点的所有孩子结点在转换之后,最后一个孩子的右指针也为空,所以森林在转换为二叉树后右指针为空的结点有n+1个(n为非终端结点)

说完树的存储结构,接下来便是树的运算啦!与二叉树一样,这里比较重要的便是树的先根遍历和树的后根遍历;以及森林的先序遍历和森林的中序遍历;继续用图片来说明:
树的先根遍历:
在这里插入图片描述
若树非空,先访问根结点,再依次遍历根结点的每棵子树;遍历顺序和这棵树对应二叉树的先序序列一样

树的后根遍历
在这里插入图片描述
遍历序列和这棵树相应的二叉树中序序列是相同的;

森林的线序遍历则是访问森林的第一棵树的根结点,再先序遍历第一棵树根结点的子树森林,先序遍历除去第一棵树之后剩余的树构成的森林;森林 的中序也是如此;

并且**森林的先序遍历和中序遍历结果和该森林对应的二叉树的先序遍历和中序遍历是一样的;**这也是考试中在这里最常见的考点;

6.二叉树的应用

6.1. 二叉排序树

前面我们有谈到,这一章的工作便在于种树;那么接下来呢!我们便会开始种几种在日常生活中非常常见的二叉树。分别是二叉排序树(BST)、平衡二叉树(AVL)和哈夫曼树;这几颗树都是在我们日常生活中用到频率非常高的树;

首先二叉排序树,这种树其实我们在之前也有提到过,他的目的便是排序,他遵从的一个规律便是——根结点值大于右子树结点值,根结点值小于左子树结点值;就像这样:
在这里插入图片描述
可以看到我们每一个结点都是遵循左小右大这个基本原则的;值得注意的是:基于这个基本原则,我们发现二叉排序树的中序序列正好是这些树的升序序列,关于二叉排序树的运算,我们需要知道任意一棵二叉排序树T1中删除某结点v之后形成的二叉排序树T2,再该结点v插入T2中得到的二叉排序树T3存在这样的关系:
若v是T1的叶子结点,则T1与T3相同;
若v是T1的非叶子结点,则T1与T3就不会相同;

除此之外,也会考察二叉排序树查找效率之类的问题,如查找成功的平均查找长度ASL和查找失败的平均查找长度,这是关注的便是我们二叉排序树的高度啦!
在这里插入图片描述
向左边和右边两个二叉排序树,我们通常都会更加喜欢左边的,因为他的树的高度更小,那么查找的时间更短;

6.2 平衡二叉树

平衡二叉树的出现便来自于我们上面说到的树的高度啦!我们在二叉树排序树中往往更喜欢胖一点、矮一点的树,事实也证实这样的查找效率更高;平衡二叉树的定义便是任意结点的左右子树高度差绝对值不超过1;刚开始接触可能不太好理解,我们先看几张平衡二叉树的图片,像这样:
在这里插入图片描述
更复杂点的像这样:
在这里插入图片描述
这里放的是平衡二叉树和非平衡二叉树的对比
在这里插入图片描述

他们的规则就是任意结点的左右子树高度差不能超过1;那问题便来了啊!我插入和删除时如果有结点的左右子树高度差大于1了怎么办?
为此,我们会将调整平衡二叉树问题分成四类:LL、RR、LR、RL;

  1. LL指的是A结点左孩子的左子树插入新的结点导致失衡,解决办法一般是进行一次右旋操作,过程大概是这样的:
    在这里插入图片描述

  2. RR指的是A结点的右孩子的右子树插入新结点,导致失去平衡;这时我们会对A结点左旋;大概是这样的:
    在这里插入图片描述

  3. LR指的是结点A的左孩子的右子树上插入新的结点导致失衡,需要先左旋再右旋,大概过程是这样的:
    在这里插入图片描述

  4. RL指的是A结点的右孩子的左子树上插入新结点,导致二叉树不平衡,,这时需要先右旋再左旋
    在这里插入图片描述

平衡二叉树的调整貌似有点复杂,有四种情况需要考虑,但是我们不难发现其实我们在进行调整时,总是找到出问题地方的三个非叶子结点,并找到这三个非叶子结点的中间值作为新的根结点,而剩下的结点我们只需要按照二叉排序树的方法更改位置即可;

此外,平衡二叉树的结点数也有一个常用的规律:假设n(h)表示深度为h的平衡二叉树含有的最少结点数,那么n(0) = 0,n(1) = 1,n(2) = 2,n(h) = n(h-1) + n(h-2) +1;这个规律在考试中也是经常会考察到,大家记住这个结论可以在做题中节省很多的时间并且可以提高准确度;

平衡二叉树还有一个考点需要我们知道的是:树中最大元素一i的那个是无左子树的,否则肯定会不平衡的

6.3 哈夫曼树和哈夫曼编码

在我们常见的树中,往往结点都是被赋值的,我们将该值称之为结点的权,从根结点到任意结点的路径长度与该结点上的权值的乘积称之为该结点的带权路径长度,树的所有叶结点的带权路径长度之和称之为带权路径长度(WPL);而带权路径长度最小的二叉树便是哈夫曼树;

哈夫曼树的构造过程如下所示:
在这里插入图片描述
其中哈夫曼树有几个考点是值得我们注意的:

1. 哈夫曼的叶子结点为n,那么非叶子结点为n -1 个
2. 哈夫曼树不存在度为1的结点

而这些在考试中可能会结合二叉树以及树的一些特性进行考察;

哈夫曼编码主要是基于哈夫曼树而来,常用于数据压缩,在考试中也经常出现,这里给出哈夫曼编码的一个例子,可以看到我们通过哈夫曼编码的方式给我们的a~h字母进行了编码;一
至此,树便完结啦!可以说内容非常之多,但是,还是容易掌握的,这里还是需要注意线索树这样的难点,希望不会出线索树遍历这样的代码题吧!


三、常见题型及易错题总结

串和模式匹配算法的答案:C

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

答案见下一章(PS:昨天吃东西吃坏肚子了,今天都快拉虚脱了,难受的不行,学习效率也十分低下,希望早点好起来吧!!!大家在考研期间一定要注意卫生啊!身体才是革命的本钱啊!)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值