NOI C++ 课程体系 / 数据结构篇 / 第2讲
CSP-J 入门级 · 线性数据结构 · m5-02

栈
后进先出的线性表

从洗碗池里的盘子到括号匹配与表达式求值 —— 掌握栈的定义与基本操作,彻底搞懂栈顶指针的两种约定,熟练运用数组模拟与 STL stack 解决括号匹配、表达式求值、递归与出栈序列等经典问题。

6 大模块 2 种实现方式 3 类经典应用 5 大易错点 12+20 道练习
本讲 · 第 42/56 讲栈
模块 01

情景导航

从一摞洗好的盘子开始,直觉理解"后来者先走"的秩序。

故事洗碗池里的盘子

晚饭后,一家人把洗好的盘子一个一个叠在水池边。妈妈洗好第一个,放在最底下;接着第二个叠在它上面,第三个再叠上去……

↑ 第三个(最后洗的)/第二个/第一个(最先洗的)↓

开饭时,爸爸来拿盘子。他只能从最上面开始拿 —— 拿走的是最后洗好的第三个;想拿到最底下那个,必须先把上面两个都拿走。

这摞盘子有两个天然限制:

① 新盘子只能放在最上面(不可能塞到中间或底下);

② 取盘子也只能从最上面取(不可能从中间抽走一个)。

最后放上去的,最先被拿走 —— 这就是栈的"后进先出"!🍽️

这种"只能在一端操作"的结构,在程序里就叫栈(Stack)。那个唯一能进能出的一端,叫栈顶;压在最底下、动不了的一端,叫栈底。

💡 和队列对比一下

如果是一队人排队打饭,先来的先打 —— 那是队列(先进先出 FIFO)。

而栈像一摞盘子或一条死胡同:只有一端开口,后进去的必定先出来(LIFO)。

①生活类比:栈无处不在
🥫

超市货架罐头

补货员从前面一个个往里推,顾客只能拿最前面那个,最后补的先卖掉。

→ 后进先出 LIFO
↩️

编辑器的"撤销"

每次操作压入撤销栈,Ctrl+Z 撤销的永远是最近一次操作。

→ 栈顶 = 最近操作
🧭

浏览器的"后退"

每打开一个新页面就压栈,点后退回到上一个页面,正是弹出栈顶。

→ pop 回到上一层
🔫

弹夹压子弹

子弹从顶部一颗颗压进去,射击时最后压进去的那一颗最先被击发。

→ 只能操作栈顶
②编程定义

栈(Stack)是一种操作受限的线性表:只允许在一端进行插入和删除,这一端称为栈顶(top);另一端固定不动,称为栈底(bottom)。

它的核心特性是 后进先出(LIFO,Last In First Out):最后进入栈的元素,必定最先离开栈。插入叫入栈 / 压栈(push),删除叫出栈 / 弹栈(pop)。

📌 本讲核心知识地图

板块核心内容对应大纲
栈的概念定义、LIFO 特性、栈顶栈底、基本操作栈的基本概念
顺序栈数组实现、top 指针的两种约定、上溢与下溢栈的顺序存储
栈的实现数组模拟栈、STL stack 的六大成员函数栈的基本操作
栈的应用括号匹配、表达式求值、递归与出栈序列栈的应用
③核心口诀
🔑 栈

只能从栈顶进出,中间不许插手;
后进先出 LIFO,压栈 push,弹栈 pop;
看栈顶 top,数个数 size,
取之前一定先判空!

🔑 顺序栈

数组加个 top,元素个数就是 top;
入栈 st[top++]=x,出栈只要 top--;
判空 top==0,判满 top==MAXN;
出栈空出的位置,下次直接覆盖用!

模块 02

知识探究

从数组 + 栈顶指针出发,搞懂两种 top 约定,再掌握竞赛中必备的两种写法与三大应用。

1栈的基本概念 核心

结构约定:栈可以是空的,也可以存若干个元素。无论何时,能操作的只有栈顶那一个元素。栈底的元素必须等它上面的所有元素都出栈之后,才轮得到它。

栈顶与栈底
  • 栈顶 top:唯一允许插入与删除的一端
  • 栈底 bottom:固定不动的另一端
  • 空栈:一个元素都没有
  • 栈中元素按"栈底 → 栈顶"排列
五种基本操作
  • push(x):压入栈顶
  • pop():弹出栈顶
  • top():读取栈顶元素
  • empty():判断是否为空
  • size():返回元素个数
时间复杂度
  • 入栈:O(1)
  • 出栈:O(1)
  • 取栈顶:O(1)
  • 查找第 i 个:O(n)(不支持随机访问)
线性结构插入端删除端核心特点CSP-J 常见用途
数组(普通线性表)任意位置任意位置随机访问 O(1)存数据、前缀和
栈(Stack)栈顶栈顶后进先出 LIFO括号匹配、表达式求值、递归
队列(Queue)队尾队头先进先出 FIFOBFS、约瑟夫、排队模拟
2顺序栈与栈顶指针 必会
▶ 栈动画演示LIFO · 后进先出

顺序栈:用数组 st[MAXN] 存储,配一个整型变量 top 充当栈顶标记。注意这里的 top 不是 C++ 指针,只是数组下标。

本讲统一采用"top 指向下一个可插入位置"的约定:初始 top = 0,入栈时先写 st[top] 再 top++,出栈时直接 top--。此时 top 的值恰好等于元素个数,栈顶元素是 st[top-1]。

📊 顺序栈演示:数组 st[6](下标 0~5,从左往右是栈底 → 栈顶)

🥞 出栈空出来的位置,下一次 push 会直接覆盖复用
初始top=0
0·
1·
2·
3·
4·
5·
size=0 · 栈空
push Atop=1
0A
1·
2·
3·
4·
5·
size=1
push Btop=2
0A
1B
2·
3·
4·
5·
size=2
push Ctop=3
0A
1B
2C
3·
4·
5·
size=3
push Dtop=4
0A
1B
2C
3D
4·
5·
size=4
pop → Dtop=3
0A
1B
2C
3D
4·
5·
size=3 · top 退回 3,位置 3 立刻空闲
pop → Ctop=2
0A
1B
2C
3D
4·
5·
size=2
push Etop=3
0A
1B
2E
3D
4·
5·
size=3 · 直接复用刚才 C 的位置
push Ftop=4
0A
1B
2E
3F
4D
5·
size=4
push Gtop=5
0A
1B
2E
3F
4G
5·
size=5
push Htop=6
0A
1B
2E
3F
4G
5H
size=6 · 6 个格子全部装满
push Itop=6
0A
1B
2E
3F
4G
5H
size=6 · top==MAXN,这才是真·栈满

💡 关键结论:栈不存在"假溢出"

回顾上一讲:队列的 head 和 tail 都只朝一个方向走,出队空出来的前半段永远用不上,于是有了"假溢出",不得不引入循环队列。

而栈的入栈和出栈在同一个位置(栈顶)发生 —— 出栈只是 top--,下一次入栈会原封不动地写回同一个格子。所以上例中 6 个格子能实实在在装满 6 个元素。

一句话记住:队列要绕圈,栈不用绕圈;只有当 top == MAXN 时,栈才真的满了。

C++seq_stack.cpp — 顺序栈的完整实现
#include <iostream>
using namespace std;
const int MAXN = 6;
int st[MAXN];
int top = 0;                  // top 指向"下一个可插入位置",同时就是元素个数

void push(int x) {
    if (top == MAXN) { cout << "栈满(溢出)!" << endl; return; }
    st[top++] = x;            // 入栈:放到 top 位置,top 上移
}
void pop() {
    if (top == 0) { cout << "栈空!" << endl; return; }
    top--;                    // 出栈:top 下移(原位置的数据不用管)
}
int main() {
    push(1); push(2); push(3); push(4);      // top = 4
    pop(); pop();                            // 4、3 出栈,top 回到 2
    push(5); push(6); push(7); push(8);      // 复用空位,top 回到 6
    push(9);                                 // 只有 top==MAXN 时才真的满了
    return 0;
}
运行结果
栈满(溢出)!

🔍 关键细节分析

判空条件:top == 0。此时一个元素都没有,绝对不能再执行 pop 或读栈顶。

元素个数:top 本身(这是"指向下一个可插入位置"这一约定最大的好处)。

出栈为什么不用真的删除? top-- 之后,st[top] 里的旧数据依然躺在那儿,但它已经位于栈顶之上,逻辑上不属于栈,下一次 push 会把它覆盖掉。千万不要去读它。

时间复杂度:入栈、出栈、取栈顶全部是 O(1),没有移动元素的过程,这是栈最值钱的地方。

3两种 top 约定与上溢下溢 必考

关于 top 的含义,教材和题解里有两套约定。它们都对,但绝对不能混用 —— 混用是初学阶段最常见的错误来源。

对比项约定一:top 指向下一个可插入位置约定二:top 指向栈顶元素
初始值top = 0top = -1
判空top == 0top == -1
判满top == MAXNtop == MAXN - 1
入栈st[top++] = x;st[++top] = x;
出栈top--;top--;
读栈顶st[top - 1]st[top]
元素个数toptop + 1
本讲采用✅ 是(与 STL 的 size() 一致,不易算错)了解即可(B3 题有完整代码)
术语

上溢(Overflow)与下溢(Underflow)

上溢:栈已满还继续 push。数组模拟时会数组越界,轻则写到别人的内存里,重则直接 RE(运行错误)。

下溢:栈已空还继续 pop 或读栈顶。数组模拟时 top 会变成负数,访问 st[-1] 是未定义行为;STL 的 stack 同样不检查,也会出错。每次操作前先判空 / 判满,是写栈代码的铁律。

4数组模拟 vs STL stack 必会

竞赛中有两种写法:数组模拟栈(能看清内部细节、方便调试、适合初赛手推)和 STL stack(代码短、不易出错,实际复赛首选)。两种都必须会。

A写法一:数组模拟栈
C++array_stack.cpp — 数组模拟栈(竞赛推荐写法)
#include <iostream>
using namespace std;
const int MAXN = 1005;
int st[MAXN], top = 0;        // top 既是"下一个可插入位置",也是元素个数

bool empty() { return top == 0; }        // 判空
bool full()  { return top == MAXN; }     // 判满
int  size()  { return top; }             // 元素个数(直接就是 top!)
int  peek()  { return st[top - 1]; }     // 取栈顶元素(注意要 -1)

void push(int x) {
    if (full()) { cout << "Stack Full!\n"; return; }
    st[top++] = x;                       // 先放后加
}
void pop() {
    if (empty()) { cout << "Stack Empty!\n"; return; }
    top--;                               // 只需下移,原值下次会被覆盖
}

int main() {
    push(10); push(20); push(30);
    cout << peek() << endl;                  // 30
    pop();
    cout << peek() << " " << size() << endl; // 20 2
    push(40);
    cout << size() << endl;                  // 3
    while (!empty()) { cout << peek() << " "; pop(); }
    return 0;
}
运行结果
30
20 2
3
40 20 10
B写法二:STL stack

使用 STL 需要 #include <stack>。常用成员函数如下:

函数作用返回值注意事项
s.push(x)压入栈顶void唯一的插入方式
s.pop()弹出栈顶void不返回元素!要先用 top() 取
s.top()读取栈顶元素元素引用栈空时调用是未定义行为
s.size()元素个数无符号整数与 int 比较建议写 (int)s.size()
s.empty()是否为空bool遍历循环的条件
s.swap(t)与另一个栈交换内容void较少使用
C++stl_stack.cpp — STL stack 的基本操作
#include <iostream>
#include <stack>
using namespace std;
int main() {
    stack<int> s;
    s.push(10); s.push(20); s.push(30);     // 依次压栈
    cout << s.top() << " " << s.size() << endl;   // 栈顶 30,共 3 个
    s.pop();                                // 弹出栈顶(不返回元素!)
    cout << s.top() << " " << s.size() << endl;   // 20 2
    while (!s.empty()) {                    // 遍历栈的唯一方式:边取边弹
        cout << s.top() << " ";
        s.pop();
    }
    return 0;
}
运行结果
30 3
20 2
20 10
注意

STL 的 stack 没有 clear(),也不能用下标访问

stack 是容器适配器,只暴露栈顶,不支持 s[i]、s.begin()、范围 for 遍历。想清空只能 while(!s.empty()) s.pop();;如果确实需要遍历或按下标访问,请改用 vector 模拟栈(见模块四拓展)。

5栈 vs 队列:一张表彻底分清
对比项栈 stack队列 queue
进出规则后进先出 LIFO先进先出 FIFO
插入 / 删除端都在栈顶 push() / pop()队尾 push() / 队头 pop()
读取端只有栈顶 top()队头 front() 与队尾 back()
数组实现的下标只用一个 top,不用取模用 head、tail,必须取模
空间利用MAXN 个格子能存 MAXN 个循环队列只能存 MAXN-1 个
典型应用括号匹配、表达式求值、递归、DFSBFS、约瑟夫、排队模拟、缓冲区
共同点都是操作受限的线性表,插入删除都是 O(1),都不支持随机访问
口诀:栈是死胡同(后进先出),队列是通道(先进先出);栈只管一头,队列要看两头!
6栈的三大经典应用 必考
①应用一:括号匹配(P1739)

思路:遇到左括号就入栈(相当于"欠下一个待配对的右括号");遇到右括号时,检查栈顶的左括号是否与之同型 —— 是就弹出(配对成功),不是或栈已空就直接失败。全部读完后,栈必须为空(不能有余下的左括号)。

为什么必须看栈顶?因为括号是嵌套的:最后出现的左括号,必须最先被配对 —— 这正是 LIFO。

🧩 表达式 (([()])) 的匹配过程(左侧为栈底,黄色为当前栈顶)
初始top=0
0·
1·
2·
3·
4·
5·
6·
7·
栈空
读 (top=1
0(
1·
2·
3·
4·
5·
6·
7·
左括号入栈
读 (top=2
0(
1(
2·
3·
4·
5·
6·
7·
左括号入栈
读 [top=3
0(
1(
2[
3·
4·
5·
6·
7·
左括号入栈
读 (top=4
0(
1(
2[
3(
4·
5·
6·
7·
左括号入栈
读 )top=3
0(
1(
2[
3(
4·
5·
6·
7·
栈顶是 ( ,与 ) 匹配 → 弹出
读 ]top=2
0(
1(
2[
3(
4·
5·
6·
7·
栈顶是 [ ,与 ] 匹配 → 弹出
读 )top=1
0(
1(
2[
3(
4·
5·
6·
7·
匹配 → 弹出
读 )top=0
0(
1(
2[
3(
4·
5·
6·
7·
匹配 → 弹出
结束top=0
0·
1·
2·
3·
4·
5·
6·
7·
栈为空且中途没出错 → 输出 Yes
C++bracket.cpp — 括号匹配核心框架
#include <iostream>
#include <stack>
#include <string>
using namespace std;
bool match(char a, char b) {                 // a 是栈顶左括号,b 是当前右括号
    return (a == '(' && b == ')') || (a == '[' && b == ']');
}
int main() {
    string str;
    cin >> str;
    stack<char> s;
    bool ok = true;
    for (int i = 0; i < (int)str.size(); i++) {
        char c = str[i];
        if (c == '(' || c == '[') s.push(c);           // 左括号:入栈
        else {
            if (s.empty() || !match(s.top(), c)) {     // 没得配 或 类型不符
                ok = false; break;
            }
            s.pop();                                   // 配对成功:弹出栈顶
        }
    }
    if (!s.empty()) ok = false;                        // 还有没配完的左括号
    cout << (ok ? "Yes" : "No") << endl;
    return 0;
}
输入 (([()]))
Yes
②应用二:表达式求值(后缀 / 中缀转后缀)

后缀表达式(逆波兰式)把运算符写在两个操作数之后,例如 3 4 + 就是 3+4。它最大的好处是不需要括号 —— 用栈即可求值。

求值规则:遇到数字就入栈;遇到运算符就弹出两个栈顶元素做运算,再把结果压回去。先弹出的是右操作数,减法除法尤其不能弄反。

C++postfix.cpp — 后缀表达式求值
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int main() {
    string t;
    stack<int> s;
    while (cin >> t) {                       // 后缀表达式,以 @ 结束
        if (t == "@") break;
        if (t == "+" || t == "-" || t == "*" || t == "/") {
            int b = s.top(); s.pop();        // 先弹出的是右操作数,顺序不能反
            int a = s.top(); s.pop();        // 后弹出的是左操作数
            if (t == "+") s.push(a + b);
            else if (t == "-") s.push(a - b);
            else if (t == "*") s.push(a * b);
            else s.push(a / b);
        } else {                             // 数字:转成整数后入栈
            int x = 0;
            for (int i = 0; i < (int)t.size(); i++)
                x = x * 10 + (t[i] - '0');
            s.push(x);
        }
    }
    cout << s.top() << endl;                 // 最后栈中只剩答案
    return 0;
}
输入 3 12 4 + * 20 5 / - @
44

那么中缀表达式(我们平时写的 1+2*(3+4))怎么转后缀?同样用栈,栈里存运算符:

数字 → 直接输出;左括号 → 入栈;右括号 → 不断弹出栈顶并输出,直到弹出左括号;运算符 → 先弹出所有优先级 ≥ 自己的,再把自己入栈。读完把栈里剩下的全部输出。

C++infix2postfix.cpp — 中缀表达式转后缀
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int pri(char c) {                            // 优先级:乘除 2,加减 1,左括号 0
    if (c == '+' || c == '-') return 1;
    if (c == '*' || c == '/') return 2;
    return 0;
}
int main() {
    string s, out;
    cin >> s;
    stack<char> op;
    for (int i = 0; i < (int)s.size(); i++) {
        char c = s[i];
        if (c >= '0' && c <= '9') out += c;                  // 数字:直接输出
        else if (c == '(') op.push(c);                       // 左括号:入栈
        else if (c == ')') {                                 // 右括号:一直弹到左括号
            while (!op.empty() && op.top() != '(') { out += op.top(); op.pop(); }
            op.pop();                                        // 左括号弹出但不输出
        } else {                                             // 运算符:先弹出优先级 >= 自己的
            while (!op.empty() && pri(op.top()) >= pri(c)) { out += op.top(); op.pop(); }
            op.push(c);
        }
    }
    while (!op.empty()) { out += op.top(); op.pop(); }       // 剩余运算符全部输出
    cout << out << endl;
    return 0;
}
输入 1+2*(3+4)-5
1234+*+5-
③应用三:递归的本质就是栈

函数调用时,系统会把当前层的局部变量、参数、返回地址打包成一个"栈帧"压进系统栈;函数返回时再弹出。所以递归的"递"就是连续入栈,"归"就是连续出栈。

明白了这一点,任何递归都能用显式栈改写成非递归;反过来,递归层数太深导致爆栈(Stack Overflow),也是因为系统栈的空间被占满了。

C++recursion_stack.cpp — 递归的本质就是栈
#include <iostream>
#include <stack>
using namespace std;
// 递归的每一次调用,本质就是一次"入栈";每一次返回,就是一次"出栈"
int fact(int n) {
    if (n == 0) return 1;
    return n * fact(n - 1);          // 先算 fact(n-1),本层调用被压在系统栈里
}
// 用显式栈把上面这段递归改写成非递归
int fact_stack(int n) {
    stack<int> s;
    while (n > 0) { s.push(n); n--; }                 // "递":一路压栈
    int ans = 1;
    while (!s.empty()) { ans *= s.top(); s.pop(); }   // "归":一路弹栈
    return ans;
}
int main() {
    cout << fact(5) << " " << fact_stack(5) << endl;
    return 0;
}
运行结果
120 120
模块 03

实践应用

通过详细分析题,巩固栈的手推能力与建模能力。

题1栈操作过程追踪

题目:设栈用数组 st[100] 实现,top 指向下一个可插入位置,初始 top = 0。依次执行下列操作:

push(10)、push(20)、push(30)、pop()、push(40)、pop()、pop()、push(50)

请写出每一步之后 top 的值、栈中从栈底到栈顶的元素序列、元素个数,以及每次 pop 弹出的元素;最后说明栈顶元素是什么。

提示:出栈只是 top--,被弹出的那个数仍然留在数组里,下次 push 会把它覆盖掉。
查看参考答案

逐步追踪表

操作top栈底 → 栈顶个数本次弹出说明
初始0(空)0—top==0,栈空
push(10)1101—st[0]=10
push(20)210 202—st[1]=20
push(30)310 20 303—st[2]=30
pop()210 20230弹的是栈顶,不是栈底的 10
push(40)310 20 403—st[2]=40,覆盖了旧值 30
pop()210 20240top 回到 2
pop()110120top 回到 1
push(50)210 502—st[1]=50,覆盖了旧值 20

结论:最后 top=2,栈中从栈底到栈顶为 10、50,栈顶元素是 50,共 2 个元素。

关键提醒:数组中 st[2] 此刻存的仍是 40,但它位于栈顶之上,逻辑上已经不属于栈,读写它都是错误行为。

对比一下:若换成队列做同样操作,出队要移动 head、入队要移动 tail,两个指针各走各的;栈只有一个 top,进出都在同一处,这就是栈不需要"循环"的原因。

💡 解析思路:画一列格子,只盯栈顶指针 top:push 就 top++ 再放元素,pop 就先取元素再 top--。每一步都问自己「现在 top 指向谁」。
⚠️ 易错:pop 空栈(下溢)和 push 满栈(上溢)都是考点;另外注意 top 的两种约定(指向栈顶元素 vs 指向栈顶下一个空位),初始化分别是 -1 和 0。
题2出栈序列合法性判断

题目:元素按 1、2、3、4、5 的顺序依次入栈(允许在任意时刻出栈)。判断下列四个序列中,哪些是合法的出栈序列:

① 3 2 1 5 4     ② 4 3 5 1 2     ③ 2 1 4 3 5     ④ 5 4 3 2 1

判定法则:对每个待出栈的数 x —— 若 x 尚未入栈,就按 1,2,3… 的顺序一路入栈直到 x 也进去;此时 x 必须是栈顶,否则该序列非法。
查看参考答案

① 3 2 1 5 4 —— 合法 ✅

入 1 2 3 → 弹 3(栈内 1 2);弹 2(栈内 1);弹 1(栈空);入 4 5 → 弹 5;弹 4。全部成功。

② 4 3 5 1 2 —— 非法 ❌

入 1 2 3 4 → 弹 4(栈内 1 2 3);弹 3(栈内 1 2);入 5 → 弹 5(栈内 1 2);

接下来要出 1,可栈顶是 2,1 被压在 2 的下面,永远出不来 → 非法。

③ 2 1 4 3 5 —— 合法 ✅

入 1 2 → 弹 2、弹 1(栈空);入 3 4 → 弹 4、弹 3(栈空);入 5 → 弹 5。全部成功。

④ 5 4 3 2 1 —— 合法 ✅

五个元素全部入栈后再依次弹出,得到的就是完全逆序。这是栈最"极端"的合法输出。

答案:①③④ 合法,② 非法。

通用解法(见 C4):用一个栈按 1..n 模拟入栈,指针 p 指向"下一个该入栈的数";对每个待检验的出栈元素,先把栈补到它为止,再检查它是不是栈顶。

💡 解析思路:用「目标序列驱动法」:按入栈顺序逐个压栈,压栈过程中只要栈顶等于目标序列的下一个元素就立即出栈。全程顺利 → 合法;卡住 → 非法。
⚠️ 易错:入栈中途随时可以出栈,不必等全部压完——像 3,2,1 出栈只能产生「反序」这一种序列,但中间混合出栈能产生很多种合法序列。
题3综合编程:表达式括号匹配(P1739)

题目:输入一个以 @ 结束的表达式(只含 () 与 []),判断其中的括号是否正确匹配。匹配则输出 YES,否则输出 NO。

提示:三种失败情况要分别处理 —— ① 右括号时栈为空;② 右括号与栈顶类型不符;③ 全部读完后栈中仍有剩余。缺一不可。
查看参考答案
C++p1739.cpp — P1739 表达式括号匹配
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int main() {
    string s;
    cin >> s;                        // 表达式不含空格,最后一个字符是 @
    stack<char> st;
    bool ok = true;
    for (int i = 0; i < (int)s.size() && s[i] != '@'; i++) {
        char c = s[i];
        if (c == '(' || c == '[') st.push(c);                  // 左括号入栈
        else if (c == ')' || c == ']') {                       // 右括号:必须匹配栈顶
            if (st.empty() || (c == ')' && st.top() != '(')
                           || (c == ']' && st.top() != '[')) {
                ok = false; break;
            }
            st.pop();
        }
    }
    if (!st.empty()) ok = false;      // 全部读完,栈里还有剩 → 左括号多了
    cout << (ok ? "YES" : "NO") << endl;
    return 0;
}

样例

输入
(([()]))@
输出
YES

解析:( ( [ ( 依次入栈;接下来四个右括号 ) ] ) ) 分别与栈顶 ( [ ( ( 匹配并弹出;读完栈为空,输出 YES。

三种失败的典型例子:()) → 第二个 ) 时栈空;([)] → ] 遇到栈顶 ( ,类型不符;(() → 读完栈里还剩一个 ( 。

复杂度:每个字符最多入栈、出栈各一次,时间 O(n),空间 O(n)。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
栈的定义只能在一端(栈顶)插入与删除的线性表,后进先出 LIFO
栈顶与栈底栈顶唯一可操作,栈底固定不动
top 约定本讲采用"top 指向下一个可插入位置",元素个数 = top
判空 / 判满top == 0 / top == MAXN(另一套约定是 -1 与 MAXN-1)
入栈 / 出栈st[top++] = x / top--(出栈不必清除原数据)
读栈顶st[top - 1] | s.top(),操作前必须判空
上溢 / 下溢满栈还 push → 越界 RE;空栈还 pop → 未定义行为
与队列的区别栈不用取模、没有假溢出,MAXN 个格子能存满 MAXN 个
STL stackpush/pop/top/size/empty;没有 clear(),不能下标访问
三大应用括号匹配、表达式求值、递归与出栈序列
2拓展:vector 模拟栈与单调栈 CSP-J 不要求
说明

以下内容属于 CSP-S(提高级)范围

本讲主体内容严格限定在 CSP-J 入门级大纲内。下面两种写法仅作了解,知道"有这个东西、能解决什么问题"即可,初赛复赛均不作要求。

vector 模拟栈
  • push_back / pop_back
  • back() 取栈顶
  • 支持下标访问 v[i]
  • 支持 clear() 与遍历
  • 需要"回头看"时用它
单调栈
  • 栈内元素保持单调
  • 求"左/右侧第一个更大(小)"
  • 每个元素最多进出各一次
  • 均摊 O(n)
  • 典型题:P5788、P2947
系统栈与爆栈
  • 递归依赖系统栈
  • 层数过深 → Stack Overflow
  • 可用显式栈改非递归
  • 也可改写成递推 / 循环
  • 典型场景:深搜、快速排序
C++extend.cpp — vector 当栈 与 单调栈
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int main() {
    // 写法一:vector 也能当栈用,而且能随时查看、遍历栈里的元素
    vector<int> v;
    v.push_back(1); v.push_back(2);
    cout << v.back() << " " << v.size() << endl;   // 2 2
    v.pop_back();
    cout << v.back() << endl;                      // 1

// 写法二:单调栈 —— 求每个数右边第一个比它大的数(CSP-S 内容,仅作了解)
    int a[5] = {2, 1, 4, 3, 5}, n = 5, ans[5];
    stack<int> s;                                  // 栈里存下标,对应的值单调递减
    for (int i = 0; i < n; i++) ans[i] = -1;
    for (int i = 0; i < n; i++) {
        while (!s.empty() && a[s.top()] < a[i]) {  // 栈顶被当前数"打败"
            ans[s.top()] = a[i];
            s.pop();
        }
        s.push(i);
    }
    for (int i = 0; i < n; i++) cout << ans[i] << " ";
    cout << endl;
    return 0;
}
3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能说出栈的定义与 LIFO 特性,并区分栈顶、栈底。

2. 能熟练写出顺序栈的入栈、出栈、判空、判满、取栈顶五个式子。

3. 能解释为什么栈不需要"循环",以及它与队列假溢出的本质区别。

4. 能手推栈的操作过程,正确算出 top 的值与从栈底到栈顶的元素序列。

5. 能用栈解决括号匹配与后缀表达式求值,并理解中缀转后缀的规则。

6. 能判断一个出栈序列是否合法,并理解"递归的本质就是栈"。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

对空栈执行 pop 或读栈顶

❌ 错误
循环写成 while (true) { cout << s.top(); s.pop(); },栈空后 top 变负,读到垃圾值甚至崩溃。
✅ 正确
遍历一律写 while (!s.empty());数组模拟则写 while (top > 0)。取栈顶前先判空。
口诀:取之前先看空,pop 之前先判空!
易错2

以为 pop() 会返回被弹出的元素

❌ 错误
int x = s.pop(); — STL 的 pop() 返回 void,这行编译都过不了。
✅ 正确
分两步:int x = s.top(); s.pop(); —— 先取后弹。
口诀:pop 不返回值,先 top 再 pop!
易错3

两种 top 约定混用,取栈顶差一位

❌ 错误
top = 0 却写成 st[top] 取栈顶 —— 那其实是栈顶上面的空位;或 top = -1 却写成 st[top-1]。
✅ 正确
先确定约定再动手:top=0 配 st[top-1],top=-1 配 st[top]。整份代码只用一套。
口诀:约定只能选一套,先想清楚再写代码!
易错4

括号匹配漏掉"读完栈非空"的判断

❌ 错误
只判断"右括号能否匹配",忘了最后检查 s.empty() —— 输入 (() 会被误判为合法。
✅ 正确
收尾补一句 if (!s.empty()) ok = false;,把"左括号多出来"的情况一网打尽。
口诀:配完还要看余量,栈空才算真匹配!
易错5

减法除法把两个操作数的顺序弄反

❌ 错误
后缀表达式求值时先弹的是右操作数,却写成 b - a / b / a,结果全错。
✅ 正确
int b = s.top(); s.pop(); int a = s.top(); s.pop(); 再算 a - b、a / b。
口诀:先弹右边后弹左,减除顺序别搞反!
速查栈速查表
知识点关键结论
栈的特性后进先出 LIFO,只在栈顶进出
判空top == 0 | s.empty()
判满top == MAXN(另一套约定为 top == MAXN-1)
入栈st[top++] = x | s.push(x)
出栈top-- | s.pop()(无返回值)
取栈顶st[top - 1] | s.top()
元素个数top | s.size()
最大容量MAXN(不浪费格子,也没有假溢出)
遍历方式while (!s.empty()) { s.top(); s.pop(); }
括号匹配左括号入栈,右括号匹配栈顶,读完栈必须为空
STL 头文件#include <stack>
时间复杂度入栈 / 出栈 / 取栈顶 均为 O(1)
📌知识点笔记卡

📥 入栈 push

压到栈顶

st[top++]=x

O(1)

📤 出栈 pop

弹出栈顶

top-- 即可

O(1)

🎯 栈顶元素

st[top-1]

STL:s.top()

先判空!

⚠️ 上溢下溢

满栈 push → 越界

空栈 pop → 出错

先判空判满

🆚 与队列比

栈不用取模

能存满 MAXN 个

没有假溢出

🏆 三大应用

括号匹配

表达式求值

递归与出栈序列

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

栈的特点是()

A. 先进先出   B. 后进先出   C. 可以在任意位置插入和删除   D. 可以随机访问任意元素

查看参考答案

答案:B

栈是后进先出(LIFO)的线性表。A 是队列的特点;C、D 描述的是普通数组,栈只能在栈顶操作,也不支持随机访问。

考点:栈是只允许在同一端(栈顶)插入和删除的线性表,因此后进先出(LIFO)。

易错:把栈的“后进先出”和队列的“先进先出”记反。

A2选择题

用数组 st[100] 实现栈,top 指向下一个可插入位置。依次入栈 3 个元素后出栈 1 个,此时 top 的值与栈顶元素分别是()

A. top=2,栈顶是第 2 个入栈的元素   B. top=2,栈顶是第 3 个入栈的元素
C. top=3,栈顶是第 3 个入栈的元素   D. top=1,栈顶是第 1 个入栈的元素

查看参考答案

答案:A

入栈 3 次后 top=3,栈内为 [第1个, 第2个, 第3个],栈顶是第 3 个;出栈 1 次后 top=2,第 3 个被弹出,新的栈顶是第 2 个入栈的元素。

注意"元素个数 = top"这条约定:top 从 3 变 2,正好反映元素从 3 个变 2 个。

考点:top 表示元素个数时:入栈 3 次后 top=3、栈里有 3 个元素;弹出 1 个后 top=2、元素剩 2 个,栈顶正是第 2 个入栈的元素。

A3选择题

下列关于栈的叙述,错误的是()

A. 栈可以用数组实现   B. 栈的插入和删除都在栈顶进行
C. 栈可能出现"假溢出",必须改成循环栈   D. 栈的入栈和出栈时间复杂度都是 O(1)

查看参考答案

答案:C

假溢出是队列特有的问题。栈的入栈出栈都在同一端,出栈空出的位置会被下一次入栈立刻覆盖复用,因此 st[MAXN] 可以存满 MAXN 个元素,不需要也不存在"循环栈"。

A、B、D 都是对栈的正确描述:数组与链表都能实现栈;操作只在栈顶;两种操作都是 O(1)。

考点:假溢出是顺序队列特有的毛病——出队留下的空位因队尾不能回退而无法复用。栈的进出都在同一端,空出的位置下一次入栈立刻复用,所以能存满 MAXN 个元素。

易错:把“假溢出”当成栈的缺陷,误以为数组栈也需要做成循环的。

A4编程题

用 STL stack 读入 n 个整数并依次入栈,输出栈顶元素与元素个数,然后全部出栈并输出。

查看参考答案
C++a4.cpp — STL stack 的基本操作
#include <iostream>
#include <stack>
using namespace std;
int main() {
    int n, x;
    cin >> n;
    stack<int> s;
    for (int i = 0; i < n; i++) { cin >> x; s.push(x); }
    cout << "top=" << s.top() << " size=" << s.size() << endl;
    while (!s.empty()) {          // 出栈顺序与入栈顺序完全相反
        cout << s.top() << " ";
        s.pop();
    }
    return 0;
}
输入
5
10 20 30 40 50
输出
top=50 size=5
50 40 30 20 10

出栈顺序与入栈顺序完全相反,这就是 LIFO 的直接体现。

思路:用 STL stack 依次 push 入栈;top() 只读栈顶不弹出、size() 给出元素个数;清空时 while(!s.empty()){ 输出 s.top(); s.pop(); }。

易错:没判空就在空栈上调用 top(),或把 top() 与 pop() 的顺序写反,先 pop 再取 top 就会漏掉栈顶元素。

🟡 B组 · 提高(4题)

B1分析题

元素按 1~6 的顺序入栈(允许随时出栈)。序列 3 2 4 6 5 1 是否合法?若合法,写出完整的操作序列(用 push/pop 表示)。

查看参考答案

结论:合法 ✅

操作序列:push1 push2 push3 → pop(3) → pop(2) → push4 → pop(4) → push5 push6 → pop(6) → pop(5) → pop(1)

逐步验证:弹 3 后栈内 [1,2],栈顶是 2,可弹;弹 2 后栈内 [1],栈顶是 1,但下一个要弹的是 4,于是 push4,弹 4;再 push5、push6,弹 6,弹 5;此时栈顶终于轮到 1,弹出成功。

关键观察:1 一直"垫在栈底"等了 5 步才出来 —— 栈允许元素长时间滞留,这正是它区别于队列的地方。

思路:按目标序列从前往后核对:需要的数若还没入栈就继续 push,能出的只能是栈顶;每一步对得上就推进,一旦栈顶不是当前需要的数即判为非法。

易错:只数元素个数对得上就判合法,忽略了被压在下面的元素不能抢先出栈。

B2编程题

不用 STL,改用数组模拟栈实现括号匹配(只含 () 与 []),匹配输出 Yes,否则输出 No。

查看参考答案
C++b2.cpp — 数组模拟栈实现括号匹配
#include <iostream>
#include <string>
using namespace std;
const int MAXN = 1005;
char st[MAXN];
int top = 0;                      // 数组模拟栈,逻辑与 STL 版本完全一致
bool match(char a, char b) {
    return (a == '(' && b == ')') || (a == '[' && b == ']');
}
int main() {
    string str;
    cin >> str;
    bool ok = true;
    for (int i = 0; i < (int)str.size(); i++) {
        char c = str[i];
        if (c == '(' || c == '[') {
            if (top == MAXN) { ok = false; break; }
            st[top++] = c;
        } else {
            if (top == 0 || !match(st[top - 1], c)) { ok = false; break; }
            top--;                // 出栈只需 top--,不碰原数据
        }
    }
    if (top != 0) ok = false;
    cout << (ok ? "Yes" : "No") << endl;
    return 0;
}
输入
(([()]))
运行结果
Yes

与 STL 版本的差别:只有三处 —— 判空写成 top == 0,取栈顶写成 st[top-1],出栈写成 top--。

为什么要练数组版:初赛常有"手推栈内容"的题,必须清楚每一步 top 指向哪里;另外数组版可以自己加 cout 调试,比 STL 直观。

思路:遇到左括号就入栈;遇到右括号时,若栈空或栈顶不是配对的左括号就判不匹配,否则弹出。整个串扫完后栈必须为空才算匹配成功。

易错:漏掉两种边界:右括号来时栈已经为空(多余的右括号),以及扫完后栈里还有左括号(括号没闭合)。

B3分析题

另一套约定是 top = -1(top 直接指向栈顶元素)。请写出这套约定下完整的入栈、出栈、判空、判满、取栈顶、求元素个数,并说明它与本讲约定的对应关系。

查看参考答案

对应关系:两套约定本质上是同一个 top 变量平移了 1。若记本讲的 top 为 top₁,另一套为 top₂,则恒有 top₂ = top₁ - 1。

于是:本讲的 st[top₁-1] 等价于 st[top₂];本讲的 top₁++ 后写入,等价于先 ++top₂ 再写入。

完整代码(top = -1 版本)

C++b3.cpp — top = -1 的另一套写法
#include <iostream>
using namespace std;
const int MAXN = 1005;
int st[MAXN];
int top = -1;                              // 约定二:top 直接指向栈顶元素,空栈为 -1

bool empty() { return top == -1; }         // 判空
bool full()  { return top == MAXN - 1; }   // 判满
int  peek()  { return st[top]; }           // 取栈顶(不用 -1)
int  size()  { return top + 1; }           // 元素个数

void push(int x) {
    if (full()) { cout << "Stack Full!\n"; return; }
    st[++top] = x;                         // 先加后用
}
void pop() {
    if (empty()) { cout << "Stack Empty!\n"; return; }
    top--;
}
int main() {
    push(10); push(20); push(30);
    cout << peek() << " " << size() << endl;   // 30 3
    pop();
    cout << peek() << " " << size() << endl;   // 20 2
    while (!empty()) { cout << peek() << " "; pop(); }
    return 0;
}
输出
30 3
20 2
20 10

怎么选:本讲推荐 top = 0,因为"元素个数 = top"最不容易算错,且与 STL 的 size() 含义一致。考试时若题目给的约定不同,只需在判空、判满、取栈顶三处同步调整即可。

思路:top = -1 时 top 直接指向栈顶元素:入栈写作 st[++top] = x,出栈取 st[top--],判空是 top == -1,判满是 top == MAXN - 1,元素个数是 top + 1;它和本讲的约定只差一次平移。

易错:把两套约定的判空条件混用(这套写成 top == 0),或入栈时写成 st[top++] = x 导致元素整体错位。

B4分析题

后缀表达式 5 1 2 + 4 * + 3 - 的值是多少?请写出用栈求值时,每一次运算符运算前后栈中(栈底→栈顶)的内容。

查看参考答案
C++完整参考代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;

int main() {
    string tok[] = {"5", "1", "2", "+", "4", "*", "+", "3", "-"};
    stack<int> st;

    for (string t : tok) {
        if (t == "+" || t == "-" || t == "*" || t == "/") {
            int r = st.top(); st.pop();   // 先弹出的是右操作数
            int l = st.top(); st.pop();   // 后弹出的是左操作数
            int v = 0;
            if (t == "+") v = l + r;
            if (t == "-") v = l - r;      // 注意是「左 - 右」
            if (t == "*") v = l * r;
            if (t == "/") v = l / r;
            st.push(v);
            cout << "读到 " << t << ":弹 " << r << "(右) " << l
                 << "(左) → " << l << t << r << " = " << v
                 << ",栈里剩下 " << st.size() << " 个数" << endl;
        } else {
            st.push(stoi(t));
            cout << "读到 " << t << ":入栈,栈里共 " << st.size() << " 个数" << endl;
        }
    }
    cout << "结果 = " << st.top() << endl;   // 结果 = 14
    return 0;
}

答案:后缀表达式 5 1 2 + 4 * + 3 - 的值是 14,对应的中缀式是 5 + ((1 + 2) × 4) - 3。

思路:从左往右扫描,遇到数字就入栈;遇到运算符就弹出两个数,先弹出的当右操作数、后弹出的当左操作数,算完把结果压回栈。全过程栈(栈底→栈顶)变化:
5 → [5];1 → [5,1];2 → [5,1,2];+ 得 1+2=3 → [5,3];4 → [5,3,4];* 得 3×4=12 → [5,12];+ 得 5+12=17 → [17];3 → [17,3];- 得 17-3=14 → [14]。

考点:栈的应用——后缀(逆波兰)表达式求值。

易错:减法和除法必须是「左 - 右」「左 ÷ 右」。如果弹出顺序弄反写成 3 - 17,就会得到 -14,整个结果全错。

🔴 C组 · 挑战(4题)

C1初赛真题

元素按 1、2、3、4 的顺序依次入栈(允许随时出栈),下列不可能得到的出栈序列是()

A. 1 2 3 4   B. 4 3 2 1   C. 2 3 4 1   D. 3 1 4 2

查看参考答案

答案:D

A:入 1 弹 1、入 2 弹 2 …… 可以。

B:全部入栈后依次弹出,得到 4 3 2 1,可以。

C:入 1 2 弹 2,入 3 弹 3,入 4 弹 4,最后弹 1,可以。

D:要第一个出 3,必须先入 1 2 3,弹 3 后栈顶是 2;下一个要出1,但 1 被 2 压在下面 —— 不可能。

通用判定:对于出栈序列中的数 x,它后面所有比 x 小的数,必须以递减顺序出现。D 中 3 后面的 1、2 是递增的,违反该规律。

考点:用“栈中元素必须自上而下递减”来判定:x 出栈后,其后所有比 x 小的数只能按从大到小出现。D 中 3 之后是 1、2,顺序递增,故不可能。

易错:把“允许随时入栈随时出栈”误解成“必须全部入栈后再统一出栈”。

C2初赛真题

若元素按 1、2、3、4、5 的顺序依次入栈(允许随时出栈),则可能的出栈序列总数为()

A. 24   B. 32   C. 42   D. 120

查看参考答案

答案:C. 42

n 个元素合法出栈序列的个数是第 n 个卡特兰数:C(n) = C(2n, n) / (n+1)。

n=5 时:C(10, 5) / 6 = 252 / 6 = 42。

前几项要记住:C(1)=1,C(2)=2,C(3)=5,C(4)=14,C(5)=42,C(6)=132。

为什么不是 5! = 120:120 是所有排列的总数,其中大部分(如 3 1 2)违反栈的后进先出约束,是不可能出现的。这正是 P1044「栈」考查的内容。

考点:n 个元素合法出栈序列的个数是第 n 个卡特兰数 C(2n,n)/(n+1),n = 5 时 252 / 6 = 42。

易错:误答 5! = 120,忘了后进先出会排除掉绝大多数排列。

C3综合编程

用双栈(操作数栈 + 运算符栈)直接求中缀表达式的值,支持 + - * / 与括号,输入保证合法且不含空格。

查看参考答案
C++c3.cpp — 双栈实现中缀表达式求值
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int pri(char c) {                                 // 乘除 2,加减 1,左括号 0
    if (c == '+' || c == '-') return 1;
    if (c == '*' || c == '/') return 2;
    return 0;
}
void calc(stack<int>& num, stack<char>& op) {     // 弹一个运算符,算一步
    int b = num.top(); num.pop();                 // 右操作数
    int a = num.top(); num.pop();                 // 左操作数
    char c = op.top(); op.pop();
    if (c == '+') num.push(a + b);
    else if (c == '-') num.push(a - b);
    else if (c == '*') num.push(a * b);
    else num.push(a / b);
}
int main() {
    string s;
    cin >> s;
    stack<int> num;                               // 操作数栈
    stack<char> op;                               // 运算符栈
    for (int i = 0; i < (int)s.size(); i++) {
        char c = s[i];
        if (c >= '0' && c <= '9') {               // 读一个完整数字
            int x = 0;
            while (i < (int)s.size() && s[i] >= '0' && s[i] <= '9')
                x = x * 10 + (s[i++] - '0');
            i--;
            num.push(x);
        } else if (c == '(') op.push(c);
        else if (c == ')') {                      // 右括号:算到左括号为止
            while (!op.empty() && op.top() != '(') calc(num, op);
            if (!op.empty()) op.pop();            // 丢掉左括号
        } else {                                  // 运算符:先把能算的算掉
            while (!op.empty() && op.top() != '(' && pri(op.top()) >= pri(c))
                calc(num, op);
            op.push(c);
        }
    }
    while (!op.empty()) calc(num, op);            // 收尾:把剩下的全部算完
    cout << num.top() << endl;
    return 0;
}
输入
1+2*(3+4)-6/3
输出
13

推导:1 + 2*7 - 2 = 1 + 14 - 2 = 13。

核心规则:遇到运算符 c 时,只要栈顶运算符优先级 ≥ c,就先算掉;遇到 ) 就一直算到 ( 为止;读完把栈里剩下的全部算完。

与"中缀转后缀"的关系:本法是"边转边算",转后缀是"先记下来再统一算",两者用到的优先级规则完全一致。

思路:数字直接进操作数栈;运算符入栈前把栈顶优先级 ≥ 自己的运算符全部弹出计算;遇到右括号就一路弹到左括号为止;扫完后把剩余的运算符弹空,栈中最后剩下的数就是答案。

易错:遇到右括号时把左括号也当运算符弹出计算,或忘记最后清空运算符栈导致结果没算完。

C4综合编程

出栈序列验证(P4387):已知入栈序列为 1~n,给定一个长度为 n 的排列,判断它是否可能是某个合法操作过程下的出栈序列。

查看参考答案
C++c4.cpp — 出栈序列合法性判断(P4387)
#include <iostream>
#include <stack>
using namespace std;
const int MAXN = 100005;
int a[MAXN];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];     // 待检验的出栈序列
    stack<int> s;
    int p = 1;                                    // 下一个该入栈的数是 p
    for (int i = 1; i <= n; i++) {
        while (s.empty() || s.top() != a[i]) {    // 栈顶不是目标,就继续入栈
            if (p > n) { cout << "No" << endl; return 0; }   // 数都用完了还配不上
            s.push(p++);
        }
        s.pop();                                  // 正好是栈顶 → 弹出
    }
    cout << "Yes" << endl;
    return 0;
}
输入
5
4 3 5 1 2
输出
No

模拟过程:检验 4 → 依次入 1 2 3 4,弹出 4;检验 3 → 栈顶正好是 3,弹出;检验 5 → 入 5,弹出;检验 1 → 栈顶是 2 且所有数都已入过栈,1 永远出不来 → 输出 No。

复杂度:每个元素最多入栈一次、出栈一次,时间 O(n),空间 O(n)。

易错提醒:p > n 时必须立刻停止,否则后面的 s.push(p++) 会无限入栈,导致 TLE / MLE。

思路:模拟入栈过程:设当前待出栈元素为 a[j],若栈顶不等于它,就把 1、2… 依次压入直到栈顶等于 a[j],再弹出并让 j 前移;一旦 a[j] 比已压入的最大值还小又不在栈顶,就说明序列非法。每个元素最多进出一次,总复杂度 O(n)。

🏆洛谷原题精选(20道)

以下题目均以栈为核心考点,按难度从易到难排列。建议在洛谷官网提交代码评测。

1B3614【模板】栈普及-
2P1427小鱼的数字游戏入门
3P1449后缀表达式入门
4P1739表达式括号匹配普及-
5P1165日志分析普及-
6P1241括号序列普及-
7P4387【深基15.习9】验证栈序列普及-
8P1044[NOIP2003] 栈普及-
9P1754球迷购票问题普及-
10P1981[NOIP2013] 表达式求值普及-
11P1175表达式的转换普及-
12P2947[USACO09MAR] Look Up S普及-
13P1901发射站提高-
14P2866[USACO06NOV] Bad Hair Day S提高-
15P2422良好的感觉提高-
16P7073[CSP-J2020] 表达式普及+
17P5788【模板】单调栈普及+
18P2659美丽的序列提高-
19P4147玉蟾宫提高-
20P1950长方形提高
🔑 刷题建议

① 先做入门题(B3614、P1427、P1449),把栈的基本操作与后缀表达式求值练熟

② 再做普及- 题(P1739 括号匹配、P4387 验证栈序列、P1165 日志分析),掌握"栈 + 模拟"的建模方法

③ 表达式三连(P1981 求值、P1175 转换、P7073 表达式树)建议按顺序刷,是初赛复赛的共同高频考点

④ 第 17~20 题属于单调栈(提高级)内容,CSP-J 阶段可以先跳过