数据结构 第3章(栈和队列)

1. 栈和队列的定义和特点

1.1 栈的定义和特点

  • 栈(stack):限定仅在表尾进行插入或删除操作的线性表
    • 栈顶(top):表尾端
    • 栈底(bottom):表头端
    • 空栈:不含元素的空表
    • 后进先出(Last In First Out,LIFO) 的线性表:退栈的第一个元素为栈顶元素

1.2 队列的定义和特点

  • 队列(queue)先进先出(First In First Out,FIFO) 的线性表
    • 只允许在表的一端进行插入,而在另一端删除元素
    • 队尾(rear):允许插入的一端
    • 队头(front):允许删除的一端

2. 栈的表示和操作的实现

2.1 顺序栈的表示和实现

  • 利用顺序存储结构实现的栈
  • 利用一组连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针 top 指示栈顶元素在顺序栈中的位置
#define MAXSIZE 5

typedef struct {
    SElemType *base;						// 栈底指针
    SElemType *top;							// 栈顶指针
    int stacksize;							// 栈可用的最大容量
} SqStack;

栈的示意图

2.1.1 初始化

  • 步骤
    • 为顺序栈动态分配一个最大容量为 MAXSIZE 的数组空间,使 base 指向这段空间的基地址
    • 栈顶指针 top 初始为 base ,表示栈为空
    • stacksize 置为栈的最大容量 MAXSIZE
void InitStack(SqStack* S) {
	S->base = (ABC*)malloc(MAXSIZE * sizeof(ABC));
    
	S->top = S->base;
    
	S->stacksize = MAXSIZE;
    
	printf("Init Success\n");
}

顺序栈的初始化

2.1.2 入栈

  • 步骤
    • 将新元素压入栈顶
    • 栈顶指针加 1
void Push(SqStack* S, ABC abc) {
	*S->top = abc;
	S->top++;
	printf("Push Success\n");
}

顺序栈的入栈

2.1.3 出栈

  • 步骤
    • 栈顶指针减 1
    • 栈顶元素出栈
void Pop(SqStack* S) {						// 没有释放栈顶空间
	S->top--;
	printf("Pop Success\n");
}

顺序栈的出栈

2.1.4 取栈顶元素

void GetTop(SqStack S) {
	ABC* abc = S.top - 1;
	printf("first = %d, second = %d\n", abc->first, abc->second);
	printf("Get Success\n");
}

测试代码

#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 5

void InitStack(SqStack);
void Push(SqStack, ABC);
void Pop(SqStack);
void GetTop(SqStack);

typedef struct {
	int first;
	int second;
} ABC;

typedef struct {
	ABC* top;
	ABC* base;
	int stacksize;
} SqStack;

int main() {
	SqStack S;
	ABC a;
	ABC b;
	ABC c;

	a.first = 0;
	a.second = 0;
	b.first = 1;
	b.second = 1;
	c.first = 2;
	c.second = 2;

	InitStack(&S);
	printf("****************\n");

	Push(&S, a);
	Push(&S, b);
	Push(&S, c);
	GetTop(S);
	printf("****************\n");

	Pop(&S);
	printf("first = %d\n", (S.top)->first);
	printf("****************\n");

	GetTop(S);
	printf("****************\n");
}

void InitStack(SqStack* S) {
	S->base = (ABC*)malloc(MAXSIZE * sizeof(ABC));
	S->top = S->base;
	S->stacksize = MAXSIZE;
	printf("Init Success\n");
}

void Push(SqStack* S, ABC abc) {
	*S->top = abc;
	S->top++;
	printf("Push Success\n");
}

void Pop(SqStack* S) {						// 没有释放栈顶空间
	S->top--;
	printf("Pop Success\n");
}

void GetTop(SqStack S) {
	ABC* abc = S.top - 1;
	printf("first = %d, second = %d\n", abc->first, abc->second);
	printf("Get Success\n");
}

2.2 链栈的表示和实现

  • 采用链式存储结构实现的栈
  • 通常链栈用单链表表示
typedef struct StackNode {
    ElemType data;
    struct StackNode *next;
} StackNode, *LinkStack;

链栈的示意图

  • 由于栈的主要作用是在栈顶插入和删除,以链表的头部作为栈顶作为方便,而且没有必要像单链表那样为了操作方便附加一个头结点

2.2.1 初始化

void InitStack(LinkStack* S) {
	*S = (LinkStack)malloc(sizeof(StackNode));
	(*S)->stacksize = 0;
	(*S)->next =  NULL;
	printf("Init Success\n");
}

2.2.2 入栈

  • 步骤
    • 为入栈元素 e 分配空间,用指针 p 指向
    • 将新结点数据域置为 e
    • 将新结点插入栈顶
    • 修改栈顶指针为 p
void Push(LinkStack* S, ABC abc) {
	LinkStack new = (LinkStack)malloc(sizeof(StackNode));
    
	new->abc = abc;
    
	new->next = (*S)->next;
    
	(*S)->next = new;
    
	(*S)->stacksize++;
	printf("Push Success\n");
}

链栈的入栈

2.2.3 出栈

  • 步骤
    • 将栈顶元素赋给 e
    • 临时保存栈顶元素的空间,以备释放
    • 修改栈顶指针,指向新的栈顶元素
    • 释放原栈顶元素的空间
void Pop(LinkStack* S) {
	StackNode* node = (*S)->next;
    
	(*S)->next = node->next;
    
	(*S)->stacksize--;
    
	free(node);
    
	printf("Pop Success\n");
}

链栈的出栈

2.2.4 取栈顶元素

void GetTop(LinkStack S) {
	ABC abc = S->next->abc;
	printf("first = %d, second = %d\n", abc.first, abc.second);
	printf("Get Success\n");
}

测试代码

#include <stdio.h>
#include <stdlib.h>

void InitStack(LinkStack);
void Push(LinkStack, ABC);
void Pop(LinkStack);
void GetTop(LinkStack);

typedef struct {
	int first;
	int second;
} ABC;

typedef struct StackNode{
	ABC abc;
	int stacksize;
	struct StackNode* next;
} StackNode, *LinkStack;

int main() {
	LinkStack S;
	ABC a;
	ABC b;
	ABC c;

	a.first = 0;
	a.second = 0;
	b.first = 1;
	b.second = 1;
	c.first = 2;
	c.second = 2;

	InitStack(&S);
	printf("****************\n");

	Push(&S, a);
	Push(&S, b);
	Push(&S, c);
	GetTop(S);
	printf("****************\n");

	Pop(&S);
	printf("****************\n");

	GetTop(S);
	printf("****************\n");
}

void InitStack(LinkStack* S) {
	*S = (LinkStack)malloc(sizeof(StackNode));
	(*S)->stacksize = 0;
	(*S)->next =  NULL;
	printf("Init Success\n");
}

void Push(LinkStack* S, ABC abc) {
	LinkStack new = (LinkStack)malloc(sizeof(StackNode));
	new->abc = abc;
	new->next = (*S)->next;
	(*S)->next = new;
	(*S)->stacksize++;
	printf("Push Success\n");
}

void Pop(LinkStack* S) {
	StackNode* node = (*S)->next;
	(*S)->next = node->next;
	(*S)->stacksize--;
	free(node);
	printf("Pop Success\n");
}

void GetTop(LinkStack S) {
	ABC abc = S->next->abc;
	printf("first = %d, second = %d\n", abc.first, abc.second);
	printf("Get Success\n");
}

3. 栈与递归

3.1 采用递归算法解决的问题

  • 递归:在一个函数、过程或者数据结构定义的内部又直接(或间接)出现定义本身的应用

3.1.1 定义是递归的

  • 阶乘函数
    F a c t ( n ) = { 1 n = 0 n ∗ F a c t ( n − 1 ) n > 0 Fact(n)=\begin{cases} 1 \quad\quad\quad\quad\quad\quad\quad\quad n=0 \\ n*Fact(n-1)\quad\quad n>0\end{cases} Fact(n)={1n=0nFact(n1)n>0

    int Fact(int i) {
        if (i == 0) {
            return 0;
        } else {
            return i*Fact(i-1);
        }
    }
    
  • 二阶 Fibonacci 数列
    F i b ( n ) = { 1 n = 1 或 n = 2 F i b ( n − 1 ) + F i b ( n − 2 ) 其 他 情 形 Fib(n)=\begin{cases} 1 \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad n=1或n=2 \\ Fib(n-1)+Fib(n-2)\quad\quad 其他情形\end{cases} Fib(n)={1n=1n=2Fib(n1)+Fib(n2)

    int Fib(int i) {
        if (i == 1 || i == 2) {
            return 1;
        } else {
            return Fib(n-1) + Fib(n-2);
        }
    }
    
  • 递归求解:将复杂的问题分解成几个相对简单且解法相同或相似的子问题来求解

  • 分治法:分解-求解的策略

    • 使用条件
      • 能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,并且这些处理对象更小且变化有规律
      • 可以通过上述转化而使问题简化
      • 必须有一个明确的递归出口,或递归的边界

3.1.2 数据结构是递归的

  • 链表是一种递归的数据结构

  • 遍历输出链表中各个结点的递归算法

    • 步骤
      • 如果 p 为 NULL ,递归结束返回
      • 否则输出 p->data ,p 指向后继结点继续递归
    void TraverseList(LinkList p) {
        if (p == NULL) {
            return 0;
        } else {
            printf("data = %d\n", p->data);
            TraverseList(p->next);
        }
    }
    
    // 可简化为
    
    void TraverseList(LinkList p) {
        if (p) {
            printf("data = %d\n", p->data);
            TraverseList(p->next);
        }
    }
    

3.1.3 问题的解法是递归的

  • Hanoi 塔问题、八皇后问题、迷宫问题等

  • Hanoi 塔问题的递归算法

    • 步骤
      • 如果 n=1 ,将 A 上编号为 1 至 n-1 的圆盘移到 B ,C 做辅助塔
      • 直接将编号为 n 的圆盘从 A 移到 C
      • 递归,将 B 上编号为 1 至 n-1 的圆盘移到 C ,A 做辅助塔
    #define count 0;
    
    void Hanoi(int n, char A, char B, char C) {
        if (n == 1) {
            move(A, 1, C);
        } else {
            Hanoi(n-1, A, C, B);
            move(A, n, C);
            Hanoi(n-1, B, A, C);
        }
    }
    
    void move(char A, int n, char C) {
        printf("%d, %d, %c, %c", ++count, n, A, C);
    }
    

3.2 递归过程与递归工作栈

  • 调用函数和被调用函数之间的链接及信息交换需通过栈来进行

  • 在一个函数的运行期间调用另一个函数时

    • 在运行被调用函数之前,系统需先完成 3 件事
      • 将所有的实参、返回地址等信息传递给被调用函数保存
      • 为被调用的局部变量分配存储区
      • 将控制转移到被调函数的入口
    • 从被调用函数返回调用函数之前,系统也应完成 3 件事
      • 保存被调函数的计算结果
      • 释放被调函数的数据区
      • 依照被调函数保存的返回地址将控制转移到调用函数
  • 当有多个函数构成嵌套调用时,按照 “后调用先返回” 的原则,上述函数之间的信息传递和控制转移必须通过 “栈” 来实现

    • 系统将整个系统运行时所需的数据空间安排在一个栈中
    • 每当调用一个函数时,就为它在栈顶分配一个存储区
    • 每当从一个函数退出时,就释放它的存储区
    • 当前正运行的函数的数据区必在栈顶
  • 为了保证递归函数正确执行,系统需设立一个 “递归工作栈” 作为整个递归函数运行期间使用的数据存储区

    • 每一层递归所需信息构成一个工作记录,其中包括所有的实参、所有的局部变量,以及上一层的返回地址

    • 每进入一层递归,就产生一个新的工作记录压入栈顶

    • 每退出一层递归,就从栈顶弹出一个工作记录

    • 当前执行层的工作记录必是递归工作栈栈顶的工作记录,该记录称为 “活动记录

3.3 递归算法的效率分析

3.3.1 时间复杂度的分析

  • 以 Fact(n) 为例
    T ( n ) = { O ( 1 ) n = 0 O ( 1 ) + T ( n − 1 ) n ≥ 1 T(n)=\begin{cases} O(1) \quad\quad\quad\quad\quad\quad\quad n=0 \\ O(1) + T(n-1)\quad\quad n\geq1\end{cases} T(n)={O(1)n=0O(1)+T(n1)n1
    设 n > 2 ,利用上式对 T(n-1) 展开
    T ( n − 1 ) = O ( 1 ) + T ( n − 2 ) T(n-1)=O(1)+T(n-2) T(n1)=O(1)+T(n2)
    带入 T(n) = O(1) + T(n-1) 中
    T ( n ) = 2 O ( 1 ) + T ( n − 2 ) T(n)=2O(1)+T(n-2) T(n)=2O(1)+T(n2)
    同理,n > 3 时有
    T ( n ) = 3 O ( 1 ) + T ( n − 3 ) T(n)=3O(1)+T(n-3) T(n)=3O(1)+T(n3)
    依次类推,n > i 时有
    T ( n ) = i O ( 1 ) + T ( n − i ) T(n)=iO(1)+T(n-i) T(n)=iO(1)+T(ni)
    当 i = n 时有
    T ( n ) = n O ( 1 ) + T ( 0 ) = ( n + 1 ) O ( 1 ) T(n)=nO(1)+T(0)=(n+1)O(1) T(n)=nO(1)+T(0)=(n+1)O(1)
    求得递归方程的解为: T(n) = O(n)

  • Fibonacci 数列问题的递归算法的时间复杂度均为 O(2^n)

  • Hanoi 塔问题的递归算法的时间复杂度均为 O(2^n)

3.3.2 空间复杂度的分析

  • 空间复杂度
    S ( n ) = O ( f ( n ) ) S(n)=O(f(n)) S(n)=O(f(n))
    f(n) 为 “递归工作栈” 中工作记录的个数与问题规模 n 的函数关系

  • 阶乘问题的递归算法的空间复杂度均为 O(n)

  • Fibonacci 数列问题的递归算法的空间复杂度为 O(n)

  • Hanoi 塔问题的递归算法的空间复杂度为 O(n)

3.4 利用栈将递归转换为非递归的方法

  • 步骤
    • 设置一个工作栈皴法递归工作记录(包括实参、返回地址及局部变量等)
    • 进入非递归调用入口(被调用程序开始处)将调用程序传来的实际参数和返回地址入栈
    • 进入递归调用入口
      • 当不满足递归结束条件时,逐层递归,将实参、返回地址及局部变量入栈(这个过程可以用循环语句来实现)
      • 当满足递归结束条件时,将到达递归出口的给定常数作为当前的函数值
    • 返回处理
      • 在栈不为空的情况下,反复退出栈顶记录,根据记录中的返回地址进行题意规定的操作
        • 逐层计算当前函数值,直至栈空为止

4. 队列的表示和操作的实现

在这里插入图片描述

4.1 循环队列—队列的顺序表示和实现

  • 在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需设置两个整型变量 front 和 rear 分别指示队列头元素及队列尾元素的位置
#define MAXSIZE 5

typedef struct {
    ElemType *elem;							// 存储空间的基地址
    int front;								// 头指针
    int rear;								// 尾指针
} SqQueue;
  • 初始化创建空队列时,令 front=rear=0

    顺序队列的初始化

  • 每当插入新的队列尾元素时,尾指针 rear 加 1

    顺序队列的入队

  • 每当删除队列头元素时,头指针 front 加 1

    顺序队列的出队

  • 溢出现象:数组越界,无法再插入新的队尾元素

  • “假溢出” 现象:此时的队列实际可用空间可能并未占满

    顺序队列的假溢出

    • 解决方法:将顺序队列变为一个闭环空间,形成循环队列
  • 循环队列:

    • 头、尾指针 “依环状加 1” 的操作可用 “模” 运算来实现

    • 判断队满和队空

      • 少用一个元素空间,队列空间大小为 n 时,有 n-1 个元素就认为是队满

        队空条件:Q.front == Q.rear;
        队满条件:(Q.rear + 1) % MAXSIZE == Q.front;
        
      • 设一个标志位以区别队列是 “空” 还是 “满”

4.1.1 初始化

  • 步骤
    • 为队列分配一个最大容量为 MAXSIZE 的数组空间,base 指向数组空间的首地址
    • 头指针和尾指针置零,表示队列为空
void InitQueue(SqQueue* Q) {
	Q->abc = (ABC *)malloc(MAXSIZE * sizeof(ABC));
	Q->front = Q->rear = 0;
	printf("Init Success\n");
}

4.1.2 求队列长度

void QueueLength(SqQueue Q) {
	printf("length = %d\n", (Q.rear - Q.front + MAXSIZE) % MAXSIZE);
}

4.1.3 入队

  • 步骤
    • 将新元素插入队尾
    • 队尾指针加 1
void EnQueue(SqQueue* Q, ABC abc) {
	if ((Q->rear + 1) % MAXSIZE == Q->front)
	{
		printf("En Error\n");
		return 0;
	}

	Q->abc[Q->rear] = abc;
	Q->rear = (Q->rear + 1) % MAXSIZE;
	printf("En Success\n");
}

循环队列的入队

4.1.4 出队

  • 步骤
    • 释放队头元素
    • 队头指针加 1
void DeQueue(SqQueue* Q) {				// 没有释放队头空间
	if (Q->front == Q->rear)
	{
		printf("De Error\n");
		return 0;
	}

	Q->front = (Q->front + 1) % MAXSIZE;
	printf("De Success\n");
}

4.1.5 取队头元素

void GetHead(SqQueue Q) {
	if (Q.front != Q.rear)
	{
		ABC abc = Q.abc[Q.front];
		printf("first = %d, second = %d\n", abc.first, abc.second);
		printf("Get Success\n");
		return 0;
	}

	printf("Get Error\n");
}

测试代码

#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 5

void InitQueue(SqQueue);
void QueueLength(SqQueue);
void EnQueue(SqQueue, ABC);
void DeQueue(SqQueue);
void GetHead(SqQueue);

typedef struct {
	int first;
	int second;
} ABC;

typedef struct {
	ABC* abc;
	int front;
	int rear;
} SqQueue;

int main() {
	SqQueue Q;
	ABC a;
	ABC b;
	ABC c;

	a.first = 0;
	a.second = 0;
	b.first = 1;
	b.second = 1;
	c.first = 2;
	c.second = 2;

	InitQueue(&Q);
	printf("****************\n");

	QueueLength(Q);
	printf("****************\n");

	EnQueue(&Q, a);
	EnQueue(&Q, b);
	EnQueue(&Q, c);

	printf("****************\n");

	DeQueue(&Q);
	printf("****************\n");

	GetHead(Q);
	printf("****************\n");
}

void InitQueue(SqQueue* Q) {
	Q->abc = (ABC*)malloc(MAXSIZE * sizeof(ABC));
	Q->front = Q->rear = 0;
	printf("Init Success\n");
}

void QueueLength(SqQueue Q) {
	printf("length = %d\n", (Q.rear - Q.front + MAXSIZE) % MAXSIZE);
}

void EnQueue(SqQueue* Q, ABC abc) {
	if ((Q->rear + 1) % MAXSIZE == Q->front)
	{
		printf("En Error\n");
		return 0;
	}

	Q->abc[Q->rear] = abc;
	Q->rear = (Q->rear + 1) % MAXSIZE;
	printf("En Success\n");
}

void DeQueue(SqQueue* Q) {
	if (Q->front == Q->rear)
	{
		printf("De Error\n");
		return 0;
	}

	Q->front = (Q->front + 1) % MAXSIZE;
	printf("De Success\n");
}

void GetHead(SqQueue Q) {
	if (Q.front != Q.rear)
	{
		ABC abc = Q.abc[Q.front];
		printf("first = %d, second = %d\n", abc.first, abc.second);
		printf("Get Success\n");
		return 0;
	}

	printf("Get Error\n");
}

4.2 链队—队列的链式表示和实现

  • 采用链式存储结构实现的队列
  • 通常链队用单链表来表示
typedef struct QNode {
    ElemType data;
    struct QNode *next;
} QNode, *QueuePtr;

typedef struct {
    QueuePtr front;							// 队头指针
    QueuePtr rear;							// 队尾指针
} LinkQueue;

链队列的示意图

4.2.1 初始化

  • 步骤
    • 生成新结点作为头结点,队头和队尾指针指向此结点
    • 头结点的指针域置空
void InitQueue(LinkQueue* Q) {
	QNode* node = (QNode *)malloc(sizeof(QNode));

	Q->front = node;
	Q->rear = node;
    node->next = NULL;
	printf("Init Success\n");
}

链队的初始化

4.2.2 入队

  • 步骤
    • 为入队元素分配结点空间,用指针 p 指向
    • 将新结点数据域置为 e
    • 将新结点插入到队尾
    • 修改队尾指针为 p
void EnQueue(LinkQueue* Q, ABC abc) {
	QNode* node = (QNode*)malloc(sizeof(QNode));

	node->abc = abc;
	node->next = NULL;
	Q->rear->next = node;
	Q->rear = node;
	printf("En Success\n");
}

链队的入队

4.2.3 出队

  • 步骤
    • 临时保存队头元素的空间,以备释放
    • 修改头结点的指针域,指向下一个结点
    • 判断出队元素是否为最后一个元素
      • 是:将队尾指针重新赋值,指向头结点
    • 释放原队头元素的空间
void DeQueue(LinkQueue* Q) {
	if (Q->front == Q->rear)
	{
		printf("De Error\n");
		return 0;
	}

	QNode* node = Q->front->next;
	Q->front->next = node->next;
	free(node);
	printf("De Success\n");
}

链队的出队

4.2.4 取队头元素

void GetHead(LinkQueue Q) {
	if (Q.front != Q.rear)
	{
		ABC abc = Q.front->next->abc;
		printf("first = %d, second = %d\n", abc.first, abc.second);
		printf("Get Success\n");
		return 0;
	}

	printf("Get Error\n");
}

测试代码

#include <stdio.h>
#include <stdlib.h>

void InitQueue(LinkQueue);
void EnQueue(LinkQueue, ABC);
void DeQueue(LinkQueue);
void GetHead(LinkQueue);

typedef struct {
	int first;
	int second;
} ABC;

typedef struct QNode {
	ABC abc;
	struct QNode* next;
} QNode, *QueuePtr;

typedef struct {
	QueuePtr front;							
	QueuePtr rear;							
} LinkQueue;

int main() {
	LinkQueue Q;
	ABC a;
	ABC b;
	ABC c;

	a.first = 0;
	a.second = 0;
	b.first = 1;
	b.second = 1;
	c.first = 2;
	c.second = 2;

	InitQueue(&Q);
	printf("****************\n");

	EnQueue(&Q, a);
	EnQueue(&Q, b);
	EnQueue(&Q, c);
	printf("****************\n");

	DeQueue(&Q);
	printf("****************\n");

	GetHead(Q);
	printf("****************\n");
}

void InitQueue(LinkQueue* Q) {
	QNode* node = (QNode*)malloc(sizeof(QNode));

	Q->front = node;
	Q->rear = node;
	node->next = NULL;
	printf("Init Success\n");
}

void EnQueue(LinkQueue* Q, ABC abc) {
	QNode* node = (QNode*)malloc(sizeof(QNode));

	node->abc = abc;
	node->next = NULL;
	Q->rear->next = node;
	Q->rear = node;
	printf("En Success\n");
}

void DeQueue(LinkQueue* Q) {
	if (Q->front == Q->rear)
	{
		printf("De Error\n");
		return 0;
	}

	QNode* node = Q->front->next;
	Q->front->next = node->next;
	free(node);
	printf("De Success\n");
}

void GetHead(LinkQueue Q) {
	if (Q.front != Q.rear)
	{
		ABC abc = Q.front->next->abc;
		printf("first = %d, second = %d\n", abc.first, abc.second);
		printf("Get Success\n");
		return 0;
	}

	printf("Get Error\n");
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值