🍱

栈(Stack)

一摞便当盒:最后放进去的,最先拿得到——"后进先出"。

💡 打个比方

想象你往桌上一摞便当盒里放东西:你只能往最顶上放(push),也只能从最顶上拿(pop)——想拿最底下那个?得先把上面的全搬走!这就是栈(stack,一摞)的核心规矩:后进先出(LIFO,Last In First Out)。弹夹也是:最后压进去的子弹最先打出去。栈只露出"顶"这一个口,每次操作只看栈顶,所以极快(O(1))。

术语:栈顶 top(最上面那个元素);入栈 push(放上去);出栈 pop(拿走顶层);栈空 empty(一个都没有了)。栈底 = 最先放进去、最后才能拿到的那个。

🖼️ 大图看明白

左边:便当盒栈(只碰最顶) 🍜 拉面盒(第 1 个放,最底) 🍙 饭团盒(第 2 个放) 🍣 寿司盒(第 3 个放) 🥚 鸡蛋盒(第 4 个放,最顶) 栈顶 top push 从顶上放 pop 只从顶上拿 想拿最底的拉面?先把 3 个盒子都搬走! 右边:数组实现的栈(top 记到第几个) 4 a[3] a[4] a[5] a[6] 1 a[0] 2 a[1] 3 a[2] top = 3 栈底在 a[0],栈顶是 a[top]——数组不用搬,只改 top 数字!

🧩 子任务 1:LIFO——预测"谁先出"

🎯 小目标:看懂"后进先出",能预测一串 push / pop 操作的输出顺序。

规则只有一条:pop 永远弹出当前栈顶。跟着下面的操作,在纸上画便当盒,预测输出:

// 初始:栈空
push 1
push 2
push 3
pop   // → 输出 ?
pop   // → 输出 ?
push 4
pop   // → 输出 ?

自己先推一遍,再对答案。

✅ 小检查:三次 pop 依次输出什么?
看看答案
依次输出 3、2、4。
  • push 1,2,3 后,栈从底到顶是 [1,2,3],栈顶是 3。
  • 第 1 次 pop → 3(最后放进去的,最先出来)。
  • 第 2 次 pop → 2(现在栈顶)。
  • push 4 后栈是 [1,4],第 3 次 pop → 4。
1 一直在最底下"压舱",全程没出场。
易错:别把栈想成排队!排队是先来先走(那是队列,下一课的主角);栈是后放先走。

🧩 子任务 2:数组实现栈——top 指针与四操作

🎯 小目标:看懂 top 指针,掌握 push / pop / top / empty 四个操作,能手推执行过程。

用数组存栈,只需要一个变量 top 记住"栈顶在第几格":

注意:栈底永远在 a[0],数组不用搬,每次只改 top 这个数字——所以 push、pop 都是 O(1),飞快。

// 数组实现栈(top = 栈顶元素的下标;top == -1 表示空)
int a[100];
int top = -1;

// 入栈:顶 +1,再放
void push(int x) { a[++top] = x; }

// 出栈:拿走,顶 -1
int pop() { return a[top--]; }

// 看栈顶(不拿走)
int topVal() { return a[top]; }

// 判空
bool empty() { return top == -1; }

手推一遍下面的操作,每步写下 top 和数组:

push 10
push 20
pop        // 输出 ?
topVal()   // 得到 ?
✅ 小检查:第 3 步 pop 输出什么?第 4 步 topVal() 得到什么?此时 top 等于几?
看看答案
  • push 10:top = 0,a[0] = 10。
  • push 20:top = 1,a[1] = 20。
  • pop:输出 20,然后 top = 0。
  • topVal():读 a[0] = 10(栈顶现在是 10,没拿走)。
  • 结束时 top = 0。
口诀:push 先加后放,pop 先拿后减。

🧩 子任务 3:应用①——括号匹配

🎯 小目标:用栈判断一串括号是否"正确嵌套"(这是栈最经典的应用)。

看一段代码里的括号:(a + (b * c))——每个 ( 都要找到"自己那个" )。为什么正好用栈?因为最后打开的括号,必须最先关上——这就是 LIFO!

算法三步走:

// 括号匹配
#include <iostream>
using namespace std;

bool match(string s) {
    int st[100], top = -1;
    for (int i = 0; i < s.size(); i++) {
        if (s[i] == '(') top++;              // 压栈
        else if (s[i] == ')') {
            if (top == -1) return false;     // 没有可配的
            top--;                           // 弹一个,配一对
        }
    }
    return top == -1;                        // 最后必须全配完
}
✅ 小检查:判断这两个串:① ((())) ② ()(())——各自成功还是失败?哪一步暴露的问题?
看看答案
  • ① ((())):成功。( ( ( 三次压栈 top=2;) ) ) 三次弹栈 top 回到 -1,走完栈空 ✓。
  • ② ()(()):成功。( 压;) 弹(空);( 压;( 压;) 弹;) 弹——最后栈空 ✓。
两个都成功,正好演示了两种"配"的方式:先开后关和先关再开。 换成会失败的例子感受一下: (() → 走完还剩 1 个未关的 ( → 失败("栈不空"暴露); ))(( → 第 1 个字符就遇到 ) 而栈空 → 失败("栈空还弹"暴露)。
易错:栈只记"数量"就够了(top 就是计数),因为只有一种括号;如果还要支持 [、{,就得把左括号本身压进去,右括号来时比对是不是同一对——这就是"栈存什么"的设计题。

🧩 子任务 4:应用②——用栈算"逆波兰式"

🎯 小目标:看懂"运算符放在后面"的写法,能用栈一步一步算出结果(计算机真的这么干)。

平常我们写 3 + 4(运算符在中间)。如果把运算符挪到最后:3 4 + 就表示 3+4。这叫逆波兰式(后缀式)。妙处:不需要括号、不需要优先级——从左往右读,见到运算符,它前面的两个数就是它的操作数。

用栈算它,规则只有两条:

为什么弹的顺序是"先右后左"?因为减和除不满足交换律:栈顶是最后来的数,它正好是运算符右边的数。

手推一道:5 1 2 + 4 * + 3 -(它 = (5 + (1+2)×4) − 3)。

✅ 小检查:按规则一步步推,每步写下栈的内容(底→顶)和最终答案。
看看答案
读到的动作栈(底→顶)
5压入[5]
1压入[5,1]
2压入[5,1,2]
+弹 2、1 → 1+2=3,压入[5,3]
4压入[5,3,4]
*弹 4、3 → 3×4=12,压入[5,12]
+弹 12、5 → 5+12=17,压入[17]
3压入[17,3]
-弹 3、17 → 17−3=14,压入[14]
最终答案 14 ✓。栈里"最后压进来的操作数"总是最先用——LIFO 又一次立功。
小知识:平时写的 3 + 4 * 5(中缀)要处理优先级和括号,很麻烦。计算机常常先用栈把它翻译成逆波兰式 3 4 5 * +,再用栈求值——栈是表达式求值的发动机。

💻 完整代码:逆波兰式求值机

// 输入一行逆波兰式(数字与运算符用空格分开,正整数)
// 例如:5 1 2 + 4 * + 3 -   →  输出 14
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    getline(cin, s);
    int st[100], top = -1;
    int i = 0, n = s.size();
    while (i < n) {
        while (i < n && s[i] == ' ') i++;      // 跳过空格
        if (i >= n) break;
        if (s[i] == '+' || s[i] == '-' || s[i] == '*' || s[i] == '/') {
            int b = st[top--];                  // 先弹:右操作数
            int a = st[top--];                  // 后弹:左操作数
            if (s[i] == '+')      st[++top] = a + b;
            else if (s[i] == '-') st[++top] = a - b;
            else if (s[i] == '*') st[++top] = a * b;
            else                  st[++top] = a / b;
            i++;
        } else {
            int x = 0;                          // 读一个正整数
            while (i < n && s[i] != ' ') x = x * 10 + (s[i] - '0'), i++;
            st[++top] = x;
        }
    }
    cout << st[top] << endl;                     // 最后栈顶就是答案
    return 0;
}

✋ 你来试试

第 1 题:push 5、push 6、pop、push 7、pop、pop——三次 pop 的输出依次是什么?最后栈空吗?
push 5 → [5];push 6 → [5,6];pop → 6,剩 [5];push 7 → [5,7];pop → 7,剩 [5];pop → 5,栈空。输出 6、7、5,最后空。每次都弹的是当时最顶。
第 2 题:判断 (() 括号匹配成功还是失败?是哪一步暴露的?
失败。第 1 个 ( 压栈(top=0);第 2 个 ) 弹掉(top=-1);第 3 个 ( 压栈(top=0)。走完还有 1 个 ( 没关,"最后栈不空"这一步判出失败。
第 3 题:用栈算 8 2 / 3 +(= 8÷2+3),写出每步的栈和最终结果。
8 → [8];2 → [8,2];/ → 弹 2、8,8÷2=4,[4];3 → [4,3];+ → 弹 3、4,4+3=7,[7]。最终结果 7。注意除法:先弹的 2 是被除数右边的数(8÷2,不是 2÷8)。
🎯 一句话记住:栈 = 一摞便当盒,后进先出;top 只露一个口,push/pop 都 O(1);括号匹配和逆波兰求值是它的两道招牌菜。