队列各个功能函数的实现

目录

一、队列

1.队列的概念

 2..队列和栈的结构的对比

二、对队列的实现方式进行解析

1.队列用链表实现比队列用数组实现更加方便的原因

2.对队列使用链表实现时选择8种链表结构的那一种实现进行解析

3.解析队列在用链表实现时为什么要定义一个结构体来控制队列

对Oueue.h队列的头文件要声明结构体struct Oueue的原因进行解析,分析过程如下

1.在单链表和带头循环双向链表的头文件SList.h和头文件List.h中不需要声明一个结构体并用这个结构体在主调函数中控制单链表和带头循环双向链表的原因

2.在Oueue.h队列的头文件中要声明一个结构体struct Queue并用这个结构体在主调函数中控制队列的原因

三、队列的链表实现

1.Queue.h头文件

2.Queue.c源文件

3.test.c测试代码

四、总结

1.栈和队列的打印函数Stackprintf和Queueprintf使用的方式

2.遍历栈并取栈中的数据

3.遍历队列并取队列中的数据

4.分析说明当我们在统计多个队列中各自存储数据个数时最后把size包含到队列结构体中。


一、队列

1.队列的概念

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有FIFO(First In First Out) 先进先出特性。入队列:进行插入操作的一端称为队尾;出队列:进行删除操作的一端称为队头。

 2..队列和栈的结构的对比

队列的结构和栈的结构是相反的,栈的结构是只能在栈顶进行插入和删除数据,即栈的结构是固定在一端进数据和出数据的。而队列的结构是只允许在队尾进数据,在队头出数据,即队列只允许在一端插入数据而必须在另一端删除数据。

二、对队列的实现方式进行解析

1.队列用链表实现比队列用数组实现更加方便的原因

(1)因为用数组实现队列的话,由于数组的尾部方便入数据导致队列选数组的尾部当队尾,而数组的头部当队头。由于数组的头部出数据需要通过挪动大量的数据才能实现出1个数据导致队列的队头出数据效率低。总的来说,即使数组尾部是队尾而这个队尾入数据方便,但是数组头部是队头而这个队头出数据不方便,所以队列一般不用数组实现。

(2)虽然利用数组实现队列是队头和队尾是我们自己定的,但不管数组的那一端是队头那一端是队尾,反正其中有一端是一定要大量挪动数据来实现插入1个数据或者删除1个数据的,所以此时利用数组实现队列的效率低。

2.对队列使用链表实现时选择8种链表结构的那一种实现进行解析

在使用链表实现队列时,应该使用8种链表中的那一种比较合适,以下是对队列选用哪种链表的分析过程:

(1)队列选单链表实现而不适用双向链表实现的原因:双向链表的优势是方便找前一个结点,但是由于队列没有要找前一个结点的需求导致队列的实现不用双向链表,所以队列的实现用单链表。

(2) 队列用链表实现时这个链表带或不带哨兵位的头结点。

(3)队列不用循环链表的原因:因为用循环链表去实现队列的价值不大。

(4)这里不用带头循环双向链表实现队列的原因:

带头循环双向链表的优势是方便头插头删、方便尾插尾删、方便中间插中间删,但是队列不需要用这么强的链表去实现,所以队列才不用带头循环双向链表去实现。

(5)以下是队列真正结构:

利用两个指针控制队列,一个是队头指针head,而队头指针head指向队列的队头,而且队头指针head是指向出数据的一端即单链表要进行头删的一端;另一个是队尾指针tail,而队尾指针tail指向队列的队尾,而且队尾指针tail是指向入数据的一端即单链表要进行尾插的一端。

注意:由于单链表的优势是头删所以直接用单链表的头部当队列的队头,而且用队头指针head指向单链表的头部,由于单链表的尾插不方便导致队列再用单链表的尾部作为队尾进行尾插的效率低所以用一个队尾指针rear指向单链表的尾部这样的话就方便单链表进行尾插了。队列的结构体如下图所示:

总的来说,队列只需用不带头不循环的单链表实现。注意:队列在用单链表进行实现时,在主调中要定义一个结构体struct Queue类型的变量,而这个结构体类型变量中包含控制队列的两个指针即一个是队头指针head而另一个是队尾指针tail,而定义的这个结构体是用来在主调函数中控制队列的。

3.解析队列在用链表实现时为什么要定义一个结构体来控制队列

对Oueue.h队列的头文件要声明结构体struct Oueue的原因进行解析,分析过程如下

1.在单链表和带头循环双向链表的头文件SList.h和头文件List.h中不需要声明一个结构体并用这个结构体在主调函数中控制单链表和带头循环双向链表的原因

在单链表和带头循环双向链表的代码实现中主调函数都是只需定义一个指针head指向单链表和带头循环双向链表就可以通过指针head找到单链表和带头循环双向链表,而且通过把指针head传参给各个功能函数,而各个功能函数就可以通过指针head找到单链表和带头循环双向链表并对单链表和带头循环双向链表进行操作,所以不需要在链表的头文件中声明一个结构体包含指针head并在主调函数中定义一个结构体去控制单链表和带头循环双向链表。

总的来说,由于单链表和带头循环双向链表在主调函数中只需由一个指针head进行控制即只需一个值控制链表(注意:这个值指的是指针head),而结构体(struct)是一种集合数据类型而且结构体是将不同类型的数据组合在一起,所以我们不需定义一个结构体把指针head包含在内,这样做是没有必要的。

2.在Oueue.h队列的头文件中要声明一个结构体struct Queue并用这个结构体在主调函数中控制队列的原因

由于队列是由两个指针进行控制的即队头指针head和队尾指针tail,但是在主调函数中定义两个指针不太方便即定义一个指针head指向队列的队头和定义一个指针tail指向队列的队尾,所以可以在队列的头文件中声明一个结构体struct Queue来包含这两个指针即队头指针head和队尾指针tail,然后在主调函数中只需定义一个struct Queue类型的结构体变量就可以控制队列了。

总的来说,由于队列在主调函数中是由两个指针进行控制的即多个值控制队列,则此时才定义一个结构体包含控制队列的多个值,然后在主调函数中只需定义一个结构体变量就代替多个值去控制队列了。

三、队列的链表实现

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。(注意:下面图形中指针front相当于代码的队头指针head,而指针rear相当于代码的队尾指针tail)

1.Queue.h头文件

//Queue.h


#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

//队列的数据类型
typedef int QDataType;

//队列链表的结点类型
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

//队列的结构体类型
typedef struct Queue
{
	QNode* head;//指针head指向队列队头数据。
	QNode* tail;//指针tail指向队列队尾数据。
	int size;//统计队列存储数据的个数
}Queue;


//初始化函数
void QueueInit(Queue* pq);

//销毁函数
void QueueDestroy(Queue* pq);

//入队函数->尾插(插入数据)
void QueuePush(Queue* pq, QDataType x);

//出队函数->头删(删除数据)
void QueuePop(Queue* pq);

//取队头数据
QDataType QueueFront(Queue* pq);

//取队尾数据
QDataType QueueBack(Queue* pq);

//判断队列是否是空队列
bool QueueEmpty(Queue* pq);

//统计队列中存放数据的个数
int QueueSize(Queue* pq);

//打印函数
void QueuePrint(Queue* pq);

2.Queue.c源文件

//Queue.c


#include "Queue.h"

//初始化函数->目的:把队列q初始化为空队列
void QueueInit(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	//对队列q的结构体成员进行初始化
	//由于队列最初状态是个空队列使得当用单链表实现队列时单链表最初的状态也是空链表,而要使得指针pq->head指向的链表是个空链表,则只需把指针pq->head的值设置为空指针NULL即可。
	pq->head = pq->tail = NULL;//由于队列一开始是个空队列使得让队头指针pq->head和队尾指针pq->tail一开始指向空链表。
	pq->size = 0;
}

//销毁函数
void QueueDestroy(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	//由于队头指针pq->head始终是指向队头数据的,所以想要遍历整个链表的话则我们需要定义一个临时指针cur遍历链表。
	QNode* cur = pq->head;
	while (cur)//当cur指向NULL,则遍历链表结束。
	{
		//用指针del保存当前要销毁结点的地址
		QNode* del = cur;
		//注意:必须是先让cur移动到下一个结点的位置后再删除del位置的结点。
		cur = cur->next;
		free(del);
	}
	//当链表的所有结点都删除完后必须把队头指针和队尾指针都设置成空指针,否则没有置为空指针的话若通过主调函数队列的结构体访问到队头指针和队尾指针的话会造成对野指针进行访问的风险。
	pq->head = pq->tail = NULL;
	pq->size = 0;
}

//入队函数->尾插(插入数据)
//注意:由于在队列的所有功能函数中只有入队函数QueuePush才会增加队列的数据个数,
//而其他队列的功能函数都没有增加队列数据的个数,所以不需要单独写一个函数来创建队列链表的结点。
void QueuePush(Queue* pq, QDataType x)
{
	//判断指针pq是否是空指针
	assert(pq);
	//创建结点
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//对新创建结点的成员变量进行初始化
	newnode->data = x;
	newnode->next = NULL;

	//判断是否是头插
	if (pq->head == NULL)//或者写成if (pq->tail == NULL)
		pq->head = pq->tail = newnode;
	else//尾插
	{
		//把新创建的结点和队尾结点链接起来
		pq->tail->next = newnode;
		//让队尾指针指向新的队尾结点
		pq->tail = newnode;
	}
	pq->size++;
}

//写法1:出队函数->头删(删除数据)
void QueuePop(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	//判断队列是否是空队列,若是空队列则不继续删除队头数据。
	assert(!QueueEmpty(pq));

	//判断链表是否删除到还剩一个结点->这里要判断这种情况的原因是:由于我们会使用队头指针和队尾指针判断队列是否是空队列,当队列还剩一个结点而且还要进行头删的时候,
	//若没有if语句的判断只执行else语句的内容会使得即使队头指针pq->head会被赋值成空指针NULL,但是队尾指针pq->tail会变成野指针,这样会在QueueEmpty函数中发生对野指针pq->tail进行解引用。
	if (pq->head->next == NULL)
	{
		free(pq->head);
		//这种情况一定要把队头指针head和队尾指针tail置成空指针NULL,否则在判断队列是否是空队列时会发生对野指针进行解引用的风险
		pq->head = pq->tail = NULL;
	}
	else//头删
	{
		//保存当前要删除结点的地址
		QNode* del = pq->head;

		//注意:必须先让队头指针指向新的队头结点后再删除del位置的结点。
		//让队头指针指向新的队头结点
		pq->head = pq->head->next;
		//删除del位置的结点
		free(del);
	}
	pq->size--;
}

//写法2:出队函数->头删(删除数据)
void QueuePop(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	//判断队列是否是空队列
	assert(!QueueEmpty(pq));

	//头删的过程:
	//保存当前要删除的结点
	QNode* del = pq->head;
	//注意:必须先让队头指针指向新的队头结点后再删除del位置的结点。
	pq->head = pq->head->next;//换头
	//删除结点
	free(del);

	if (pq->head == NULL)//若队头指针pq->head = NULL说明此时队列被删除成空队列,但是此时队尾指针pq->tail为野指针,则此时必须把野指针pq->tail设置成空指针。
		pq->head = pq->tail = NULL;

	pq->size--;
}

//取队头数据
QDataType QueueFront(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);
	//判断队列是否是空队列,若队列为空则不需要取队头数据了。
	assert(!QueueEmpty(pq));

	return pq->head->data;//由于指针pq->head指向队列队头结点,所以只需通过队头指针访问队头结点中存放的数据即可。
}

//取队尾数据
QDataType QueueBack(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);
	//判断队列是否是空队列,若队列为空则不需要取队尾数据了。
	assert(!QueueEmpty(pq));

	return pq->tail->data;//由于指针pq->head指向队列队尾结点,所以只需通过队头指针访问队尾结点中存放的数据即可。
}

//判断队列是否是空队列
bool QueueEmpty(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	return (pq->head == NULL) && (pq->tail == NULL);//只有队头指针和队尾指针同时为空指针才能说明队列是个空队列
}

//统计队列中存放数据的个数
int QueueSize(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);
	
	写法1:时间复杂度O(N)
	//int size = 0;
	//QNode* cur = pq->head;
	//while (cur)
	//{
	//	cur = cur->next;
	//	size++;
	//}

	//return size;

	//写法2:时间复杂度O(1)
	return pq->size;
}


//打印函数
void QueuePrint(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	QNode* cur = pq->head;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

3.test.c测试代码

//test.c测试代码


#include "Queue.h"

void TestQueue1()
{
	//创建队列
	Queue q;
	//对队列q进行初始化
	QueueInit(&q);

	//入队->尾插
	QueuePush(&q, 1);
	QueuePrint(&q);

	QueuePush(&q, 2);
	QueuePrint(&q);

	QueuePush(&q, 3);
	QueuePrint(&q);

	QueuePush(&q, 4);
	QueuePrint(&q);

	QueuePush(&q, 5);
	QueuePrint(&q);

	//统计此时队列q存放数据的个数
	printf("size:%d\n", QueueSize(&q));

	printf("队头数据:%d\n", QueueFront(&q));
	printf("队尾数据:%d\n", QueueBack(&q));

	//出队->头删
	QueuePop(&q);
	QueuePrint(&q);

	QueuePop(&q);
	QueuePrint(&q);

	QueuePop(&q);
	QueuePrint(&q);

	QueuePop(&q);
	QueuePrint(&q);

	QueuePop(&q);
	QueuePrint(&q);


	//判断队列是否是空队列
	printf("QueueSize(0为非空队列,非0为空队列):%d\n", QueueEmpty(&q));
	//统计此时队列q存放数据的个数
	printf("size:%d\n", QueueSize(&q));

	//QueuePop(&q);

	//销毁队列q
	QueueDestroy(&q);
}

void TestQueue2()
{
	//创建队列q1、q2
	Queue q1;
	Queue q2;

	//对队列q1、q2进行初始化
	QueueInit(&q1);
	QueueInit(&q2);

	printf("1、队列q1:\n");
	//对队列q1进行压栈
	QueuePush(&q1, 1);
	QueuePush(&q1, 2);
	QueuePush(&q1, 3);
	QueuePush(&q1, 4);
	QueuePush(&q1, 5);
	QueuePrint(&q1);
	//统计此时队列q存放数据的个数
	printf("size:%d\n", QueueSize(&q1));
	printf("队头数据:%d\n", QueueFront(&q1));
	printf("队尾数据:%d\n", QueueBack(&q1));

	printf("\n\n");//把队列q1和队列q2分割开

	printf("2、队列q2:\n");
	//对队列q1进行压栈
	QueuePush(&q2, 3);
	QueuePush(&q2, 4);
	QueuePush(&q2, 5);
	QueuePrint(&q2);
	//统计此时队列q存放数据的个数
	printf("size:%d\n", QueueSize(&q2));
	printf("队头数据:%d\n", QueueFront(&q2));
	printf("队尾数据:%d\n", QueueBack(&q2));

}

int main()
{
	//测试队列
	TestQueue2();
	return 0;
}

四、总结

1.栈和队列的打印函数Stackprintf和Queueprintf使用的方式

栈和队列一般是不写打印函数的,因为打印函数是无法直接呈现出栈先进后成和队列先进先出的特性的,若想要呈现这两个数据结构各自的特性则它们各自的打印函数必须要边遍历边出数据。

2.遍历栈并取栈中所有数据的方式

注意:虽然while(cur)循环可以访问栈的链表结构,但是我们在主调函数访问栈中的数据时不要用while(cur)循环访问栈的链表结构,而是在主调函数中通过栈的各个接口函数去操作栈并通过接口函数去底层访问栈的链表结构中存放的数据。

遍历栈并取栈中所有数据的思路:由于栈是后进先出的特性,所以我们必须先利用StackTop函数取栈顶元素,然后若想取下一个元素则必须利用StackPop函数把当前栈顶元素删除掉。

遍历栈并取栈中的所有数据的代码:

3.遍历队列并取队列中所有数据的方式

注意:在访问队列时不要利用while(cur)去访问队列的链表结构即不要用while(cur)去访问链表的每个结点同时也不要利用while(cur)循环边遍历链表边打印链表每个结点中的数据data因为队列中的数据不是通过这样的方式去访问的;而想要访问队列中的数据只能在主调函数中通过所有的接口函数再结合利用while(!QueueEmpty(&s))循环遍历队列,然后通过这样的方式去访问队列中的每个数据,而且在访问数据时队列中的数据都是先进先出的。

遍历队列并取队列中的所有数据的思路:由于队列是先进先出的特性,所以我们必须利用QueueFront函数取队头的1个数据,然后若想取队头的下一个数据则必须在取数据之前先用QueuePop函数把队头当前的数据删除掉。

遍历队列并取队列中的所有数据的代码:

4.分析说明当我们在统计多个队列中各自存储数据个数时,最好在队列的结构体类型struct Oueue中增加一个成员变量size来统计多个队列各自存放数据的个数。

QueueSize函数统计每个队列存放的数据个数的错误思路:用全局变量size统计每个队列中存放数据的个数。

1.定义一个全局变量int size统计队列中存放的数据个数的思路和缺陷

(1) 利用全局变量int size统计队列中存放的数据个数的统计思路是:把全局变量int size写入到入队函数QueuePush和出队函数QueuePop中。当在主调函数中要用QueuePush函数在队尾插入数据时,QueuePush函数中的size值就size++;当在主调函数中要用QueuePop函数在队头删除数据时,QueuePop函数中的size值就size--。

(2) 利用全局变量int size统计队列中的数据的个数的缺点和隐患是:

当在主调函数中同时有两个队列即队列q1和队列q2,若这两个队列同时用QueuePush函数对队列1和队列2进行插入数据的话,我们无法知道QueuePush函数中全局变量size统计的是那个队列存放数据的个数。但我们知道这个全局变量int size此时统计的是整个工程中所有队列中存放数据的总个数。

下面是错误写法的QueueSize函数:(注意:下面代码只是错误写法的参考,所以看一下就行不用太在意这个代码)

//注意:这种写法只适用整个工程中只有1个队列并统计1个队列中存放数据个数这一种情况。


int size = 0;//全局变量

//队列的数据类型
typedef int QDataType;

//队列链表的结点类型
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

//队列的结构体类型
typedef struct Queue
{
	QNode* head;//指针head指向队列队头数据。
	QNode* tail;//指针tail指向队列队尾数据。
}Queue;


void QueuePush(Queue* pq, QDataType x)
{
	//判断指针pq是否是空指针
	assert(pq);
	//创建结点
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//对新创建结点的成员变量进行初始化
	newnode->data = x;
	newnode->next = NULL;

	//判断是否是头插
	if (pq->head == NULL)//或者写成if (pq->tail == NULL)
		pq->head = pq->tail = newnode;
	else//尾插
	{
		//把新创建的结点和队尾结点链接起来
		pq->tail->next = newnode;
		//让队尾指针指向新的队尾结点
		pq->tail = newnode;
	}
	size++;
}


//出队函数->头删(删除数据)
void QueuePop(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	//判断队列是否是空队列
	assert(!QueueEmpty(pq));

	//头删的过程:
	//保存当前要删除的结点
	QNode* del = pq->head;
	//注意:必须先让队头指针指向新的队头结点后再删除del位置的结点。
	pq->head = pq->head->next;//换头
	//删除结点
	free(del);

	if (pq->head == NULL)//若队头指针pq->head = NULL说明此时队列被删除成空队列,但是此时队尾指针pq->tail为野指针,则此时必须把野指针pq->tail设置成空指针。
		pq->head = pq->tail = NULL;

	size--;
}

//统计队列中存放数据的个数
int QueueSize(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	return size;
}

图形解析——整个工程中同时有两个队列进行插入和删除数据。

QueueSize函数统计每个队列存放的数据个数的正确思路:即在队列的结构体类型中struct Oueue增加一个成员变量size,而这个队列的结构体中的成员变量size统计的是队列中存放数据的个数

1. 队列的结构体类型中struct Oueue增加一个成员变量size来统计队列中存放数据的个数的好处是:因为当整个工程中有多个队列在同时进行插入和删除数据时,每个队列(结构体)都有自己独立的size统计各自队列中存放数据的个数。

下面是正确写法的QueueSize函数:

//队列的数据类型
typedef int QDataType;

//队列链表的结点类型
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

//队列的结构体类型
typedef struct Queue
{
	QNode* head;//指针head指向队列队头数据。
	QNode* tail;//指针tail指向队列队尾数据。
	int size;//统计队列存储数据的个数
}Queue;


//入队函数->尾插(插入数据)
//注意:由于在队列的所有功能函数中只有入队函数QueuePush才会增加队列的数据个数,
//而其他队列的功能函数都没有增加队列数据的个数,所以不需要单独写一个函数来创建队列链表的结点。
void QueuePush(Queue* pq, QDataType x)
{
	//判断指针pq是否是空指针
	assert(pq);
	//创建结点
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//对新创建结点的成员变量进行初始化
	newnode->data = x;
	newnode->next = NULL;

	//判断是否是头插
	if (pq->head == NULL)//或者写成if (pq->tail == NULL)
		pq->head = pq->tail = newnode;
	else//尾插
	{
		//把新创建的结点和队尾结点链接起来
		pq->tail->next = newnode;
		//让队尾指针指向新的队尾结点
		pq->tail = newnode;
	}
	pq->size++;
}


//出队函数->头删(删除数据)
void QueuePop(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);

	//判断队列是否是空队列
	assert(!QueueEmpty(pq));

	//头删的过程:
	//保存当前要删除的结点
	QNode* del = pq->head;
	//注意:必须先让队头指针指向新的队头结点后再删除del位置的结点。
	pq->head = pq->head->next;//换头
	//删除结点
	free(del);

	if (pq->head == NULL)//若队头指针pq->head = NULL说明此时队列被删除成空队列,但是此时队尾指针pq->tail为野指针,则此时必须把野指针pq->tail设置成空指针。
		pq->head = pq->tail = NULL;

	pq->size--;
}


//统计队列中存放数据的个数
int QueueSize(Queue* pq)
{
	//判断指针pq是否是空指针
	assert(pq);
	
	写法1:时间复杂度O(N)
	//int size = 0;
	//QNode* cur = pq->head;
	//while (cur)
	//{
	//	cur = cur->next;
	//	size++;
	//}

	//return size;

	//写法2:时间复杂度O(1)
	return pq->size;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值