什么是栈?首先引用维基百科的解释:
栈(stack)是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。
有点难懂?没关系,我们来看一张图:
没错,桌上的这一叠盘子就是一个栈结构(实际上还需要加一些限定条件,一会会讲到)。其实我们每天都会与“栈”接触。可以说只要理解了这一叠盘子,你就理解了栈!
首先根据维基百科的专业定义,栈有两种基本操作:弹栈和压栈:
对应到我们的这一叠盘子,通常情况下也是两个操作:取盘子和放盘子:
还记得我们上文说过这一叠盘子如果要成为栈,还需要一些限定条件吗?下面我们就来看一下这个条件:
上述对盘子的所有操作必须在盘子顶进行!
说得更直白点,就是取盘子的时候只能从顶部取,不能从中间取;放盘子的时候只能放在顶部,不能插到中间。
于是我们发现,我们每次从这一叠盘子里取出的盘子都是最后叠上去的,最先叠上去的只有在上面的盘子全部取完以后才能取出来。而上面这句话,就是栈结构的最大特点:
后进先出(LIFO, Last In First Out)
OK。以上就是认识栈结构所需要的所有知识,是不是很简单。
下面我们再用一组图片深入理解一下栈结构与其操作过程:
这是一个单向开口的空间,所有元素都通过顶部的开口进入和弹出,内部的橙色方块就是进栈的元素。很明显,先进入的元素会被后进入的元素盖住,取数据的时候只能先取后进入的数据。此时最上面的元素C就被称之为栈顶,最下面的元素A自然就成为了栈底。
这就是压栈过程,新放入的元素D放在栈的最上面,代替原来的元素C成为了新的栈顶。栈底仍然为A。
这是弹栈(出栈)过程,元素D被取出,元素C再次成为栈顶,元素A依然是栈底,并且只有等到A上面的所有元素(B、C)全部取出时,A才有机会出栈。
除了压栈和弹栈这两个基本操作,栈结构还有两个特殊状态。此处停止下滑,请思考一下是哪两个。
左边的栈中没有任何元素,所以被称之为空栈,右边被填满了元素,因此被称之为满栈。一般情况下,在程序初始化栈的时候要保证栈为空,在后续的压栈与弹栈操作流程中还要在每次操作前检测栈状态,避免在满栈状态下压栈,在空栈的状态下弹栈,一旦出现上面两种情况,就会导致数据溢出或访问到非法数据,轻则导致程序崩溃死机,重则引起设备失控,甚至出现伤人事件!这并不是危言耸听,试想如果一架运行着的飞机突然自动控制单元出现了栈溢出,导致控制程序死机,无法被控制,也无法切换手动运行。后果自然是非常严重的!
说了这么多,好像栈是一个很简单,且很不灵活的结构,那这玩意儿到底有啥用?
可不要小看了这个结构,在如今的计算机科学、编程与算法中,栈是非常重要,也是用得非常多的一种数据结构,下面我们举几个例子:
-
函数调用:在计算机程序中,函数的调用和返回借助栈来实现。每次调用函数时,函数的参数和局部变量都会被存储在栈中,当函数执行完成后,栈会弹出这些数据,返回调用点。
-
表达式求值:栈可以用于解析和求值表达式,包括中缀表达式转换为后缀表达式以及后缀表达式的求值。
-
内存管理:栈用于存储函数的局部变量和临时数据,对内存的分配和释放起到了关键作用。
-
括号匹配:栈可以用于检查括号匹配,例如检查一个字符串中的括号是否正确闭合。
-
后退和撤销:在许多应用程序中,栈可以用于实现后退和撤销功能,例如文本编辑器中的撤销操作。
上面这些例子都是我们日常生活中频繁遇到的场景,由此可见栈无处不在。比如就在写这段话的时候,我还撤销了一个错别字。
到这里你以为就结束了吗?NO! NO! NO! 作为实践主义的一员,我深信实践是检验真理的唯一标准,没有实践,怎么能说理解!接下来,我们一起来用 C 语言实现一个栈结构!
发车之前,我们先明确一点,栈在软件上一般有数组和链表两种实现方式,链表形式会比较复杂,并且涉及到另外的数据结构,本文既然是讲栈的,就尽量不引入其他结构来避免理解困难,因此下文会实现一个纯数组栈。
首先我们来定义一个最简单的栈结构:
#define MAX_SIZE 100 // 栈的最大容量
typedef struct {
int data[MAX_SIZE];
int top;
} Stack;
可以看到,这个结构里有一个数组和一个变量。其中 data 数组就是栈容器,用于存储数据,而 top 变量就用于指示当前栈顶的栈顶计数(有些地方会用一个指针,即栈顶指针。这里直接用计数比较好理解,作用是一样的)。
定义完结构后我们来进行初始化:
// 初始化栈
void init(Stack *stack) { stack->top = -1; }
这里的初始化逻辑很简单,就是将 top 变量的值设置为 -1 。在后续的程序中只要这个值为 -1,我们就认为当前栈为空:
// 判断栈是否为空
bool isEmpty(Stack *stack) { return stack->top == -1; }
自然,当 top 值为 MAX_SIZE - 1 时,就代表当前栈已满(有一个元素时值为0):
// 判断栈是否已满
bool isFull(Stack *stack) { return stack->top == MAX_SIZE - 1; }
压栈:
// 压栈
void push(Stack *stack, int value) {
if (isFull(stack)) {
printf("栈已满,无法入栈\n");
} else {
stack->data[++stack->top] = value;
}
}
将数据压栈就是把数据放入数组,同时栈顶计数加一表示当前加入了一个数据。这里要注意,压入数据之前必须要检查栈是否满,避免栈溢出。
弹栈(出栈):
// 出栈
int pop(Stack *stack, int *value) {
if (isEmpty(stack)) {
printf("栈已空,无法出栈\n");
return -1;
} else {
*value = stack->data[stack->top--];
return 0;
}
}
弹栈则是对外弹出(返回)数组内有效数据的顶部数据,同时,栈顶计数减一表示当前取出了一个数据。同样,必须确保栈不为空再进行弹栈操作。
查看栈顶元素:
// 获取栈顶元素
int top(Stack *stack, int *value) {
if (isEmpty(stack)) {
printf("栈已空,无栈顶元素\n");
return -1;
} else {
*value = stack->data[stack->top];
return 0;
}
}
该操作和弹栈很类似,但必须要注意,该操作不会导致栈顶计数的变化,只是查看元素。可以理解为,你小时候每次走过玩具店都要看一眼橱窗,然后对妈妈说:我就看看,不买~
打印栈中的元素:
// 打印栈中的元素
void printStack(Stack *stack) {
printf("栈中的元素为:");
for (int i = 0; i <= stack->top; i++) {
printf("%d ", stack->data[i]);
}
printf("\n");
}
调试接口,从栈顶到栈底依次打印出栈内数据,用于查看栈内的数据状态。
最后我们用一个 main 函数将上述接口都串起来,形成一个可以运行的程序:
int main() {
int ret = -1, value = 0;
Stack stack;
init(&stack);
push(&stack, 3);
push(&stack, 5);
push(&stack, 7);
printStack(&stack);
ret = pop(&stack, &value);
if (ret == 0) {
printf("出栈元素为:%d\n", value);
} else {
printf("出栈失败");
}
printStack(&stack);
return 0;
}
这个程序的运行逻辑如下:
定义一个栈 > 初始化栈 > 将3压入栈 > 将5压入栈 > 将7压入栈 > 打印当前栈状态 > 弹栈 > 打印弹出数据 > 打印当前栈状态。
这里再次停止往下滑,先思考一下输出结果会是什么。下面来看下运行结果:
jay@jaylinuxlenovo:~/test/stack$ ./stack
栈中的元素为:3 5 7
出栈元素为:7
栈中的元素为:3 5
是不是和你想的一样呢?对于栈的概念,你是不是真的理解了呢。下面附上完整代码,感兴趣的小伙伴可以自己尝试运行一下。
/***************************************************************
* @file stack.c
* @brief
* @author WKJay
* @Version
* @Date 2023-12-07
***************************************************************/
#include <stdio.h>
#include <stdbool.h>
#define MAX_SIZE 100 // 栈的最大容量
typedef struct {
int data[MAX_SIZE];
int top;
} Stack;
// 初始化栈
void init(Stack *stack) { stack->top = -1; }
// 判断栈是否为空
bool isEmpty(Stack *stack) { return stack->top == -1; }
// 判断栈是否已满
bool isFull(Stack *stack) { return stack->top == MAX_SIZE - 1; }
// 压栈
void push(Stack *stack, int value) {
if (isFull(stack)) {
printf("栈已满,无法入栈\n");
} else {
stack->data[++stack->top] = value;
}
}
// 出栈
int pop(Stack *stack, int *value) {
if (isEmpty(stack)) {
printf("栈已空,无法出栈\n");
return -1;
} else {
*value = stack->data[stack->top--];
return 0;
}
}
// 获取栈顶元素
int top(Stack *stack, int *value) {
if (isEmpty(stack)) {
printf("栈已空,无栈顶元素\n");
return -1;
} else {
*value = stack->data[stack->top];
return 0;
}
}
// 打印栈中的元素
void printStack(Stack *stack) {
printf("栈中的元素为:");
for (int i = 0; i <= stack->top; i++) {
printf("%d ", stack->data[i]);
}
printf("\n");
}
int main() {
int ret = -1, value = 0;
Stack stack;
init(&stack);
push(&stack, 3);
push(&stack, 5);
push(&stack, 7);
printStack(&stack);
ret = pop(&stack, &value);
if (ret == 0) {
printf("出栈元素为:%d\n", value);
} else {
printf("出栈失败");
}
printStack(&stack);
return 0;
}
到站,下车!