栈的定义与顺序实现:栈的概念与特性(LIFO)
说到栈,我脑子里第一个蹦出来的画面就是食堂里那摞餐盘。你想想看,后来放上去的盘子总是最先被拿走,最早放进去的反倒压在最底下。这就是栈最核心的特性——后进先出(LIFO, Last In First Out)。
我在做嵌入式协议栈的时候,经常用栈来保存函数调用的返回地址。每次调用子函数,就把返回地址压进去;函数返回时,再从栈顶弹出。这个机制要是搞错了,程序跑飞那是分分钟的事。
栈的抽象数据类型
栈说白了就是一种操作受限的线性表。它只允许在一端进行插入和删除操作,这一端我们叫它栈顶(Top),另一端叫栈底(Bottom)。
一个标准的栈应该提供以下操作:
| 操作 | 描述 |
|---|---|
| InitStack | 初始化一个空栈 |
| Push | 将元素压入栈顶 |
| Pop | 弹出栈顶元素 |
| Peek / Top | 获取栈顶元素但不弹出 |
| IsEmpty | 判断栈是否为空 |
| IsFull | 判断栈是否已满(顺序栈特有) |
| Size | 获取栈中元素个数 |
核心要点:栈的所有操作都围绕栈顶展开。你没法直接访问栈底的元素,必须先把上面的元素全部弹出才行。这就是LIFO的约束力。
顺序栈的实现
顺序栈就是用数组来模拟栈。我习惯用一个结构体来封装栈的数据和状态,这样代码更清晰,也方便移植。
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE]; // 存放栈元素的数组
int top; // 栈顶指针,指向当前栈顶元素的位置
} SeqStack;
这里有个细节要注意:top 的初始值怎么设?我个人习惯设成 -1,表示空栈。当压入第一个元素时,top 变成 0,元素放在 data[0]。这样 top 始终指向栈顶元素的位置。
入栈操作(Push)
入栈就是把新元素放到栈顶。逻辑很简单:先检查栈是否满了,没满就把 top 加 1,然后把元素放进去。
int Push(SeqStack *stack, int value) {
if (stack->top >= MAX_SIZE - 1) {
printf("栈已满,无法入栈\n");
return -1; // 返回错误码
}
stack->data[++stack->top] = value;
return 0; // 成功
}
你看,++stack->top 这个写法,先自增再赋值。嗯,这里要注意:如果你写成 stack->data[stack->top++] = value,那就变成先赋值再自增,逻辑就全乱了。我曾经在调试一个通信协议栈时,就因为这种细节没注意,导致数据错位,排查了整整一个下午。
出栈操作(Pop)
出栈是从栈顶取出元素。同样先检查栈是否为空,不为空就把 top 减 1,返回原来的栈顶元素。
int Pop(SeqStack *stack, int *value) {
if (stack->top == -1) {
printf("栈已空,无法出栈\n");
return -1;
}
*value = stack->data[stack->top--];
return 0;
}
这里我用的是传指针的方式把弹出的值带出来。为什么不用返回值?因为返回值我已经用来表示操作状态了(0 成功,-1 失败)。这是C语言里很常见的做法——用返回值表示状态,用参数传递数据。
小技巧:出栈时其实不需要真的把数据从数组里清掉,只要把 top 减 1 就行。下次入栈时,新数据会直接覆盖掉旧数据。省掉一次赋值操作,在嵌入式环境里能省一点是一点。
辅助操作
除了入栈出栈,还有几个辅助函数也很常用:
// 获取栈顶元素,不弹出
int Peek(SeqStack *stack, int *value) {
if (stack->top == -1) {
return -1;
}
*value = stack->data[stack->top];
return 0;
}
// 判断栈是否为空
int IsEmpty(SeqStack *stack) {
return stack->top == -1;
}
// 判断栈是否已满
int IsFull(SeqStack *stack) {
return stack->top >= MAX_SIZE - 1;
}
// 获取栈中元素个数
int Size(SeqStack *stack) {
return stack->top + 1;
}
避坑指南:我曾经在一个项目里,因为忘记在初始化时把 top 设成 -1,导致第一次入栈时数据放到了 data[0] 但 top 还是 0,第二次入栈时直接覆盖了第一次的数据。这种 bug 很难查,因为代码逻辑看起来都对,就是初始化漏了一行。所以,初始化一定要做彻底。
顺序栈的完整示例
下面是一个完整的顺序栈使用示例:
#include <stdio.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} SeqStack;
void InitStack(SeqStack *stack) {
stack->top = -1;
}
int Push(SeqStack *stack, int value) {
if (stack->top >= MAX_SIZE - 1) return -1;
stack->data[++stack->top] = value;
return 0;
}
int Pop(SeqStack *stack, int *value) {
if (stack->top == -1) return -1;
*value = stack->data[stack->top--];
return 0;
}
int main() {
SeqStack stack;
InitStack(&stack);
Push(&stack, 10);
Push(&stack, 20);
Push(&stack, 30);
int val;
while (Pop(&stack, &val) == 0) {
printf("弹出: %d\n", val);
}
// 输出顺序: 30, 20, 10
return 0;
}
运行结果会依次打印 30、20、10。你看,后进去的 30 最先出来,这就是 LIFO 的直观体现。
知识体系图
下面这张图帮你理清顺序栈的核心脉络:
顺序栈的实现思路很直白,但越简单的东西越容易在细节上翻车。你写代码时多留个心眼,把边界条件都考虑清楚,基本就稳了。
公众号:蓝海资料掘金营,微信deep3321