讲解栈和队列的实现(C语言)

1.栈

1.1 栈的概念

从定义上来说,栈是一种只允许在一端进行插入和删除操作的线性表。其中,仅允许进行插入和删除操作的那一端被称为栈顶,不允许进行插入和删除操作的那一端则被称为栈底。此外,向栈中插入新元素的操作被称为进栈,删除栈顶元素的操作则被称为出栈

因此,当要执行出栈操作时,被删除的栈顶元素将会是最后进栈的那个元素,我们称栈的这个特点为“后进先出”。

如果觉得抽象,我们还可以把栈和现实生活中的一些事物联系起来,例如手枪弹夹。把手枪弹夹看作栈,那么里面的每一颗子弹就都是一个元素,压入一颗子弹就是进栈一个元素,取出一颗子弹就是出栈一个元素。显然,越往后面压进去的子弹就越快被打出来,这和栈的“后进先出”特点是一致的。

1.2 栈的构造

1.2.1 使用数组实现

要使用数组实现栈的构造,我们既可以用空间大小固定的数组,也可以用动态分配空间的数组,这两者中动态分配空间的数组更加灵活,这里我们将会以动态分配空间的数组为例。

1.2.1.1 栈的结构体定义和栈的初始化

要使用数组实现栈的基本功能,我们需要知道栈的空间大小,并且定义一个整型变量充当栈顶指针,表示栈顶元素在数组中的下标。

据此定义栈的结构体如下:

#define SIZE_INIT 50	// 定义栈的初始空间大小


// 定义栈的结构体
struct Stack {
	int* stack;		// 栈数组的指针变量
	int top;		// 栈顶指针
	int size;		// 栈当前的空间大小
};

接下来我们要编写初始化栈的函数,在这个函数中我们将初始化栈的空间大小,同时初始化栈顶指针为-1表示栈为空,并为栈动态分配好空间。

初始化栈的函数如下:

// 初始化栈
void init_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 初始化栈的空间大小
	s->size = SIZE_INIT;
	// 初始化栈顶指针
	s->top = -1;
	// 初始化栈数组
	s->stack = (int*)malloc(sizeof(int) * s->size);
	if (s->stack == NULL) {
		printf("动态分配空间失败!\n");
		exit(1);
	}
}
1.2.1.2 判断栈满和栈空

要判断栈是否已满或者已空,我们都可以借助栈顶指针——即栈顶元素在数组中的下标来判断。若栈顶元素在数组中的下标取到了最大值,也就是数组的最大空间大小减一,此时栈就是满的;若栈顶元素在数组中的下标是-1,则表明栈中没有任何元素,此时栈就是空的。

据此编写代码如下:

// 判断栈是否已满
bool full(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 当栈顶元素在数组中的下标为数组的最后一个下标时表示栈满
	return s->top == s->size - 1;
}

// 判断栈是否为空
bool empty(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 当栈顶元素在数组中的下标为-1时,表示栈中没有元素
	return s->top == -1;
}
1.2.1.3 进栈

要实现元素进栈功能,我们不能一上来就直接把这个元素进栈,因为栈的空间是有限的,我们需要在进栈之前先判断栈是否已满,以防止出现数组越界访问的风险。

如果栈已满,那么我们需要重新为栈分配空间,然后再执行元素进栈操作。但是,我们并不能保证为栈重新分配足够空间这一步是成功的(可能由内存不足等原因导致),所以我们为实现进栈功能的函数设置了bool类型的返回值,若空间足够或者分配空间成功,那么执行完元素进栈操作后就会返回true,而若空间不足且分配空间失败,则直接返回false表示进栈失败。

注意,因为我们在重新分配空间时调用的是realloc函数,当没有足够的内存空间可供使用时,realloc函数会直接返回NULL指针,但原内存是不会改变的,所以我们需要保留原指针,而用一个临时指针变量tmp去接收realloc函数的返回值,以防止出现内存泄漏。关于realloc函数的具体用法读者可以自行去查找资料,这里不作详细说明。

实现进栈功能的函数代码如下:

// 进栈,返回值为true或false,分别表示进栈成功以及失败
bool push(struct Stack* s, int num) {
	// 参数s为栈的结构体的地址,num为待进栈的元素
	
	// 栈已满,需要重新分配空间
	if (full(s)) {
		// 更新栈的空间大小
		s->size += SIZE_INIT;
		// 重新分配空间
		int* tmp = (int*)realloc(s->stack, s->size);
		if (tmp == NULL) {
			return false;
		}
		s->stack = tmp;
	}
	// 栈没满,将元素进栈
	s->stack[++(s->top)] = num;
	return true;
}
1.2.1.4 出栈

和进栈类似,我们在进行元素出栈之前,要先判断栈是否为空。若栈为空,则直接返回false表示出栈失败,反之则继续执行元素出栈操作。

而因为C语言函数最多只能有一个返回值,所以要获取出栈的元素,我们需要将函数外的变量的地址作为参数传入函数,然后在函数内部通过解引用直接访问函数外部变量的存储空间,以达到将出栈的元素赋值给函数外部变量的目的。

实现出栈功能的函数代码如下:

// 出栈,返回值为true或false,分别表示出栈成功以及失败
bool pop(struct Stack* s, int* num) {
	// 参数s为栈的结构体的地址,num为指向存储出栈元素变量的指针

	// 栈已空,返回false表示出栈失败
	if (empty(s)) {
		return false;
	}
	// 栈不为空,将元素出栈
	*num = s->stack[(s->top)--];
	return true;
}
1.2.1.5 打印栈中所有元素

依次打印从栈底到栈顶的所有元素,函数代码如下:

// 打印栈中所有元素
void display(struct Stack* s) {
	// 参数s为栈的结构体的地址

	printf("从栈底到栈顶的元素依次为:");
	for (int i = 0; i <= s->top; i++) {
		printf("%d ", s->stack[i]);
	}
	printf("\n\n");
}
1.2.1.6 释放栈的空间

这一步也没什么好说的,就是把动态分配的数组空间释放掉,然后更新栈的结构体的成员变量值。

释放栈的空间的函数代码如下:

// 释放空间
void free_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 释放动态分配的数组空间
	free(s->stack);
	s->stack = NULL;
	s->top = -1;
	s->size = 0;
}
1.2.1.7 完整代码

以上就是使用数组实现的栈,当然如果大家还有其他功能,也可以编写对应的函数,这里就不再提供了。下面是完整的代码:

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

#define SIZE_INIT 50	// 定义栈的初始空间大小


// 函数声明
void init_stack(struct Stack* s);
bool full(struct Stack* s);
bool empty(struct Stack* s);
bool push(struct Stack* s, int num);
bool pop(struct Stack* s, int* num);
void display(struct Stack* s);
void free_stack(struct Stack* s);
void menu();

// 定义栈的结构体
struct Stack {
	int* stack;		// 栈数组的指针变量
	int top;		// 栈顶指针
	int size;		// 栈当前的空间大小
};

// 初始化栈
void init_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 初始化栈的空间大小
	s->size = SIZE_INIT;
	// 初始化栈顶指针
	s->top = -1;
	// 初始化栈数组
	s->stack = (int*)malloc(sizeof(int) * s->size);
	if (s->stack == NULL) {
		printf("动态分配空间失败!\n");
		exit(1);
	}
}

// 判断栈是否已满
bool full(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 当栈顶元素在数组中的下标为数组的最后一个下标时表示栈满
	return s->top == s->size - 1;
}

// 判断栈是否为空
bool empty(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 当栈顶元素在数组中的下标为-1时,表示栈中没有元素
	return s->top == -1;
}

// 进栈,返回值为true或false,分别表示进栈成功以及失败
bool push(struct Stack* s, int num) {
	// 参数s为栈的结构体的地址,num为待进栈的元素
	
	// 栈已满,需要重新分配空间
	if (full(s)) {
		// 更新栈的空间大小
		s->size += SIZE_INIT;
		// 重新分配空间
		int* tmp = (int*)realloc(s->stack, s->size);
		if (tmp == NULL) {
			return false;
		}
		s->stack = tmp;
	}
	// 栈没满,将元素进栈
	s->stack[++(s->top)] = num;
	return true;
}

// 出栈,返回值为true或false,分别表示出栈成功以及失败
bool pop(struct Stack* s, int* num) {
	// 参数s为栈的结构体的地址,num为指向存储出栈元素变量的指针

	// 栈已空,返回false表示出栈失败
	if (empty(s)) {
		return false;
	}
	// 栈不为空,将元素出栈
	*num = s->stack[(s->top)--];
	return true;
}

// 打印栈中所有元素
void display(struct Stack* s) {
	// 参数s为栈的结构体的地址

	printf("从栈底到栈顶的元素依次为:");
	for (int i = 0; i <= s->top; i++) {
		printf("%d ", s->stack[i]);
	}
	printf("\n\n");
}

// 释放空间
void free_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 释放动态分配的数组空间
	free(s->stack);
	s->stack = NULL;
	s->top = -1;
	s->size = 0;
}

// 打印菜单
void menu() {
	printf("1.进栈\n");
	printf("2.出栈\n");
	printf("3.判断栈是否已满\n");
	printf("4.判断栈是否为空\n");
	printf("5.打印栈中所有元素\n");
	printf("6.结束程序\n");
	printf("请输入你的选择:");
}

// 主函数
int main() {
	// 定义栈的结构体变量
	struct Stack s;
	// 初始化栈
	init_stack(&s);

	// 存储用户选择
	int choice = 0;
	// 存储进栈或出栈元素
	int num = 0;
	// 存储进栈或出栈的结果
	bool result = false;
	// 进入主循环
	while (1) {
		// 打印菜单
		menu();
		scanf_s("%d", &choice);
		if (choice == 1) {
			// 元素进栈操作
			printf("输入要进栈的元素:");
			scanf_s("%d", &num);
			result = push(&s, num);
			if (result) {
				printf("元素进栈成功!\n\n");
			}
			else {
				printf("元素进栈失败!\n\n");
			}
		}
		else if (choice == 2) {
			// 元素出栈操作
			result = pop(&s, &num);
			if (result) {
				printf("元素出栈成功!\n\n");
			}
			else {
				printf("元素出栈失败!\n\n");
			}
		}
		else if (choice == 3) {
			// 判断栈是否已满
			if (full(&s)) {
				printf("栈已满!\n\n");
			}
			else {
				printf("栈还没满!\n\n");
			}
		}
		else if (choice == 4) {
			// 判断栈是否为空
			if (empty(&s)) {
				printf("栈为空!\n\n");
			}
			else {
				printf("栈还没空!\n\n");
			}
		}
		else if (choice == 5) {
			// 打印栈中所有元素
			display(&s);
		}
		else if (choice == 6) {
			// 结束程序
			printf("准备结束程序,正在释放栈的空间......\n");
			break;
		}
		else {
			printf("无效输入!\n\n");
		}
	}

	// 释放栈空间
	free_stack(&s);
	printf("空间释放完毕!\n");

	return 0;
}

1.2.2 使用链表实现

要使用链表实现栈的构造,我们也有两种链表可以选择,一种是单链表,另一种则是双链表。其中单链表的每一个结点只包含元素值和后结点指针,双链表则在单链表的前提下增加了前结点指针。

与双链表相比起来,要实现一样的栈的构造,单链表的空间利用率显然更高,因此这里我们将会以使用单链表为例实现栈的构造。

1.2.2.1 栈的结构体定义、链表结点的结构体定义和栈的初始化

要使用单链表实现栈的构造,我们需要定义两种结构体,一种描述栈,另一种描述链表中的每一个结点。

其中,描述栈的结构体至少需要包含链表头结点的指针,在此基础上这里还加上了栈当前的元素个数。而描述链表中的每一个结点的结构体则至少需要包含当前结点的元素值以及后结点指针,这里便是如此定义。

据此定义两种结构体如下:

// 定义栈的结构体
struct Stack {
	struct Node* head;		// 链表头结点指针
	int size;				// 栈当前的元素个数
};

// 定义栈链表的结点结构体
struct Node {
	int num;				// 结点中的元素值
	struct Node* next;		// 当前结点的后结点指针
};

在初始化栈时,我们需要为头结点指针动态分配空间,并且为栈结构体中的size变量以及头结点中的两个变量都作初始化赋值。因为栈初始化时为空,即头结点没有后结点,且头结点不存储元素,所以这里将size和头结点的num都赋值为0,将头结点的next赋值为NULL。

初始化栈的函数代码如下:

// 初始化栈
void init_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 初始化栈的当前元素个数
	s->size = 0;
	// 初始化链表头结点
	s->head = (struct Node*)malloc(sizeof(struct Node));
	if (s->head == NULL) {
		printf("动态分配空间失败!\n");
		exit(1);
	}
	// 初始化头结点的元素值和后结点指针
	s->head->num = 0;
	s->head->next = NULL;
}
1.2.2.2 判断栈空

和用数组实现不同,用链表实现的栈不需要判断栈满,这是因为链表每一个结点都是即用即分配空间,而不是像数组那样一次性分配一定数目的空间。因此对于链表实现的栈来说,无论栈中有多少元素,都需要为进栈元素分配空间,所以每次进栈其实都可以看作栈满。

所以,我们这里只需要讨论栈是否为空。前面定义栈的时候,我们定义了一个变量size存储当前栈中的元素个数,这里判断栈是否为空我们就可以借助这个变量进行判断,当size为0时就表示栈为空。

判断栈是否为空的函数代码如下:

// 判断栈是否为空
bool empty(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 当栈顶元素在数组中的下标为-1时,表示栈中没有元素
	return s->size == 0;
}
1.2.2.3 进栈

我们平时往链表中新增一个结点,通常都是添加在链表的末端。但由于栈“后进先出”的特点,如果每次都把新结点添加在链表末端,那么每次出栈的时候我们都需要访问链表的尾结点。

若使用双链表,这当然是没有问题的,因为双链表不仅需要存储头结点指针,还需要存储尾结点指针,我们可以很方便的访问尾结点,并且可以从尾结点依次往前访问前面的所有结点。但若使用单链表,因为单链表往往只存储头结点(存储尾结点用处不大,因为单链表只能从前往后访问,即使知道尾结点,也不能从尾结点依次往前遍历各个结点),所以我们每次出栈都需要从前往后遍历一次整个链表,算法的效率就会变得很低。

为了解决这个问题,我们每次进栈的时候,都会使用头插法将新结点加入到链表中。所谓头插法,就是每次插入新结点时,将新结点插入到头结点的后结点位置,原本的后结点则变为新结点的后结点,其他结点则不需要进行任何操作。

以图1为例,假设我们需要将新结点New插入到链表中:

 图1

按照图1所示的进栈方法,越后面进栈的结点反而越接近头结点,且头结点的后结点存储的元素总是当前栈所有元素中最迟进栈的那个,这样的话每次出栈我们只需要访问头结点的后结点即可,算法的效率将大大提升。

据此编写进栈函数的代码如下:

// 进栈,返回值为true或false,分别表示进栈成功以及失败
bool push(struct Stack* s, int num) {
	// 参数s为栈的结构体的地址,num为待进栈的元素
	
	// 为新元素开辟一个结点的空间
	struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
	if (new_node == NULL) {
		return false;
	}
	new_node->num = num;
	// 将新结点插入链表
	new_node->next = s->head->next;
	s->head->next = new_node;
	s->size++;
	return true;
}
1.2.2.4 出栈

在出栈之前,我们需要先判断栈是否为空。若为空则返回false,不为空则访问头结点的后结点,将其元素值存储下来,然后释放该结点的空间,最后返回true即可。将元素值存储下来的方法和上文数组形式的出栈类似,都是利用传递地址参数,在函数里面进行解引用完成。

出栈函数的代码如下:

// 出栈,返回值为true或false,分别表示出栈成功以及失败
bool pop(struct Stack* s, int* num) {
	// 参数s为栈的结构体的地址,num为指向存储出栈元素变量的指针

	// 栈已空,返回false表示出栈失败
	if (empty(s)) {
		return false;
	}
	// 栈不为空,将元素出栈
	// 临时指针变量存储头结点的后结点指针
	struct Node* tmp = s->head->next;
	*num = tmp->num;
	// 将出栈元素所在结点从链表中删除
	s->head->next = tmp->next;
	s->size--;
	free(tmp);
	tmp = NULL;
	return true;
}
1.2.2.5 打印栈中所有元素

遍历链表,依次打印从栈顶到栈底的每一个元素,代码如下:

// 打印栈中所有元素
void display(struct Stack* s) {
	// 参数s为栈的结构体的地址

	printf("从栈顶到栈底的元素依次为:");
	struct Node* tmp = s->head->next;
	while (tmp) {
		printf("%d ", tmp->num);
		tmp = tmp->next;
	}
	printf("\n\n");
}
1.2.2.6 释放栈的空间

依次遍历链表,把每一个结点的空间释放掉,代码如下:

// 释放空间
void free_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 依次释放动态分配的每一个结点空间
	struct Node* tmp = NULL;
	while (s->head) {
		tmp = s->head;
		s->head = tmp->next;
		free(tmp);
		tmp = NULL;
	}
}
1.2.2.7 完整代码

以上就是使用链表实现的栈,当然如果大家还有其他功能,也可以编写对应的函数,这里就不再提供了。下面是完整的代码:

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


// 函数声明
void init_stack(struct Stack* s);
bool empty(struct Stack* s);
bool push(struct Stack* s, int num);
bool pop(struct Stack* s, int* num);
void display(struct Stack* s);
void free_stack(struct Stack* s);
void menu();

// 定义栈的结构体
struct Stack {
	struct Node* head;		// 链表头结点指针
	int size;				// 栈当前的元素个数
};

// 定义栈链表的结点结构体
struct Node {
	int num;				// 结点中的元素值
	struct Node* next;		// 当前结点的后结点指针
};

// 初始化栈
void init_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 初始化栈的当前元素个数
	s->size = 0;
	// 初始化链表头结点
	s->head = (struct Node*)malloc(sizeof(struct Node));
	if (s->head == NULL) {
		printf("动态分配空间失败!\n");
		exit(1);
	}
	// 初始化头结点的元素值和后结点指针
	s->head->num = 0;
	s->head->next = NULL;
}

// 判断栈是否为空
bool empty(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 当栈顶元素在数组中的下标为-1时,表示栈中没有元素
	return s->size == 0;
}

// 进栈,返回值为true或false,分别表示进栈成功以及失败
bool push(struct Stack* s, int num) {
	// 参数s为栈的结构体的地址,num为待进栈的元素
	
	// 为新元素开辟一个结点的空间
	struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
	if (new_node == NULL) {
		return false;
	}
	new_node->num = num;
	// 将新结点插入链表
	new_node->next = s->head->next;
	s->head->next = new_node;
	s->size++;
	return true;
}

// 出栈,返回值为true或false,分别表示出栈成功以及失败
bool pop(struct Stack* s, int* num) {
	// 参数s为栈的结构体的地址,num为指向存储出栈元素变量的指针

	// 栈已空,返回false表示出栈失败
	if (empty(s)) {
		return false;
	}
	// 栈不为空,将元素出栈
	// 临时指针变量存储头结点的后结点指针
	struct Node* tmp = s->head->next;
	*num = tmp->num;
	// 将出栈元素所在结点从链表中删除
	s->head->next = tmp->next;
	s->size--;
	free(tmp);
	tmp = NULL;
	return true;
}

// 打印栈中所有元素
void display(struct Stack* s) {
	// 参数s为栈的结构体的地址

	printf("从栈顶到栈底的元素依次为:");
	struct Node* tmp = s->head->next;
	while (tmp) {
		printf("%d ", tmp->num);
		tmp = tmp->next;
	}
	printf("\n\n");
}

// 释放空间
void free_stack(struct Stack* s) {
	// 参数s为栈的结构体的地址

	// 依次释放动态分配的每一个结点空间
	struct Node* tmp = NULL;
	while (s->head) {
		tmp = s->head;
		s->head = tmp->next;
		free(tmp);
		tmp = NULL;
	}
}

// 打印菜单
void menu() {
	printf("1.进栈\n");
	printf("2.出栈\n");
	printf("3.判断栈是否为空\n");
	printf("4.打印栈中所有元素\n");
	printf("5.结束程序\n");
	printf("请输入你的选择:");
}

// 主函数
int main() {
	// 定义栈的结构体变量
	struct Stack s;
	// 初始化栈
	init_stack(&s);

	// 存储用户选择
	int choice = 0;
	// 存储进栈或出栈元素
	int num = 0;
	// 存储进栈或出栈的结果
	bool result = false;
	// 进入主循环
	while (1) {
		// 打印菜单
		menu();
		scanf_s("%d", &choice);
		if (choice == 1) {
			// 元素进栈操作
			printf("输入要进栈的元素:");
			scanf_s("%d", &num);
			result = push(&s, num);
			if (result) {
				printf("元素进栈成功!\n\n");
			}
			else {
				printf("元素进栈失败!\n\n");
			}
		}
		else if (choice == 2) {
			// 元素出栈操作
			result = pop(&s, &num);
			if (result) {
				printf("元素出栈成功!\n\n");
			}
			else {
				printf("元素出栈失败!\n\n");
			}
		}
		else if (choice == 3) {
			// 判断栈是否为空
			if (empty(&s)) {
				printf("栈为空!\n\n");
			}
			else {
				printf("栈还没空!\n\n");
			}
		}
		else if (choice == 4) {
			// 打印栈中所有元素
			display(&s);
		}
		else if (choice == 5) {
			// 结束程序
			printf("准备结束程序,正在释放栈的空间......\n");
			break;
		}
		else {
			printf("无效输入!\n\n");
		}
	}

	// 释放栈空间
	free_stack(&s);
	printf("空间释放完毕!\n");

	return 0;
}

2.队列

2.1 队列的概念

从定义上来说,队列是一种只允许在前端进行删除操作,以及在后端进行插入操作的线性表。它和栈一样,都是一种操作受限的线性表。其中,仅允许进行删除操作的那一端被称为队头,仅允许进行插入操作的那一端则被称为队尾。此外,向队列中插入新元素的操作被称为入队,删除队列元素的操作则被称为出队

因此,当要执行出队操作时,被删除的队列元素将会是最先入队的那个元素,我们称队列的这个特点为“进先出”。

与栈一样,我们同样可以把队列和现实生活中的一些事物联系起来。所谓队列,其实和我们平时生活中排队排的那个队是差不多的。例如学生在食堂排队打饭,把学生排成的队伍看作队列,那么队伍中的每一位学生就都是一个元素,新的学生走到队伍最后也开始排队就是入队,排在队伍最前面的学生打完饭离开队伍就是出队。显然,越早开始排队的学生就越早能打到饭,这和队列的“先进先出”特点是一致的。

2.2 队列的构造

2.2.1 使用数组实现

在栈中,栈底的位置是固定不变的,即使不断有新的元素进栈或者已有元素出栈,这种性质使得栈能够通过一个普通的顺序数组就能够实现。但在队列中,无论是队头还是队尾,它们的位置都是会发生改变的。

如果我们像实现栈那样用一个普通的顺序数组实现队列的构造,那么随着入队以及出队这两个操作的不断执行,队列元素的位置就会在数组中整体向后移动,当队列队尾的位置变为数组最后一个位置时,这个队列就不能再进行入队操作了,但是实际上这个时候数组的前面部分空间很可能是空着的(因为一开始入队的元素有可能已经出队了不少)。

为了将数组中这部分空闲的空间利用起来,我们就需要模拟实现一个循环数组。主要思想就是当队尾元素的下标为数组的最大下标时,如果再有元素入队,则把此时入队的元素放到数组的第一位上,且此后再入队的元素都会按照此前正常的入队操作,依次跟在这个重新放在数组第一位的元素后面。

简单来说,就是把物理上不相连的数组最后一个位置和数组最前面的位置从逻辑角度上连接起来,把数组当作一个首尾相接的环结构。举个例子,假设我们用一个容量为10的数组A去实现队列,并且在经过一系列操作后,数组A的后面三个位置,即A[7]、A[8]、A[9]都存放有队列的元素,若此时再进行入队操作,那么新入队的元素将会被存放在A[0]中,之后再入队的则按照A[1]、A[2]......这样子排下去。

显然,这种队列可以容纳的元素数目是有限的,那么我们是否可以也使用动态分配空间的数组来实现呢?其实是可以的,不过每次重新分配空间后,我们都需要手动地将原队列元素按队列顺序,重新存放到新数组中,且从新数组0下标开始。

为直观理解,我们也举个例子,假设我们有一个容量为3的数组A实现的队列,且队列是已满状态,队头元素是A[1],即从队头到队尾依次为A[1]、A[2]、A[0],那么如果我们为其重新分配为容量是5的数组B,那么realloc函数会自动将原数组中的值按照原数组的顺序复制过去,即A[0]=B[0],A[1]=B[1],A[2]=B[2],B[3]和B[4]则是未初始化的空间,这个时候原来的队尾B[0]已经不再是跟在B[2]后面了,所以我们需要依次把A[1]、A[2]、A[0]复制到B[0]、B[1]、B[2]上。

所以,为方便讲解,我们仍然会使用动态数组实现队列,但不会在队列满的时候重新分配空间。

2.2.1.1 队列的结构体定义和队列的初始化

因为是静态数组实现,队列可以容纳的元素数目是有限的,那么该如何判断队列是否已满或者为空呢?我们可以用两个变量分别表示队列的最大空间以及当前元素数目实现这个需求。当这两个变量的值相等时表示当前队列已满,而当表示当前元素数目的值为0时则表示当前队列为空。

与此同时,我们还需要定义两个变量,分别表示队头元素在数组中的下标和队尾元素在数组中的下标,这是很有必要的,因为队列的首尾位置会随着入队出队的进行发生变化,而栈只有栈顶会发生变化,这也是为什么栈不需要记录栈底位置。

那么队头元素以及队尾元素在数组中的下标应该初始化为多少呢?若我们令head为队头元素在数组中的下标,tail为队尾元素在数组中的下标,size为队列的最大容量,则只需要保证head=(tail+1)%size,且head和tail都在数组的下标范围之内即可。

为什么要这样初始化?因为我们在进行入队操作的时候,会先执行tail=(tail+1)%size,然后将新入队元素放到数组中下标为tail的位置上。而因为初始化时head=(tail+1)%size,所以在执行完tail=(tail+1)%size后,tail的值就和head一致了,这样才能使队列中只有一个元素时队头和队尾元素下标一致。并且因为我们前面已经说过了,队头和队尾的位置是不固定的,我们对队列判空或判满都不需要借助这两个变量,所以这里我们可以将head和tail分别初始化为(0+1)%size和0。

据此我们定义队列的结构体以及其初始化如下:

#define MAX_SIZE 50		// 队列最大容量


// 定义队列的结构体
struct Queue {
	int* queue;			// 队列数组的指针变量
	int size;			// 队列的最大容量
	int count;			// 队列当前元素数量
	int head;			// 队头元素在数组中的下标
	int tail;			// 队尾元素在数组中的下标
};

// 初始化队列
void init_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 初始化队列的最大容量
	q->size = MAX_SIZE;
	// 初始化队列数组
	q->queue = (int*)malloc(sizeof(int) * q->size);
	if (q->queue == NULL) {
		printf("动态分配空间失败!\n");
		exit(1);
	}
	// 初始化队列的当前元素个数
	q->count = 0;
	// 初始化队头元素和队尾元素的位置
	q->head = 1;
	q->tail = 0;
}
2.2.1.2 判断队列满和队列空

借助队列最大容量以及当前元素数量这两个变量,我们可以判断队列是否已满或者为空,原理也很简单,这里直接上代码:

// 判断队列是否已满
bool full(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 当队列当前元素数量等于队列最大容量时,队列为满
	return q->count == q->size;
}

// 判断队列是否为空
bool empty(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 当队列当前元素数量为0时,队列为空
	return q->count == 0;
}
2.2.1.3 入队

在定义队列结构体和初始化队列的时候我们就提到了入队这个操作,其实就是把tail的值加一并模size,然后才把新元素存放到queue[tail]中。首先tail加一这个操作很好理解,至于加一之后还要模size,其实就是为了实现模拟一个循环的数组,当tail的值增加到了size-1时,tail再加一变为size就超出数组的下标范围了,所以我们这个时候进行模size处理,tail就会重新变为0,从而实现循环利用数组空间。

不过在进行入队操作之前,我们还是需要先判断队列是否已满。

下面是入队函数的代码:

// 入队,返回值为true或false,分别表示入队成功以及失败
bool push(struct Queue* q, int num) {
	// 参数q为队列的结构体的地址,num为待入队的元素
	
	// 判断队列是否已满
	if (full(q)) {
		return false;
	}
	// 元素入队
	q->tail = (q->tail + 1) % q->size;
	q->queue[q->tail] = num;
	q->count++;
	return true;
}
2.2.1.4 出队

要把队头元素出队,我们需要先把队头元素取出到接收的变量中,然后把head的值加一并模size,最后修改队列当前元素数量即可。具体的原理和入队类似,如何接收出对元素则在栈已经讲过,这里就都不再作详细说明。

下面是出队函数的代码:

// 出队,返回值为true或false,分别表示出队成功以及失败
bool pop(struct Queue* q, int* num) {
	// 参数q为队列的结构体的地址,num为指向存储出队元素变量的指针

	// 判断队列是否为空
	if (empty(q)) {
		return false;
	}
	// 元素出队
	*num = q->queue[q->head];
	q->head = (q->head + 1) % q->size;
	q->count--;
	return true;
}
2.2.1.5 打印队列所有元素

从队头位置开始按队列顺序遍历数组所有的队列元素,并把它们打印出来,代码如下:

// 打印队列中所有元素
void display(struct Queue* q) {
	// 参数q为队列的结构体的地址

	printf("从队头到队尾的元素依次为:");
	int tmp = q->head;
	for (int i = 0; i < q->count; i++) {
		printf("%d ", q->queue[tmp]);
		tmp = (tmp + 1) % q->size;
	}
	printf("\n\n");
}
2.2.1.6 释放队列的空间

把为动态数组分配的空间释放掉,代码如下:

// 释放空间
void free_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	free(q->queue);
	q->queue = NULL;
	q->count = 0;
}
2.2.1.7 完整代码

以上就是使用数组实现的队列,当然如果大家还有其他功能,也可以编写对应的函数,这里就不再提供了。下面是完整的代码:

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

#define MAX_SIZE 50		// 队列最大容量


// 函数声明
void init_queue(struct Queue* q);
bool full(struct Queue* q);
bool empty(struct Queue* q);
bool push(struct Queue* q, int num);
bool pop(struct Queue* q, int* num);
void display(struct Queue* q);
void free_queue(struct Queue* q);
void menu();

// 定义队列的结构体
struct Queue {
	int* queue;			// 队列数组的指针变量
	int size;			// 队列的最大容量
	int count;			// 队列当前元素数量
	int head;			// 队头元素在数组中的下标
	int tail;			// 队尾元素在数组中的下标
};

// 初始化队列
void init_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 初始化队列的最大容量
	q->size = MAX_SIZE;
	// 初始化队列数组
	q->queue = (int*)malloc(sizeof(int) * q->size);
	if (q->queue == NULL) {
		printf("动态分配空间失败!\n");
		exit(1);
	}
	// 初始化队列的当前元素个数
	q->count = 0;
	// 初始化队头元素和队尾元素的位置
	q->head = 1;
	q->tail = 0;
}

// 判断队列是否已满
bool full(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 当队列当前元素数量等于队列最大容量时,队列为满
	return q->count == q->size;
}

// 判断队列是否为空
bool empty(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 当队列当前元素数量为0时,队列为空
	return q->count == 0;
}

// 入队,返回值为true或false,分别表示入队成功以及失败
bool push(struct Queue* q, int num) {
	// 参数q为队列的结构体的地址,num为待入队的元素
	
	// 判断队列是否已满
	if (full(q)) {
		return false;
	}
	// 元素入队
	q->tail = (q->tail + 1) % q->size;
	q->queue[q->tail] = num;
	q->count++;
	return true;
}

// 出队,返回值为true或false,分别表示出队成功以及失败
bool pop(struct Queue* q, int* num) {
	// 参数q为队列的结构体的地址,num为指向存储出队元素变量的指针

	// 判断队列是否为空
	if (empty(q)) {
		return false;
	}
	// 元素出队
	*num = q->queue[q->head];
	q->head = (q->head + 1) % q->size;
	q->count--;
	return true;
}

// 打印队列中所有元素
void display(struct Queue* q) {
	// 参数q为队列的结构体的地址

	printf("从队头到队尾的元素依次为:");
	int tmp = q->head;
	for (int i = 0; i < q->count; i++) {
		printf("%d ", q->queue[tmp]);
		tmp = (tmp + 1) % q->size;
	}
	printf("\n\n");
}

// 释放空间
void free_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	free(q->queue);
	q->queue = NULL;
	q->count = 0;
}

// 打印菜单
void menu() {
	printf("1.入队\n");
	printf("2.出队\n");
	printf("3.判断队列是否已满\n");
	printf("4.判断队列是否为空\n");
	printf("5.打印队列中所有元素\n");
	printf("6.结束程序\n");
	printf("请输入你的选择:");
}

// 主函数
int main() {
	// 定义队列的结构体变量
	struct Queue q;
	// 初始化队列
	init_queue(&q);
	// 存储用户选择
	int choice = 0;
	// 存储入队或出队元素
	int num = 0;
	// 存储入队或出队的结果
	bool result = false;
	// 进入主循环
	while (1) {
		// 打印菜单
		menu();
		scanf_s("%d", &choice);
		if (choice == 1) {
			// 元素入队操作
			printf("输入要入队的元素:");
			scanf_s("%d", &num);
			result = push(&q, num);
			if (result) {
				printf("元素入队成功!\n\n");
			}
			else {
				printf("元素入队失败!\n\n");
			}
		}
		else if (choice == 2) {
			// 元素出队操作
			result = pop(&q, &num);
			if (result) {
				printf("元素出队成功!\n\n");
			}
			else {
				printf("元素出队失败!\n\n");
			}
		}
		else if (choice == 3) {
			// 判断队列是否已满
			if (full(&q)) {
				printf("队列已满!\n\n");
			}
			else {
				printf("队列未满!\n\n");
			}
		}
		else if (choice == 4) {
			// 判断队列是否为空
			if (empty(&q)) {
				printf("队列为空!\n\n");
			}
			else {
				printf("队列不为空!\n\n");
			}
		}
		else if (choice == 5) {
			// 打印队列中所有元素
			display(&q);
		}
		else if (choice == 6) {
			// 结束程序
			printf("准备结束程序,正在释放队列的空间......\n");
			break;
		}
		else {
			printf("无效输入!\n\n");
		}
	}

	// 释放队列空间
	free_queue(&q);
	printf("空间释放完毕!\n");

	return 0;
}

2.2.2 使用链表实现

用链表实现队列,我们同样有两种链表可以选择:单链表和双链表。因为无论是入队还是出队操作,我们都是按照沿一个方向进行,所以在使用链表实现队列时我们使用单链表就可以了。

2.2.2.1 队列的结构体定义、链表结点的结构体定义和队列的初始化

首先我们需要定义两个指针变量,分别指向队头元素所在的链表结点和队尾元素所在的链表结点。而当队列中没有任何元素,即队列为空时,这两个指针变量的值都会是NULL。同时,因为链表的长度是不固定的,所以我们不再需要定义变量存储队列的最大容量。

据此我们定义队列的结构体以及链表结点的结构体如下:

// 定义队列的结构体
struct Queue {
	struct Node* head;		// 队头元素所在链表结点的指针
	struct Node* tail;		// 队尾元素所在链表结点的指针
	int count;				// 队列当前元素数量
};

// 定义链表结点的结构体
struct Node {
	int num;				// 当前结点的元素值
	struct Node* next;		// 后结点的指针
};

当初始化队列时,因为队列一开始是空队列,所以我们需要把head和tail都赋值为NULL,并把count赋值为0。

初始化队列的函数代码如下:

// 初始化队列
void init_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 初始化队头和队尾结点的指针值
	q->head = NULL;
	q->tail = NULL;
	// 初始化队列的当前元素数量
	q->count = 0;
}
2.2.2.2 判断队列空

因为是用链表实现的队列,我们不需要判断队列是否已满,仅需要判断队列是否为空,这一步可以借助队列结构体里的count或者head实现。

下面是判断队列是否为空的函数代码:

// 判断队列是否为空
bool empty(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 当队列当前元素数量为0时,队列为空
	return q->count == 0;
}
2.2.2.3 入队

当有新元素要入队时,我们会为新元素动态分配一个结点的空间,然后把这个新节点加入到原队尾结点之后,最后修改队列中元素的数量。不过需要注意的是,如果在新元素入队时队列为空,那此时队尾结点是不存在的,我们就需要直接将新结点的指针值直接赋值给head以及tail,因为新结点加入一个空队列后,它不仅是队尾结点,还是队头结点。

据此我们编写入队函数的代码如下:

// 入队,返回值为true或false,分别表示入队成功以及失败
bool push(struct Queue* q, int num) {
	// 参数q为队列的结构体的地址,num为待入队的元素
	
	// 为新元素动态分配一个结点的空间
	struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
	if (new_node == NULL) {
		return false;
	}
	new_node->num = num;
	new_node->next = NULL;
	// 判断队列是否为空,并据此执行对应的入队操作
	if (empty(q)) {
		q->head = new_node;
		q->tail = new_node;
	}
	else {
		// 把新节点加入在原队尾结点的后面
		q->tail->next = new_node;
		q->tail = new_node;
	}
	// 更新队列元素数量
	q->count++;
	return true;
}
2.2.2.4 出队

当要执行出队操作时,我们需要先判断队列是否为空,若为空则出队失败,不为空则会把队头元素先存放到接收出队元素的变量中,然后将队头结点的指针指向其后结点,再释放原对头结点的空间,并修改队列元素数量。

不过这里也有需要注意的地方,那就是若出队的元素是当前队列中最后一个元素,那么在这个最后的元素出队前,队头结点的指针和队尾结点的指针指向的是同一个结点。在出对后队列将会变为一个空队列,此时队头结点的指针指向原队头结点的后结点,即它的值是NULL,这是没有问题的,但是原队尾结点的指针指向的还是被释放空间的那个结点,并不符合我们之前规定的队列为空时队头结点指针和队尾结点指针都应该是NULL。所以,我们在出队完后还需要进行一下特判——当队列在出队后变为空队列时,我们需要手动将队尾结点的指针赋值为NULL。

据此我们编写出队的函数代码如下:

// 出队,返回值为true或false,分别表示出队成功以及失败
bool pop(struct Queue* q, int* num) {
	// 参数q为队列的结构体的地址,num为指向存储出队元素变量的指针

	// 判断队列是否为空
	if (empty(q)) {
		return false;
	}
	// 临时结点指针变量记录要出队的元素所在结点
	struct Node* tmp = q->head;
	// 更新队头结点指针
	q->head = q->head->next;
	// 保存出队元素
	*num = tmp->num;
	// 释放空间
	free(tmp);
	tmp = NULL;
	// 修改队列元素数量
	q->count--;
	// 特判出队后队列是否为空
	if (empty(q)) {
		q->tail = NULL;
	}
	return true;
}
2.2.2.5 打印队列所有元素

从队头结点开始遍历整个链表,并依次打印所有元素值,函数代码如下:

// 打印队列中所有元素
void display(struct Queue* q) {
	// 参数q为队列的结构体的地址

	printf("从队头到队尾的元素依次为:");
	struct Node* tmp = q->head;
	while (tmp != NULL) {
		printf("%d ", tmp->num);
		tmp = tmp->next;
	}
	printf("\n\n");
}
2.2.2.6 释放队列的空间

从队头结点开始遍历整个链表,并依次释放每一个结点的空间,函数代码如下:

// 释放空间
void free_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 依次释放每一个结点的空间
	struct Node* tmp = NULL;
	while (q->head != NULL) {
		tmp = q->head;
		q->head = q->head->next;
		free(tmp);
		tmp = NULL;
	}
	// 修改队列结构体相关成员变量的值
	q->tail = NULL;
	q->count = 0;
}
2.2.2.7 完整代码

以上就是使用链表实现的队列,当然如果大家还有其他功能,也可以编写对应的函数,这里就不再提供了。下面是完整的代码:

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


// 函数声明
void init_queue(struct Queue* q);
bool empty(struct Queue* q);
bool push(struct Queue* q, int num);
bool pop(struct Queue* q, int* num);
void display(struct Queue* q);
void free_queue(struct Queue* q);
void menu();

// 定义队列的结构体
struct Queue {
	struct Node* head;		// 队头元素所在链表结点的指针
	struct Node* tail;		// 队尾元素所在链表结点的指针
	int count;				// 队列当前元素数量
};

// 定义链表结点的结构体
struct Node {
	int num;				// 当前结点的元素值
	struct Node* next;		// 后结点的指针
};

// 初始化队列
void init_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 初始化队头和队尾结点的指针值
	q->head = NULL;
	q->tail = NULL;
	// 初始化队列的当前元素数量
	q->count = 0;
}

// 判断队列是否为空
bool empty(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 当队列当前元素数量为0时,队列为空
	return q->count == 0;
}

// 入队,返回值为true或false,分别表示入队成功以及失败
bool push(struct Queue* q, int num) {
	// 参数q为队列的结构体的地址,num为待入队的元素
	
	// 为新元素动态分配一个结点的空间
	struct Node* new_node = (struct Node*)malloc(sizeof(struct Node));
	if (new_node == NULL) {
		return false;
	}
	new_node->num = num;
	new_node->next = NULL;
	// 判断队列是否为空,并据此执行对应的入队操作
	if (empty(q)) {
		q->head = new_node;
		q->tail = new_node;
	}
	else {
		// 把新节点加入在原队尾结点的后面
		q->tail->next = new_node;
		q->tail = new_node;
	}
	// 更新队列元素数量
	q->count++;
	return true;
}

// 出队,返回值为true或false,分别表示出队成功以及失败
bool pop(struct Queue* q, int* num) {
	// 参数q为队列的结构体的地址,num为指向存储出队元素变量的指针

	// 判断队列是否为空
	if (empty(q)) {
		return false;
	}
	// 临时结点指针变量记录要出队的元素所在结点
	struct Node* tmp = q->head;
	// 更新队头结点指针
	q->head = q->head->next;
	// 保存出队元素
	*num = tmp->num;
	// 释放空间
	free(tmp);
	tmp = NULL;
	// 修改队列元素数量
	q->count--;
	// 特判出队后队列是否为空
	if (empty(q)) {
		q->tail = NULL;
	}
	return true;
}

// 打印队列中所有元素
void display(struct Queue* q) {
	// 参数q为队列的结构体的地址

	printf("从队头到队尾的元素依次为:");
	struct Node* tmp = q->head;
	while (tmp != NULL) {
		printf("%d ", tmp->num);
		tmp = tmp->next;
	}
	printf("\n\n");
}

// 释放空间
void free_queue(struct Queue* q) {
	// 参数q为队列的结构体的地址

	// 依次释放每一个结点的空间
	struct Node* tmp = NULL;
	while (q->head != NULL) {
		tmp = q->head;
		q->head = q->head->next;
		free(tmp);
		tmp = NULL;
	}
	// 修改队列结构体相关成员变量的值
	q->tail = NULL;
	q->count = 0;
}

// 打印菜单
void menu() {
	printf("1.入队\n");
	printf("2.出队\n");
	printf("3.判断队列是否为空\n");
	printf("4.打印队列中所有元素\n");
	printf("5.结束程序\n");
	printf("请输入你的选择:");
}

// 主函数
int main() {
	// 定义队列的结构体变量
	struct Queue q;
	// 初始化队列
	init_queue(&q);
	// 存储用户选择
	int choice = 0;
	// 存储入队或出队元素
	int num = 0;
	// 存储入队或出队的结果
	bool result = false;
	// 进入主循环
	while (1) {
		// 打印菜单
		menu();
		scanf_s("%d", &choice);
		if (choice == 1) {
			// 元素入队操作
			printf("输入要入队的元素:");
			scanf_s("%d", &num);
			result = push(&q, num);
			if (result) {
				printf("元素入队成功!\n\n");
			}
			else {
				printf("元素入队失败!\n\n");
			}
		}
		else if (choice == 2) {
			// 元素出队操作
			result = pop(&q, &num);
			if (result) {
				printf("元素出队成功!\n\n");
			}
			else {
				printf("元素出队失败!\n\n");
			}
		}
		else if (choice == 3) {
			// 判断队列是否为空
			if (empty(&q)) {
				printf("队列为空!\n\n");
			}
			else {
				printf("队列不为空!\n\n");
			}
		}
		else if (choice == 4) {
			// 打印队列中所有元素
			display(&q);
		}
		else if (choice == 5) {
			// 结束程序
			printf("准备结束程序,正在释放队列的空间......\n");
			break;
		}
		else {
			printf("无效输入!\n\n");
		}
	}

	// 释放队列空间
	free_queue(&q);
	printf("空间释放完毕!\n");

	return 0;
}

3.总结

看完上面的内容,想必大家对于栈和队列以及它们的构造和实现都有了一定的了解。在讲解如何实现栈和队列的构造和实现的时候,我们都分别使用了两种方法——数组和链表,那么这两种实现方法有什么差别呢?或者说它们分别有什么优势和劣势呢?下面说一下笔者的个人理解。

先说说数组,它的劣势是不够灵活,如果不是动态数组,那么栈和队列的容量一开始就是写死的。同时,如果我们不能提前得知栈和队列最多需要同时存储多少元素,那么我们就很难定义一个合适空间大小的数组去实现,定义的数组空间太小可能会空间不足,空间太大有可能会造成空间的浪费。再者,如果栈或队列最大需要同时容纳的元素数量太大,计算机中就很可能找不到这样一块连续的空间开辟作为数组供使用。

那么数组的优势在哪呢?我们发现数组的劣势很大一部分都是空间上的,那么数组的优势会不会在时间上有体现?还真是这样。和链表比较起来,数组不需要每次进行元素的进/出栈或者入/出队都要调用一次动态分配空间的函数,这能为代码的运行节省下来不少时间。

说完了数组,我们再来说一下链表。其实在明确了数组的优劣势之后,链表的优劣势已经很明显了。空间上,链表实现的栈或者队列能够根据当前元素的数量分配合适大小的空间(即使有next指针占用了额外空间),而不会浪费太多空余的空间。并且链表不要求在内存中开辟一片物理上连续的空间,它可以利用碎片化的空间实现较大数量元素的栈或队列,而不用担心内存不足。

至于链表在时间性能上的表现,在讨论数组的时候我们就说到了,每次都为新结点分配一次空间,或者释放一次空间,使得链表实现的栈或者队列在时间性能上表现远不如数组。

好了,以上就是笔者关于实现栈和队列的见解了。啰啰嗦嗦唠叨了这么多,如果有不足或错误的地方,欢迎各位读者在评论区提出来,我会进行改正,谢谢!

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值