《数据结构:从0到1》-08-栈

数据结构中的栈:原来"后进先出"这么有用!

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

在讲解之前,先给大家演示下效果:
栈

什么是栈?从叠盘子说起

先做个简单实验:拿出几个盘子,一个一个叠起来。你会发现:

  • 新盘子总是放在最上面
  • 取盘子时也是从最上面开始拿
  • 最先放进去的盘子最后才能拿到

这就是栈的核心理念——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标签)
  • ✅ 深度优先搜索时(迷宫、树遍历)
  • ✅ 函数调用管理时(所有编程语言)
  • ✅ 表达式计算时(计算器、编译器)

什么时候不用栈?

  • ❌ 需要随机访问元素时(用数组)
  • ❌ 需要频繁在中间插入删除时(用链表)
  • ❌ 需要先进先出时(用队列)

避坑

  1. 栈溢出:push前一定要检查是否已满
  2. 栈下溢:pop前一定要检查是否为空
  3. 内存泄漏:链表栈记得free节点
  4. 并发安全:多线程环境要加锁

最后的话

核心:单一入口、单一出口、只能在栈顶操作

通过栈映射出的生活道理:一次只做一件事,反而能做得更好,共勉!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QuantumLeap丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值