【数据结构】堆-C语言版

目录

堆的概念及结构

堆的性质

堆的实现

向下调整算法

构造小根堆

建堆的时间复杂度:

堆的排序

堆排序时间复杂度

堆的插入

堆的删除 

堆面试题

堆的实现 

堆的应用


堆的概念及结构

 如果有一个关键码的集合K = ,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:(或),i = 0,1,2,······,则称为小堆或大堆。

小根堆:父亲小于等于孩子。

大根堆:父亲大于等于孩子。

堆的性质:


(1)堆中某个结点的值总是不大于或不小于其父结点的值;
(2)堆总是一棵完全二叉树。

大根堆可以用来选择最大的数,小根堆可以用来选择最小的数。但大根堆和小根堆不能用来排序(从上图小根堆的排序结构可以看出)。

对堆中的结点进行编号:

由于堆是完全二叉树,物理存储结构就像数组,在堆中,如果已知父亲的下标是parent,则左右孩子的下标为:

leftchild = parent*2+1

rightchild = parent*2+2

如果已知孩子的下标,则父亲下标为:

parent = (child-1)/2

堆的实现

在进行堆的构造和堆排序之前需要了解向下调整算法:

向下调整算法:

如果一棵二叉树的左子树和右子树恰好都是小(大)堆,但只有根结点不满足小(大)堆的定义,如何把这棵二叉树调成小(大)堆呢?需要使用向下调整算法调成小(大)堆

1.选出左右孩子中的小(大)孩子

2.小(大)孩子跟父亲比:

   a.如果小孩子比父亲小(大),则让小(大)孩子跟父亲交换,并把原来孩子的位置当成父亲继续往下调整,直到走到叶子结点。

   b.如果小孩子比父亲大(小),则不需要处理,调整完成,整棵树已经是最小(大)堆。

如下面这棵二叉树,蓝色框内左右子树都是小根堆,但是堆顶27和左右孩子15、19并不构成小根堆,使用向下调整算法进行调整:

第1步:选出左右孩子中的小孩子,15和19相比,15小,15比父亲27小,15和27交换

第2步:把原来孩子的位置也就是现在27的位置当成父亲继续往下调整

 第3步:选出左右孩子中的小孩子,18和28相比,18小,18比父亲27小,18和27交换

第4步:把原来孩子的位置也就是现在27的位置当成父亲继续往下调整

 第5步:选出左右孩子中的小孩子,49和25相比,25小,25比父亲27小,25和27交换

 第6步:把原来孩子的位置也就是现在27的位置当成父亲继续往下调整,发现27已经是叶子结点了,调整完毕。

如果左子树或右子树不是小根堆呢?就不能使用向下调整算法了吗?答案是需要先把左右子树构造成小根堆,再使用向下调整算法,最后整棵树调成小根堆。同理,如果左子树或右子树不是大根堆,需要先把左右子树构造成大根堆,再使用向下调整算法,最后整棵树调成大根堆。

构造小根堆:

从倒数第一个非叶子结点开始,从后往前,按编号,依次作为子树向下调整。

第1步:倒数第一个非叶子结点的下标为(n-1-1)/2,即(7-1-1)/2=3,从倒数第一个非叶子结点编号为3的结点5开始作为子树向下调整,5只有一个孩子4,4比5小,交换4和5

第2步:得到如下二叉树,对前一个编号2的位置即结点3作为子树向下调整,3的两个孩子1和7,1小,交换3和1

 第3步:得到如下二叉树,对前一个编号1的位置即结点8作为子树向下调整,8的两个孩子5和2,2小,交换8和2

第4步: 得到如下二叉树,对前一个编号0的位置即结点6作为子树向下调整,6的两个孩子8和1,1小,交换6和1

 第5步: 得到如下二叉树,现在以结点6为根结点的子树已经不符合小根堆的定义了,需要向下向下调整,6的两个孩子3和7,3小,交换6和3

此时,二叉树已经是小根堆了,调整完毕

 建堆代码:

#define  _CRT_SECURE_NO_WARNINGS  1
void Swap(int *p1, int *p2) 
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整算法
AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child <n)
	{
		//选出左右孩子中的小孩子
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child++;
		}
		//小孩子比父亲小,交换小孩子和父亲,可能导致不满足堆的定义,需要调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		//小孩子比父亲大,跳出while循环,结束这一次的向下调整,否则一直死循环并且什么都不做
		else 
		{
			break;
		}
	}
}
int main()
{
	int a[] = { 6,8,5,9,3,7,2 };
	//int a1[] = { 1,2,3,4,5,6,7 };
	int n = sizeof(a) / sizeof(a[0]);
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	return 0;
}

建堆的时间复杂度:

假设树的高度为h,则每一层最多有2^(h-1)个结点 

需要移动的结点的总数T(n)为:

堆的排序:

1.构建堆之后,就可以对数组进行排序了,堆顶元素在整个堆里是最小值。如果要将数组升序排列,小的在前,大的在后,需要建立大堆还是小堆呢?

需要建立大堆,因为如上图构建小根堆的结果,尽管1是堆顶元素,同时也是整个堆中最小的元素,但是整棵树不是按结点编号依次递增的,而顺序是{1,2,3,4,8,6,7,5},因此小堆并不能对数组进行升序排列。所以,如果要将数组进行升序排列,要用大堆;同理,如果要将数组进行倒序排列,要用小堆。

2.为什么要使用大堆对数组进行升序排列?

(1)如果使用小堆对数组进行升序排列,那么对于数组,要选出最小的数,需要花费O(N)的时间,接着再选出次小的数,又需要花费O(N)的时间,那么数组排序的时间复杂度为O(N^2),效率低。

(2)如果使用小堆输出堆顶第一个元素,那么剩下的元素父子关系有可能全乱了,又需要重新建堆,效率太低。

 3.如何使用大堆对数组进行排序?

使用堆排序的目的是为了保持父子间的关系不要变,父亲永远是父亲,孩子永远是孩子,这样就不需要重建堆,最多只需要调整。

构造大根堆之后,堆顶元素是整个数组中的最大值,将堆顶元素与最后一个叶子结点进行交换,那么数组最后一个元素就是有序的;再将堆进行向上调整,堆顶元素为次大元素,将堆顶元素与倒数第二个叶子节点进行交换,数组最后两个元素是有序的,······直到堆输出最后一个元素。

如数组{6,8,5,9,3,7,2}要进行升序排序,先进行堆的构造,再进行堆排序

(1)大根堆的构造 

第1步:倒数第一个非叶子结点的下标为(n-1-1)/2,即(5-1-1)/2=2,从倒数第一个非叶子结点编号为2的结点5开始作为子树向下调整,5的两个孩子7和2,7大,交换5和7

 第2步:得到如下二叉树,对前一个编号1的位置即结点8作为子树向下调整,8的两个孩子9和3,9大,交换8和9

 第3步:得到如下二叉树,对前一个编号0的位置即结点6进行向下调整,6的两个孩子9和3,9大,交换8和9

 第4步:得到如下二叉树,此时以6位根节点的子树不满足大堆定义,需要进行向下调整,6的两个孩子8和3,8大,6和8进行交换。

  第4步:得到如下二叉树,此时整棵树已经是大根堆了,调整完毕。

(2)堆的排序 

第1步:将堆顶元素9与最后一个叶子结点2进行交换,这时9作为堆中最大的元素在数组最后,有序。但交换之后,2作为根结点,树已经不满足大堆的定义了,需要进行向下调整,2的两个孩子8和7,8大,交换2和8(9已经是有序的数组元素了,无需参与堆调整,以下同理)

 得到如下二叉树,2作为根结点,还不满足大堆的定义,需要向下调整,2的两个孩子6和3,6大,交换2和6

 第2步:得到如下大根堆

 将堆顶元素8和倒数第一个叶子结点5进行交换,5作为根结点,树已经不满足大堆的定义了,需要进行向下调整,5的两个孩子8和7,7大,交换5和7

第3步: 得到如下大根堆,

 将堆顶元素7和倒数第一个叶子结点3进行交换,3作为根结点,树已经不满足大堆的定义了,需要进行向下调整,3的两个孩子6和5,6大,交换3和6

第4步:得到如下大根堆 ,

 将堆顶元素6和倒数第一个叶子结点2进行交换,2作为根结点,树已经不满足大堆的定义了,需要进行向下调整,2的两个孩子3和5,5大,交换2和5

 第5步:得到如下大根堆

 将堆顶元素5和倒数第一个叶子结点2进行交换,2作为根结点,树已经不满足大堆的定义了,需要进行向下调整,2只有一个孩子3,3大,交换2和3

 第6步:得到如下大根堆

将堆顶元素3和倒数第一个叶子结点2进行交换

 第7步: 得到如下堆,堆里只有一个元素2,满足大根堆定义,不需要调整,将堆顶元素2和最后一个叶子结点2进行交换

得到最终的二叉树为

 此时的二叉树为数组的升序排列的逻辑结构,数组的物理结构为:

堆排序时间复杂度:

把第1个结点挪到正确的位置需要log(n)

把第2个结点挪到正确的位置需要log(n)

······

把第n个结点挪到正确的位置需要log(n)

因此堆排序的时间复杂度为nlog(n)

堆的插入:

(1)先将元素插入到堆的末尾,即最后一个叶结点之后

(2)如果堆的性质遭到破坏,将新插入节点,顺着其双亲向上调整到合适位置

堆的删除: 

(1)将堆顶元素与堆中最后一个元素进行交换

(2)删除堆中最后一个元素

(3)将堆顶元素向下调整到满足堆的定义即可

堆面试题:

1.下列关键字序列为堆的是:()
A 100,60,70,50,32,65
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32

答案为A,分析如下:

2.已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间
的比较次数是()。
A 1
B 2
C 3
D 4

答案为C,分析如下:

3.一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为
A(11 5 7 2 3 17)
B(11 5 7 2 17 3)
C(17 11 7 2 3 5)
D(17 11 7 5 3 2)
E(17 7 11 3 5 2)
F(17 7 11 3 2 5)

答案为C,分析如下:根据选项可以看出想要建立大根堆,数组要排升序

4.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A[3,2,5,7,4,6,8]
B[2,3,5,7,4,6,8]
C[2,3,4,5,7,8,6]
D[2,3,4,5,6,7,8]

答案为C,分析如下:

堆的实现:

045-heap.h

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


typedef int HPDataType;
struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
};

typedef struct Heap HP;

//交换
void Swap(int* p1, int* p2);

//向下调整
void AdjustDown(int* a, int n, int parent);

//向上调整
void AdjustUp(int* a, int child);

//初始化
void HeapInit(HP* php, HPDataType* a, int n);

//销毁
void HeapDestroy(HP* php);

//堆的插入
void HeapPush(HP* php, HPDataType x);

//堆的删除
void HeapPop(HP* php);

//返回堆顶元素
HPDataType HeapTop(HP* php);

//堆的大小
int HeapSize(HP* php);

//判空
bool HeapEmpty(HP* php);

//打印
void HeapPrint(HP* php);
#define  _CRT_SECURE_NO_WARNINGS  1
#include "045-Heap.h"

//打印
void HeapPrint(HP* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");

	int num = 0;
	int levelSize = 1;
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
		num++;
		if (num == levelSize)
		{
			printf("\n");
			levelSize *= 2;
			num = 0;
			
		}
		
	}

	printf("\n\n");
}

//交换
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出左右孩子中的小孩子,建大堆就把第二个<改成>
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child++;
		}
		//小孩子比父亲小,交换小孩子和父亲,建大堆就把<改成>
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//交换父亲和孩子后,可能导致不满足堆的定义,需要继续调整
			parent = child;
			child = parent * 2 + 1;
		}
		//小孩子比父亲大,跳出while循环,结束这一次的向下调整,否则一直死循环并且什么都不做
		else
		{
			break;
		}
	}
}

//向上调整
void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//初始化
void HeapInit(HP* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->size = n;
	php->capacity = n;

	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}

//销毁
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

//堆的插入
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//满了则需要增容
	if (php->size == php->capacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * 2 * php->capacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	
	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}

//堆的删除
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	//将堆顶元素和最后一个叶子结点进行交换
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	//对堆顶元素进行向下调整
	AdjustDown(php->a, php->size, 0);

}

//返回堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

//堆的大小
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

//堆判空
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

045-test.c

#define  _CRT_SECURE_NO_WARNINGS  1
#include "045-Heap.h"

int main()
{
	int a1[] = { 15,18,28,34,65,19,49,25,37,27 };
	int n = sizeof(a1) / sizeof(a1[0]);

	HP hp;
	HeapInit(&hp, a1, n);
	HeapPrint(&hp);

	HeapPush(&hp, 8);
	HeapPrint(&hp);

	HeapPush(&hp, 88);
	HeapPrint(&hp);

	HeapDestroy(&hp);

	return 0;
}

运行结果如下图所示: 

TOP k问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
(1)用数据集合中前K个元素来建堆
         求前k个最大的元素,则把前k个数建小堆
         求前k个最小的元素,则把前k个数建大堆
(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

         如果建了小堆,剩下N-K个元素和堆顶元素比较,若比堆顶元素大,则替换堆顶元素,调堆             如果建了大堆,剩下N-K个元素和堆顶元素比较,若比堆顶元素小,则替换堆顶元素,调堆

 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

总结:

建堆:向下调整算法

堆的删除:向下调整

堆的插入:向上调整

堆的应用

最小的k个数   OJ链接

思路一:将堆的实现的代码拷贝过来,建小堆,将前k个元素保存到一个数组中输出

typedef int HPDataType;
struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
};

typedef struct Heap HP;

//交换
void Swap(int* p1, int* p2);

//向下调整
void AdjustDown(int* a, int n, int parent);

//向上调整
void AdjustUp(int* a, int child);

//初始化
void HeapInit(HP* php, HPDataType* a, int n);

//销毁
void HeapDestroy(HP* php);

//堆的插入
void HeapPush(HP* php, HPDataType x);

//堆的删除
void HeapPop(HP* php);

//返回堆顶元素
HPDataType HeapTop(HP* php);

//堆的大小
int HeapSize(HP* php);

//判空
bool HeapEmpty(HP* php);

//打印
void HeapPrint(HP* php);

//打印
void HeapPrint(HP* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");

	int num = 0;
	int levelSize = 1;
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
		num++;
		if (num == levelSize)
		{
			printf("\n");
			levelSize *= 2;
			num = 0;
			
		}
		
	}

	printf("\n\n");
}

//交换
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出左右孩子中的小孩子,建大堆就把第二个<改成>
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child++;
		}
		//小孩子比父亲小,交换小孩子和父亲,建大堆就把<改成>
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//交换父亲和孩子后,可能导致不满足堆的定义,需要继续调整
			parent = child;
			child = parent * 2 + 1;
		}
		//小孩子比父亲大,跳出while循环,结束这一次的向下调整,否则一直死循环并且什么都不做
		else
		{
			break;
		}
	}
}

//向上调整
void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//初始化
void HeapInit(HP* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	memcpy(php->a, a, sizeof(HPDataType) * n);
	php->size = n;
	php->capacity = n;

	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}
}

//销毁
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

//堆的插入
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//满了则需要增容
	if (php->size == php->capacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * 2 * php->capacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	
	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}

//堆的删除
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	//将堆顶元素和最后一个叶子结点进行交换
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	//对堆顶元素进行向下调整
	AdjustDown(php->a, php->size, 0);

}

//返回堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

//堆的大小
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

//堆判空
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize){
    HP hp;
    HeapInit(&hp,arr,arrSize);

    int *retArr = (int*)malloc(sizeof(int)*k);
    for(int i = 0;i<k;i++)
    {
        retArr[i] = HeapTop(&hp);
        HeapPop(&hp);
    }

    HeapDestroy(&hp);
    *returnSize = k;
    return retArr;
}

思路二:求最小的k个数,即Top k问题,用前k个元素建大堆,用剩余的N-k个元素依次和堆顶元素比较,如果比堆顶元素小,则将该元素和堆顶元素进行交换,调堆。最后堆里的元素就是最小的k个数

//交换
void Swap(int* p1, int* p2)
{
 	int tmp = *p1;
 	*p1 = *p2;
 	*p2 = tmp;
}

//向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
    {
 		//选出左右孩子中的小孩子,建大堆就把第二个<改成>
 		if (child + 1 < n && a[child + 1] > a[child])
 		{
 			child++;
 		}
 		//小孩子比父亲小,交换小孩子和父亲,建大堆就把<改成>
 		if (a[child] > a[parent])
 		{
 			Swap(&a[child], &a[parent]);
 			//交换父亲和孩子后,可能导致不满足堆的定义,需要继续调整
			parent = child;
 			child = parent * 2 + 1;
 		}
 		//小孩子比父亲大,跳出while循环,结束这一次的向下调整,否则一直死循环并且什么都不做
 		else
 		{
 			break;
 		}
 	}
}

int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize){
    
    if(k == 0)
    {
        *returnSize = 0;
        return NULL;
    }
    int *retArr = (int*)malloc(sizeof(int)*k);
   
    //取出前k个数
    for(int i=0;i<k;i++)
    {
        retArr[i] = arr[i];
    }

    //对前k个数向下调整建大堆
    for(int j = (k-1-1)/2;j>=0;j--)
    {
        AdjustDown(retArr,k,j);
    }

    //剩下的arrSize - k个数,如果比堆顶的元素小,就将该元素和堆顶元素进行交换,调堆
    for(int m =  k;m<arrSize;m++)
    {
        if(arr[m] < retArr[0])
        {
            retArr[0] = arr[m];
            AdjustDown(retArr,k,0);
        }
    }

    *returnSize = k;
    return retArr;
}

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值