栈
栈的概念
栈是限定在一端(栈顶)进行插入删除操作的线性表(LIFO线性表)。

- 数据结构研究内容
数据集合:存储什么类型的数据,采用什么结构进行存储;
使用规则:添加、查询、删除、修改数据时,要遵循的规则;
操作实现:对数据的增、删、查、改以及其他特有操作的具体实现方法。
- 线性结构
队列:两端开口,先进先出类型,FIFO;
栈:一端开口,后进先出类型,LIFO;
存储方式:一般可采用数组或链表实现,中学竞赛通常使用数组实现。

栈(stack):是一种能有效帮助我们组织数据的数据结构,按照后进先出的规则管理数据。
栈的操作
push(x):在栈顶部添加元素x;
pop():从栈顶部取出元素;
isEmpty():检查栈是否为空;
isFull():检查栈是否为满。
top():返回栈顶元素的引用;
size(): 返回栈的元素个数;
规则:
数据中后加入的元素最先被取出,即
pop取出的元素是最后一次被push的元素。
栈的实现
数组实现
- 栈顶指针
始终存储栈最上方元素下标的变量(或者最上方元素地址的指针);
通常记作
tp;栈为空时,
tp为 0,第一个元素的下标为 1(或者栈为空时tp为 -1,第一个元素的下标为 0);
- 基本操作
用
stk[N]来定义栈,用int变量tp表示栈顶元素的下标(初值为 0);
clear():直接将 tp 设为 0,相当于将数组中原来存储的元素全部弹出栈。
void clear() {
tp = 0;
}
size():直接返回 tp 的值即可,考虑到size一定是非负的,标准的写法应为unsigned int类型,但是我们竞赛中我们通常用int型简写。
int size() {
return tp;
}
isEmpty():若tp == 0,说明栈为空。
bool isEmpty() {
return tp == 0;// 或者 return !tp;
}
push(x):入栈(压栈)操作严格来说,需要先判断栈是否已满,若tp == N-1,说明栈空间已经用完,压栈操作就失败了。但我们在竞赛编程的时候,是绝对不能允许这种情况发生的,N通常会大于题目取值范围的上限,从而保证栈空间不会满,所以我们也可以简单地只写成一句话stk[++tp] = x;。
bool push(int x) {
if (tp == N-1) return false;
stk[++tp] = x;
return true;
}
pop():同入栈操作类似,弹栈操作前需要先判断栈是否为空。
bool pop() {
if (tp == 0) return false;
--tp;
return true;
}
top():同上,先判断栈是否为空。由于top()需要返回栈顶元素值,不能再返回true或者false,所以,需要在调用top()前先执行isEmpty()函数判断。
int top() {
return stk[tp];
}
STL 实现
使用 C++ 的 STL 库中的 stack 容器,可以非常容易的使用栈。
应先添加头文件
#include <stack>,头文件下面加上using namespace std;,就可以使用了。
#include <cstdio>
#include <stack>
using namespace std;
int main() {
stack<int> stk;
for (int i = 1; i <= 5; ++i)
stk.push(i);// 将 i 逐个入栈
printf("%d\n", stk.top());//已知栈非空,获取栈顶元素
return 0;
}
push(x):将
x入栈,时间复杂度为 \(O(1)\);top():获得栈顶元素,时间复杂度为 \(O(1)\);
pop():弹出栈顶元素,时间复杂度为 \(O(1)\);
empty():检测栈内是否为空,时间复杂度为 \(O(1)\)。
size():返回 stack 内元素的个数,时间复杂度为 \(O(1)\)。
注意:STL 中没有实现栈的清空,如果需要清空,可以用 while 循环反复 pop。
...{
...
stack<int> stk;
if (stk.empty()) puts("Empty");
else puts("Not Empty");
stk.push(1);
if (stk.empty()) puts("Empty");
else puts("Not Empty");
//请问输出结果是:
return 0;
}
...{
stack<int> stk;
for (int i = 1; i <= 5; ++i) stk.push(i);
printf("%d\n", stk.size());
//请问输出结果是:
return 0;
}
while (!stk.empty())
stk.pop();
栈的基本应用
1. 进制转换——数组实现和 STL 实现
数组实现
#include <cstdio>
const int N = 107;
int n, stk[N], tp;
int main() {
scanf("%d", &n);
while (n) {
stk[++tp] = n & 1;// n%2
n >>= 1;
}
while (tp) printf("%d", stk[tp--]);
return 0;
}
STL 实现
#include <cstdio>
#include <stack>
//如果不输入 using namespace std; 则声明时需要使用 std::
std::stack<int> stk;
int main() {
int n;
scanf("%d", &n);
while (n) {
stk.push(n & 1);
n >>= 1;
}
while (stk.size()) {
printf("%d", stk.top());
stk.pop();
}
return 0;
}
递归中的系统栈
利用递归实现十进制到二进制数的转换
void trans(int n) {
if (n) {
trans(n >> 1);
printf("%d", n & 1);
}
}

2. (括号)匹配问题
方法一:使用栈模拟操作。
#include <cstdio>
const int N = 257;
char s[N], stk[27];
int tp;
int main() {
scanf("%s", s);
for (int i = 0; s[i]; ++i) {
// 必压入 '(',完全可以不用存储压入的内容,不需要声明数组了。
if (s[i] == '(') stk[++tp] = '(';
if (s[i] == ')') {
if (tp == 0) {
puts("NO");
return 0;
}
--tp;
}
}
if (tp) puts("NO");
else puts("YES");
return 0;
}
方法二:仔细检查代码,我们发现,每次压栈操作只会压入 '(',弹栈的时候,只会看栈是否为空,因此,我们只需要一个栈顶指针
tp即可,根本不需要实际压栈操作。
#include <cstdio>
const int N = 257;
char s[N];
int tp;
int main() {
scanf("%s", s);
for (int i = 0; s[i]; ++i) {
if (s[i] == '(') ++tp;
if (s[i] == ')') {
if (tp == 0) {
puts("NO");
return 0;
}
--tp;
}
}
if (tp) puts("NO");
else puts("YES");
return 0;
}
3. 括号匹配检验
抓住一个规律(我们后面正式称为定理lema):每个右括号的前一个括号,必须是与其同类型的左括号。
将每个左括号入栈,遇到右括号时,就检查栈顶的左括号即可。
若栈空,则匹配失败;
若栈顶括号不与其类型相同,则匹配失败;
若字符串遍历结束后,栈不空,则匹配失败;
若上述条件均不成立,匹配成功。
4. 车厢调度
Description
有一个火车站,铁路如图所示,每辆火车从 A 驶入,再从 B 方向驶出,同时它的车厢可以重新组合。假设从 A 方向驶来的火车有 \(n\) 节(\(n\le 1000\)),分别按照顺序编号为\(1,2,3,…,n\)。假定在进入车站前,每节车厢之间都不是连着的,并且它们可以自行移动到 B 处的铁轨上。另外假定车站 C 可以停放任意多节车厢。但是一旦进入车站 C,它就不能再回到 A 方向的铁轨上了,并且一旦当它进入 B 方向的铁轨,它就不能再回到车站 C。
负责车厢调度的工作人员需要知道能否使它以 \(a_1,a_2,…,a_n\) 的顺序从 B 方向驶出,请来判断能否得到指定的车厢顺序。

Format
Input
输入文件的第一行为一个整数 \(n\),其中 \(n\le 1000\),表示有 \(n\) 节车厢,第二行为 \(n\) 个数字,表示指定的车厢顺序。
Output
如果可以得到指定的车厢顺序,则输出一个字符串 YES,否则输出 NO(注意要大写,不包含引号)。
Samples
Input1
5
5 4 3 2 1
Output1
YES
思路1:模拟栈调度
构建一个栈,根据输出的数字进行入栈、出栈的模拟操作,我们令当前要出栈的数为 a[i]:
若
a[i]就是栈顶元素,直接出栈;否则,若
a[i]比栈顶元素大,则需要后续元素依次入栈,直至a[i]入栈结束,将a[i]出栈;否则,
a[i]小于栈顶元素,说明a[i]在栈里,无法弹出,输出NO,程序结束;所有数全部出栈完毕,输出
YES。
思路2:通过观察进行优化
通过观察,可以发现一个引理:对于出栈序列中任意三个连续的数 a,b,c,若 a>c>b,则直接输出NO,程序结束;否则,当所有数均出栈后,输出YES。
5. 计算
Description
小明在你的帮助下,破密了Ferrari设的密码门,正要往前走,突然又出现了一个密码门,门上有一个算式,其中只有 (,),0-9,+,-,*,/,^,求出的值就是密码。小明数学学得不好,还需你帮他的忙。(/ 用整数除法,^ 是幂运算,2^3 为 8)
Format
Input
输入共1行,为一个算式。
Output
输出共1行,就是密码。
Samples
Input1
1+(3+2)*(7^2+6*9)/(2)
Output1
258
Input2
3*(-21)
Output2
-63
Limitation
1s, 1024KiB for each test case.
100% 的数据满足:
算式长度 \(\le 60\),其中所有数据在 \(2^{31}-1\) 的范围内。
\(2\)$3$\(2\)\(=(2^3)^2\)
算法过程
-
需要建立两个栈——数字栈(stki)和运算符栈(stkc),用一对括号将字符串括起来;
-
从左到右依次读入字符(注意,中间计算的结果需要重新压入数字栈中)
-
若读入的是数字,则直接压入数字栈
-
若读入的是左括号,则直接压入运算符栈
-
若读入的是右括号,则将运算符栈中的运算符依次弹出,并从数字栈中弹出对应的数字进行计算(注意,先弹出的数字应该在运算符的右边),直到遇到左括号停止,并将左括号弹出
-
若读入的是运算符,则先将栈里优先级大于等于该运算符的运算先依次弹出计算,再将该运算压入运算符栈
-
思路:
使用两个栈,一个数字栈,一个运算符栈。
感谢童嘉辰同学提供如下课堂笔记:
| 数字 | 运算符 | 备注 |
|---|---|---|
| 1 | 1入栈 | |
| 1 | + |
+号入栈 |
| 1 | +,( |
(入栈 |
| 1,3 | +,( |
3入栈 |
| 1,3 | +,(,+ |
+入栈,因为没有)出现,所以不动(,也不用弹出之前的运算符,即使这里本来应该弹出前一个+ |
| 1,3,2 | +,(,+ |
2入栈 |
| 1,5 | + |
)入栈,()出现,将包含在其中的+弹出栈并用最后两个数字作相应运算,然后压回栈内,(也弹出,5即为3+2 |
| 1,5 | +,* |
*入栈,优先级更高,所以不用弹出+ |
| 1,5 | +,*,( |
(入栈 |
| 1,5,7 | +,*,( |
7入栈 |
| 1,5,7 | +,*,(,^ |
^入栈 |
| 1,5,7,2 | +,*,(,^ |
2入栈 |
| 1,5,49 | +,*,(,+ |
+优先级小于^,所以将其弹出运算,7^2为49,压回栈内,然后将+入栈 |
| 1,5,49,6 | +,*(,+ |
6入栈 |
| 1,5,49,6 | +,*,(,+,* |
*入栈 |
| 1,5,49,6,9 | +,*,(,+ * |
9入栈 |
| 1,5,103 | +,* |
()又出现了!,弹出(,运算6*9为54,压回;弹出+,运算49+54为103,压回,(出栈 |
| 1,515 | +,/ |
/优先级大于*,所以弹出运算为515,压回,/入栈 |
| 1,515 | +,/,( |
(入栈 |
| 1,515,2 | +,/,( |
2入栈 |
| 1,515,2 | +,/ |
)入栈,双双弹出 |
| 1,257 | + |
输入结束,开始依次弹出注意在/,-运算时要反过来运算(不是a/b而是b/a) |
| 258 | +弹出,运算结束 |
递归的概念
在数学和计算机科学中是指在函数的定义中使用函数自身的方法,在计算机科学中还额外指一种通过重复将问题分解为同类的子问题而解决问题的方法。
递归应用
void fun() { fun(); } //死循环,不断调用自己,永远都不结束
//在dev里运行,可以看到运行效果
#include <cstdio>
void tellAStory() {
printf("从前有座山,山上有座庙,庙里有个老和尚……\n");
tellAStory();
}
int main() {
tellAStory();
return 0;
}
- 问题1:如何控制输出次数?
- 问题2:如何在每句前输出一个行号?
我们可以看到,递归就像坐电梯,从最上层逐层下降,到最底层再逐层上升,回到最初的楼层。
递归的模板
通常说,递归函数一定要具备两个部分:
停止条件;
当前层要做的事情。
为什么要写递归?
结构清晰,可读性强。
练习分析问题的结构。当发现问题可以被分解成相同结构的小问题时,递归写多了就能敏锐发现这个特点,进而高效解决问题。
递归的缺点
在程序执行中,递归是利用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。而栈不是无限大的,当递归层数过多时,就会造成 栈溢出 的后果。
显然有时候递归处理是高效的,比如归并排序;有时候是低效的,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。

浙公网安备 33010602011771号