数据结构中的栈:原来"后进先出"这么有用!
什么是栈?诈一听可能不太容易理解,其实计算机里面的很多知识点都源于生活。举个生活中的场景:厨房里面刷盘子;古代军队里面行军打仗,士兵们排队按顺序前进,突然遇到敌情需要后队变前队,前队变后队撤离,等等很多场景本质都是栈的原理。今天我就用最通俗易懂的方式,带你彻底掌握栈这个数据结构。
在讲解之前,先给大家演示下效果:

什么是栈?从叠盘子说起
先做个简单实验:拿出几个盘子,一个一个叠起来。你会发现:
- 新盘子总是放在最上面
- 取盘子时也是从最上面开始拿
- 最先放进去的盘子最后才能拿到
这就是栈的核心理念——LIFO(Last In First Out,后进先出)。
刚开始学编程时,老师用"手枪弹夹"来比喻栈,我觉得特别形象:
- 压子弹 = 入栈(最后压入的子弹在最上面)
- 射击 = 出栈(最上面的子弹最先射出)
栈的基本操作:
栈的所有操作都围绕着"栈顶"进行,就像你只能从书堆的最上面拿书一样。下面以书架存放书的生活场景来演示栈的操作全过程。
1. 初始化栈(准备一个空书架)
// 先准备一个空的书架(栈)
#define MAX_BOOKS 10 // 书架最多放10本书
typedef struct {
char *books[MAX_BOOKS]; // 书架空间
int top; // 指向最上面那本书的位置
} BookShelf;
// 初始化空书架
void initShelf(BookShelf *shelf) {
shelf->top = -1; // -1 表示书架是空的
printf("书架已清空,可以开始放书了!\n");
}
理解技巧:把 top = -1 想象成"还没有任何书",就像空书架上的指针指向"NULL"。
2. 入栈操作(往书架上放书)
// 往书架上放一本新书
int pushBook(BookShelf *shelf, char *bookName) {
// 先检查书架是否已满
if (shelf->top >= MAX_BOOKS - 1) {
printf("书架满了!《%s》放不下了\n", bookName);
return 0; // 放书失败
}
// 放书步骤:先把指针往上移一格,然后放书
shelf->top = shelf->top + 1; // 指针上移
shelf->books[shelf->top] = bookName; // 放入新书
printf("放入新书:《%s》,现在最上面是第%d本书\n",
bookName, shelf->top + 1);
return 1; // 放书成功
}
实际例子:
初始:书架空,top = -1
放入《C语言》:top = 0,books[0] = "C语言"
放入《算法》:top = 1,books[1] = "算法"
放入《数据结构》:top = 2,books[2] = "数据结构"
3. 出栈操作(从书架取书)
// 从书架拿走最上面的书
char* popBook(BookShelf *shelf) {
// 先检查书架是否为空
if (shelf->top == -1) {
printf("书架空了,没有书可拿了!\n");
return NULL;
}
// 取书步骤:先拿到书,再把指针下移
char *book = shelf->books[shelf->top]; // 拿到最上面的书
shelf->top = shelf->top - 1; // 指针下移
printf("拿走书本:《%s》,还剩%d本书\n", book, shelf->top + 1);
return book;
}
取书过程:
书架状态:["C语言", "算法", "数据结构"],top = 2
第一次取:拿到《数据结构》,top = 1
第二次取:拿到《算法》,top = 0
第三次取:拿到《C语言》,top = -1
4. 查看栈顶(看看书架最上面是什么书)
// 看看最上面是什么书,但不拿走
char* peekBook(BookShelf *shelf) {
if (shelf->top == -1) {
printf("📭 书架是空的!\n");
return NULL;
}
return shelf->books[shelf->top]; // 只读不取
}
5. 检查书架状态
// 检查书架是否为空
int isShelfEmpty(BookShelf *shelf) {
return shelf->top == -1; // -1 表示空书架
}
// 检查书架是否已满
int isShelfFull(BookShelf *shelf) {
return shelf->top == MAX_BOOKS - 1; // 达到最大容量
}
// 查看书架上有多少本书
int bookCount(BookShelf *shelf) {
return shelf->top + 1; // 书数量 = 指针位置 + 1
}
栈的完整过程演示
让我们用实际代码演示整个放书取书过程:
void bookDemo() {
BookShelf myShelf;
initShelf(&myShelf); // 初始化空书架
printf("\n=== 开始放书 ===\n");
pushBook(&myShelf, "C语言入门");
pushBook(&myShelf, "算法图解");
pushBook(&myShelf, "数据结构精讲");
pushBook(&myShelf, "操作系统原理");
printf("\n=== 偷偷看一下最上面是什么书 ===\n");
char *topBook = peekBook(&myShelf);
printf("最上面的书是:《%s》(没有拿走哦)\n", topBook);
printf("\n=== 开始取书 ===\n");
while (!isShelfEmpty(&myShelf)) {
popBook(&myShelf);
}
printf("\n=== 尝试从空书架取书 ===\n");
popBook(&myShelf); // 这会显示错误信息
}
运行结果:
书架已清空,可以开始放书了!
=== 开始放书 ===
放入新书:《C语言入门》,现在最上面是第1本书
放入新书:《算法图解》,现在最上面是第2本书
放入新书:《数据结构精讲》,现在最上面是第3本书
放入新书:《操作系统原理》,现在最上面是第4本书
=== 看看书架最上面是什么书 ===
最上面的书是:《操作系统原理》(没有拿走哦)
=== 开始取书 ===
拿走书本:《操作系统原理》,还剩3本书
拿走书本:《数据结构精讲》,还剩2本书
拿走书本:《算法图解》,还剩1本书
拿走书本:《C语言入门》,还剩0本书
=== 尝试从空书架取书 ===
书架空了,没有书可拿了!
看到没有?最后放进去的《操作系统原理》最先被拿出来,这就是栈的"后进先出"特性!
栈的5大应用场景:栈无处不在!
场景1:浏览器前进后退
每次我写网页时都在想:为什么浏览器能记住我访问的页面顺序?答案就是双栈技术!
typedef struct {
BookShelf backStack; // 后退栈:存放访问过的页面
BookShelf forwardStack; // 前进栈:存放后退时暂存的页面
char *currentPage; // 当前页面
} WebBrowser;
// 访问新页面
void visitPage(WebBrowser *browser, char *page) {
printf("\n访问新页面:%s\n", page);
// 如果当前有页面,把它放入后退栈
if (browser->currentPage != NULL) {
pushBook(&browser->backStack, browser->currentPage);
printf(" 把当前页面《%s》放入后退栈\n", browser->currentPage);
}
// 清空前进栈(因为新访问会破坏前进链)
while (!isShelfEmpty(&browser->forwardStack)) {
char *temp = popBook(&browser->forwardStack);
printf(" 清空前进栈中的《%s》\n", temp);
}
browser->currentPage = page;
}
// 后退功能
void goBack(WebBrowser *browser) {
if (isShelfEmpty(&browser->backStack)) {
printf("已经是最早的页面了,无法后退!\n");
return;
}
printf("\n点击后退按钮\n");
// 当前页面放入前进栈
pushBook(&browser->forwardStack, browser->currentPage);
printf(" 把《%s》放入前进栈\n", browser->currentPage);
// 从后退栈取出上一个页面
browser->currentPage = popBook(&browser->backStack);
printf(" 从后退栈取出《%s》作为当前页面\n", browser->currentPage);
}
// 前进功能
void goForward(WebBrowser *browser) {
if (isShelfEmpty(&browser->forwardStack)) {
printf("已经是最新的页面了,无法前进!\n");
return;
}
printf("\n点击前进按钮\n");
// 当前页面放入后退栈
pushBook(&browser->backStack, browser->currentPage);
printf(" 把《%s》放入后退栈\n", browser->currentPage);
// 从前进栈取出下一个页面
browser->currentPage = popBook(&browser->forwardStack);
printf(" 从前进栈取出《%s》作为当前页面\n", browser->currentPage);
}
实际演示:
void browserDemo() {
WebBrowser myBrowser;
initShelf(&myBrowser.backStack);
initShelf(&myBrowser.forwardStack);
myBrowser.currentPage = NULL;
printf("=== 浏览器使用演示 ===\n");
visitPage(&myBrowser, "首页");
visitPage(&myBrowser, "产品页");
visitPage(&myBrowser, "详情页");
goBack(&myBrowser); // 回到产品页
goBack(&myBrowser); // 回到首页
goForward(&myBrowser); // 前进到产品页
visitPage(&myBrowser, "关于我们"); // 新访问,会清空前进栈
goForward(&myBrowser); // 这里会失败,因为前进栈被清空了
}
场景2:函数调用栈(程序的记忆大师)
每次调用函数,计算机都在用栈来记住"要回到哪里":
void functionA() {
printf("🔹 进入A函数\n");
printf(" 在A函数中做一些工作...\n");
functionB(); // 调用B函数
printf("🔹 回到A函数,继续工作...\n");
printf("🔹 离开A函数\n");
}
void functionB() {
printf(" 🔸 进入B函数\n");
printf(" 在B函数中做一些工作...\n");
functionC(); // 调用C函数
printf(" 🔸 回到B函数,继续工作...\n");
printf(" 🔸 离开B函数\n");
}
void functionC() {
printf(" 🔹 进入C函数\n");
printf(" 在C函数中工作...\n");
printf(" 🔹 离开C函数\n");
}
// 演示函数调用
void functionCallDemo() {
printf("=== 函数调用栈演示 ===\n");
printf("开始调用A函数:\n");
functionA();
printf("所有函数调用结束!\n");
}
调用过程就像叠盘子:
开始:[]
调用A:[A的返回地址]
调用B:[A的返回地址, B的返回地址]
调用C:[A的返回地址, B的返回地址, C的返回地址]
C返回:[A的返回地址, B的返回地址]
B返回:[A的返回地址]
A返回:[]
场景3:文本编辑器的撤销功能
我写文档时经常用Ctrl+Z,这背后就是栈在帮忙:
typedef struct {
BookShelf undoStack; // 撤销栈:存放之前的状态
BookShelf redoStack; // 重做栈:存放撤销的状态
char *document; // 当前文档内容
} TextEditor;
void typeText(TextEditor *editor, char *newText) {
printf("输入文字:%s\n", newText);
// 保存当前状态到撤销栈
if (editor->document != NULL) {
pushBook(&editor->undoStack, editor->document);
}
// 清空重做栈(新输入会破坏重做链)
while (!isShelfEmpty(&editor->redoStack)) {
popBook(&editor->redoStack);
}
editor->document = newText;
}
void undo(TextEditor *editor) {
if (isShelfEmpty(&editor->undoStack)) {
printf("无法撤销了!\n");
return;
}
printf("执行撤销操作\n");
// 当前状态保存到重做栈
if (editor->document != NULL) {
pushBook(&editor->redoStack, editor->document);
}
// 从撤销栈恢复之前的状态
editor->document = popBook(&editor->undoStack);
printf(" 文档恢复到:%s\n", editor->document);
}
void redo(TextEditor *editor) {
if (isShelfEmpty(&editor->redoStack)) {
printf("无法重做了!\n");
return;
}
printf("执行重做操作\n");
// 当前状态保存到撤销栈
if (editor->document != NULL) {
pushBook(&editor->undoStack, editor->document);
}
// 从重做栈恢复之后的状态
editor->document = popBook(&editor->redoStack);
printf(" 文档重做到:%s\n", editor->document);
}
场景4:计算表达式
栈还能帮我们计算数学表达式,比如 3 + 4 × (2 - 1):
// 简单的表达式计算演示
void calculateDemo() {
BookShelf numberStack; // 数字栈
initShelf(&numberStack);
printf("=== 表达式计算演示 ===\n");
// 模拟计算 3 + 4 × 2
printf("计算:3 + 4 × 2\n");
// 放入数字
pushBook(&numberStack, "3");
pushBook(&numberStack, "4");
pushBook(&numberStack, "2");
printf("数字栈状态:");
for (int i = 0; i <= numberStack.top; i++) {
printf("%s ", numberStack.books[i]);
}
printf("\n");
// 先计算乘法:4 × 2 = 8
char *num2 = popBook(&numberStack); // 取出2
char *num1 = popBook(&numberStack); // 取出4
int result = atoi(num1) * atoi(num2); // 4 × 2 = 8
// 结果放回栈中
char resultStr[10];
sprintf(resultStr, "%d", result);
pushBook(&numberStack, resultStr);
printf("计算 4 × 2 = 8,结果放回栈中\n");
// 再计算加法:3 + 8 = 11
num2 = popBook(&numberStack); // 取出8
num1 = popBook(&numberStack); // 取出3
result = atoi(num1) + atoi(num2); // 3 + 8 = 11
printf("最终结果:3 + 8 = %d\n", result);
}
数组栈 vs 链表栈:怎么选择?
数组栈好比固定书架
// 适合:知道最大数据量的情况
typedef struct {
char *data[100]; // 固定大小数组
int top;
} ArrayStack;
优点:速度快、内存连续
缺点:大小固定、可能溢出
使用场景:嵌入式系统、性能要求高的场景
链表栈好比无限书架
// 适合:数据量不确定的情况
typedef struct StackNode {
char *data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top;
int size;
} LinkedListStack;
优点:动态扩容、永不溢出(除非内存满)
缺点:需要额外指针、访问稍慢
使用场景:通用应用程序、数据量变化大的场景
栈的使用技巧
什么时候用栈?
- ✅ 需要"撤销"功能时(编辑器、Photoshop)
- ✅ 处理嵌套结构时(括号、HTML标签)
- ✅ 深度优先搜索时(迷宫、树遍历)
- ✅ 函数调用管理时(所有编程语言)
- ✅ 表达式计算时(计算器、编译器)
什么时候不用栈?
- ❌ 需要随机访问元素时(用数组)
- ❌ 需要频繁在中间插入删除时(用链表)
- ❌ 需要先进先出时(用队列)
避坑
- 栈溢出:push前一定要检查是否已满
- 栈下溢:pop前一定要检查是否为空
- 内存泄漏:链表栈记得free节点
- 并发安全:多线程环境要加锁
最后的话
核心:单一入口、单一出口、只能在栈顶操作
通过栈映射出的生活道理:一次只做一件事,反而能做得更好,共勉!!!

被折叠的 条评论
为什么被折叠?



