写代码这些年,我越来越觉得“栈”是那种看着不起眼、实际无处不在的数据结构。平时聊“技术栈”的人很多,但真正理解数据结构里那个“栈”的人反而没那么多。一旦你把栈的原理吃透,再看函数调用、括号匹配、浏览器后退、表达式求值这些场景,全都能串起来。这篇文章就围绕“栈”展开,我会从最基础的概念讲起,用代码一步步实现它,再带你把几个经典应用场景逐个拆完。无论你是准备期末复习、考研看严蔚敏那本教材,还是工作中想补一补基本功,这篇都能给你一些可直接落地的思路。
1. 你每天都在用栈,只是可能没意识到
1.1 浏览器后退与编辑器撤销背后的共同答案
我经常在课上问学生一个问题:你在浏览器里点“后退”按钮的时候,浏览器是怎么知道要回到哪个页面的?答案就是栈。
它的模型极其简单。打开新页面,把旧页面压入一个栈;点击后退,从栈顶弹出最近一次访问的页面。这个行为恰好符合我们后来要讲的后进先出(LIFO)规则。
同样,编辑器里的 Ctrl+Z 撤销操作,本质也是一个栈。每次编辑,把“上一步操作”的记录放进栈里;按撤销键时,从栈顶把最近的那一步操作“拿出来回退”。如果你连续撤销了 5 步,你会发现撤销顺序和你当初的操作顺序是完全相反的。这背后就是栈。
1.2 一句话看懂栈的定义与操作
栈可以理解为一个“只允许在一端进出”的线性表。这一端叫栈顶,另一端叫栈底。它的核心规则只有八个字:后进先出,先进后出。英文缩写就叫 LIFO(Last In First Out)。
操作层面也就四个基本动作:
- push:把元素压入栈顶
- pop:把栈顶元素弹出
- peek / top:看一眼栈顶元素,但不弹出
- isEmpty:判断栈是否为空
你想象一摞盘子,往上放盘子是 push,从最上面拿走盘子是 pop,你永远只能碰到最上面那个盘子。这就是栈。
概念听上去太简单了,简单到很多初学者觉得“这有啥可学的”。但恰恰是这种“受限”的结构,在计算机世界里被用到了极致。
1.3 为什么一个“功能受限”的结构反而到处都在用
这里有一个很关键的点,很多人学的时候忽略了。线性表本身既可以头插,也可以尾插,还可以任意位置插入删除。而栈把自己限制成只能在一端操作,这不就少了很多灵活性吗?为什么还要用它?
我的理解是:限制操作范围,才能换来安全与清晰。 就好比现实中排队,如果所有人都可以在任意位置插队,整个队伍就乱套了。栈只允许在同一端进出,让“最近添加的数据最先被处理”这件事变成必然,这正是函数调用、递归、回溯等问题需要的天然模式。
再者,栈的设计让代码结构变得非常可控。你只需要关注栈顶,不需要管中间的数据。漏掉越界、错序这类低级问题概率就会大幅下降。所以它特别适合处理“一层套一层”的嵌套问题,比如括号匹配、HTML 标签闭合、函数调用链。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手动实现一个栈,把每一个细节都弄明白
2.1 两种存法:数组还是链表
理解栈最好的方式是自己动手写一遍。栈的实现有两大流派:顺序栈和链栈。
顺序栈,本质是一个数组加一个指向栈顶的“指针”(或用整型下标)。所有的 push 和 pop 都发生在数组尾部,所以顺序栈的操作实际上就是操作数组的最后一个元素。
链栈,本质是一个单链表,并且只能在头结点处插入和删除。也就是说链栈的栈顶就是链表的头结点。
顺序栈的优点在于内存连续、CPU 缓存命中率高、访问效率稳定。缺点在于容量固定,满了要扩容。链栈不怕扩容,随时来一个元素就临时分配一个新节点,但每个节点都要额外存一个指针,内存开销偏大。
我自己写工程代码时,大部分情况下首选顺序栈,因为无论是 STL 的 deque 还是动态数组,都可以轻松实现一个不会“爆”的顺序栈。
2.2 C 语言实现一个能扩容的顺序栈
以严蔚敏版教材为底子,我调整成了一个更适合工程实践的版本。它支持动态扩容,避免一开始就把容量写死。
c复制#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data; // 动态数组
int top; // 栈顶下标,初始化为 -1 表示空栈
int capacity; // 当前容量
} SeqStack;
void initStack(SeqStack *s, int cap) {
s->data = (int *)malloc(sizeof(int) * cap);
if (!s->data) exit(-1);
s->top = -1;
s->capacity = cap;
}
int isEmpty(SeqStack *s) {
return s->top == -1;
}
void push(SeqStack *s, int value) {
if (s->top >= s->capacity - 1) { // 栈满,扩容
s->capacity *= 2;
s->data = (int *)realloc(s->data,
