栈
栈
栈的基本概念
堆栈(Stack)是一种非常经典的数据结构,它遵循一个简单但重要的原则: 后进先出(LIFO, Last In First Out)。说白了,就是谁最后放进去,谁最先被拿出来。
在我们的日常生活中,其实到处都能看到这样的例子,比如说:
- 厨房的碗:通常我们把洗好的碗一个个叠在一起,最上面的碗是最后放上去的,也是最先拿出来用的。而最底下的那个碗,如果不把上面的碗拿开,是永远也动不到的。(如下图所示👇)
- 书堆:一堆摞起来的书,要拿书时通常是从最上面一本拿起。新的需要放入的书也需要放到最上面去。
- 死胡同里的车:最后进去的车必须最先倒出来,否则前面的车根本没办法动弹。
这些场景都体现了同样的规律:先加入的最后出来,后加入的先出来。 这,就是堆栈的核心思想。
因此,我们说堆栈这一数据结构有以下特点:
- 后进先出(LIFO):最后被压入栈中的元素,最先被弹出。
- 有栈顶和栈底之分:栈顶是操作的一端,栈底固定不动。
- 只能在一端操作:所有元素的插入(push)和删除(pop)都发生在栈顶。
- 操作简单,效率高:入栈和出栈的时间复杂度为 O(1)。
栈的基本操作
栈支持以下基本操作:
- 入栈(压栈):将一个元素添加到堆栈的顶部。
- 出栈:将堆栈顶部的元素从堆栈中移除。
注意:
- 为什么我们说是移除,而不是取出呢?因为这个操作是对栈顶元素直接丢弃,并不会获取到栈顶元素的值。就像是你把碗堆顶的碗取出来用掉了,下次使用就要考虑下一个碗了。此时,你并不关心用掉的那个碗上面画着什么花纹,只关心你还能用的那些碗。
- 栈中有元素存在时,才能出栈!(我们总不能无中生有不是qwq)
- 查看栈顶:获取栈顶元素的值。
注意:
- 只是获取哦,并不会移除。就像是你记录了一下碗堆顶部的碗上面画着什么花纹,并没有拿出来使用它。
- 栈中有元素存在时,才能获取栈顶元素!(至于怎样保证呢,待会综合示例再说)
- 查看堆栈大小:获取堆栈中元素的数量。
- 栈是否为空:查看堆栈中是否包含元素。
返回值:
true
:栈为空,栈中没有任何元素存在false
:栈不为空,栈中存在至少一个元素
栈的定义与基本用法
首先需要注意的是:
- 与数组、vector等数据结构不同,我们的堆栈在定义时,不会定义其大小和初始内容。堆栈的内容往往是通过一个一个
压栈
操作进入的。 - 我们可以定义存放整数等简单数据类型的元素的堆栈,当然也可以定义存放结构体,甚至
- 是vector等数据类型的元素的堆栈。但,whatever,初期先写好存放
int
、double
这样的元素的堆栈。一步一步来。
不同编程语言中,栈的实现方式和初始化语法略有不同,下面分别介绍几种常见语言的用法。
C++
在 C++ 中,可以使用标准模板库(STL)中的 stack
类来实现栈,定义在 <stack>
头文件中。
#include <stack>
声明和初始化栈的基本语法如下:
stack<元素类型> 栈名;
例如,如果我要声明一个存放整数的栈,栈名叫 s
,可以这样写:
stack<int> s;
对于栈的五种基本操作,他们的语法如下表所示:
入栈/压栈 | 堆栈的变量名.push(需要入栈的元素) | s.push(3) |
|
出栈 | 堆栈的变量名.pop() | s.pop() |
|
查看栈顶 | 堆栈的变量名.top() | s.top() |
|
查看堆栈大小 | 堆栈的变量名.size() | s.size() |
|
栈是否为空 | 堆栈的变量名.empty() | s.empty() |
Java
在 Java 中,可以使用 java.util.Stack
类来创建栈。它是一个泛型类,可以存放任意类型的对象。
import java.util.Stack;
声明和初始化栈的基本语法如下:
Stack<元素类型> 栈名 = new Stack<>();
例如,如果我要声明一个存放整数的栈,栈名叫 s
,可以这样写:
Stack<Integer> s = new Stack<>();
注意:
- Java 的
Stack
是线程安全的,但在高并发场景下通常推荐使用Deque
(如ArrayDeque
)来替代。
对于栈的五种基本操作,他们的语法如下表所示:
入栈/压栈 | 堆栈的变量名.push(需要入栈的元素) | s.push(3) |
|
出栈 | 堆栈的变量名.pop() | s.pop() |
|
查看栈顶 | 堆栈的变量名.peek() | s.peek() |
|
查看堆栈大小 | 堆栈的变量名.size() | s.size() |
|
栈是否为空 | 堆栈的变量名.empty() | s.empty() |
Python
Python 中没有内建的专用“栈”类,但可以使用内置的 list
或 collections.deque
来模拟栈结构。deque
是双端队列,比 list
在栈操作上效率更高,更推荐使用。我们以使用 collections.deque
模拟为例进行说明:
from collections import deque
s = deque()
对于栈的五种基本操作,他们的语法如下表所示:
入栈/压栈 | 堆栈的变量名.apend(需要入栈的元素) | s.append(3) |
|
出栈 | 堆栈的变量名.pop() | s.pop() |
|
查看栈顶 | 堆栈的变量名[-1] | s[-1] |
|
查看堆栈大小 | len(堆栈的变量名) | len(s) |
|
栈是否为空 | not 堆栈的变量名 | not s |
栈的综合操作模拟
下面我们以如下操作序列对栈的变化过程进行模拟:
操作序列:
- 定义一个空栈
s
。 - 删除栈顶元素。
- 向堆栈中压入元素
5
。 - 向堆栈中压入元素
11
。 - 输出堆栈大小。
- 向堆栈中压入元素
9
。 - 将堆栈中所有元素弹出,并输出所有被弹出元素的和。
请思考几分钟,想想堆栈的变化过程。再看分析~
我们在分析堆栈问题时,画堆栈的变化情况图往往是一个比较合适的解决方法。那么我们一步一步看看。
定义空栈
s
此时我们有了堆栈,堆栈中没有任何元素,因此栈顶和栈底是黏在一起的。如下图 (1) 删除栈顶元素 堆栈中还什么都没有呢,那堆栈状态自然是没有变。但此时直接去出栈,是会触发运行时错误的。因此,我们在出栈操作时,往往要先确认栈中还有元素存在,然后再进行出栈操作。 压入元素5
5
进入堆栈,栈顶指向5
。栈底位置不变。如下图(2) 压入元素11
11
进入堆栈,栈顶指向11
。栈底位置不变。如下图(3) 获取堆栈大小 此时堆栈中元素数量为2
。因此输出结果为2
。堆栈状态不变。 压入元素9
9
进入堆栈,栈顶指向9
。栈底位置不变。如下图(4) 将堆栈中所有元素弹出,并输出所有被弹出元素的和 可以采用循环的方式,当栈尚不为空时,依次做如下几件事: 获取栈顶元素的值,并累计到累计变量上去。 出栈操作。 循环结束即可通过累计变量的值获取到所有元素之和,最终结果为25
对于上述过程,我们给出不同语言的代码以作综合示例:
#include <iostream>
#include <stack>
using namespace std;
int main() {
stack<int> s; // 1. 定义一个空栈 s
// 2. 删除栈顶元素(前提检查是否为空)
if (!s.empty()) {
s.pop();
} else {
cout << "栈为空,无法执行出栈操作!" << endl;
}
// 3. 压入元素 5
s.push(5);
// 4. 压入元素 11
s.push(11);
// 5. 输出堆栈大小
cout << "当前栈大小为:" << s.size() << endl;
// 6. 压入元素 9
s.push(9);
// 7. 弹出所有元素并求和
int sum = 0;
while (!s.empty()) {
sum += s.top();
s.pop();
}
cout << "所有弹出元素的和为:" << sum << endl;
return 0;
}
import java.util.Stack;
public class StackSimulation {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>(); // 1. 定义一个空栈 s
// 2. 删除栈顶元素(防止异常)
if (!stack.isEmpty()) {
stack.pop();
} else {
System.out.println("栈为空,无法执行出栈操作!");
}
// 3. 压入元素 5
stack.push(5);
// 4. 压入元素 11
stack.push(11);
// 5. 输出堆栈大小
System.out.println("当前栈大小为:" + stack.size());
// 6. 压入元素 9
stack.push(9);
// 7. 弹出所有元素并求和
int sum = 0;
while (!stack.isEmpty()) {
sum += stack.pop();
}
System.out.println("所有弹出元素的和为:" + sum);
}
}
from collections import deque
stack = deque() # 1. 定义一个空栈 s
# 2. 删除栈顶元素(安全判断)
if stack:
stack.pop()
else:
print("栈为空,无法执行出栈操作!")
# 3. 压入元素 5
stack.append(5)
# 4. 压入元素 11
stack.append(11)
# 5. 输出堆栈大小
print("当前栈大小为:", len(stack))
# 6. 压入元素 9
stack.append(9)
# 7. 弹出所有元素并求和
total = 0
while stack:
total += stack.pop()
print("所有弹出元素的和为:", total)
例题
例题 1:模板:栈的操作
题意
实现一个栈,支持以下操作:
push(x)
:向栈中加入一个数 x。pop()
:将栈顶弹出。如果此时栈为空则不进行弹出操作,输出Empty
。query()
:输出栈顶元素,如果此时栈为空则输出Anguei!
。size()
:输出此时栈内元素个数。
思路分析
我们发现需要使用一个普通的栈来完成模拟操作:
- 对于
push x
操作,我们将直接将压入栈中;
- 对于
pop
操作,我们需要首先判断栈是否为空。如果当前栈是空栈,那么我们需要输出empty
;否则,执行出栈操作。 - 对于
query
操作,我们需要首先判断栈是否为空。如果当前栈是空栈,那么我们需要输出Anguei!
;否则,我们获取栈顶元素的值并输出。 - 对于
size
操作,我们直接获取栈的大小并输出即可。
本题涉及到多测。那么,我们并不希望一组测试数据的执行过程和结果对下一组测试数据的执行产生任何影响。因此,对于每一组测试数据,我们都需要单独初始化一个栈,并根据输入,对应顺序地执行上述对应操作。
另外,我们注意到:输入数据保证插入堆栈的整数 满足
。也就是说,
的取值超过了
long long
的范围,那么我们的堆栈需要存储的元素类型应该为:unsigned long long
。
代码实现
#include <bits/stdc++.h>
using namespace std;
// 具体解决每一组测试数据的函数
void solve() {
int n;
cin >> n; // 读入本组操作次数
stack<unsigned long long> s; // 定义一个存储无符号长整型数据的栈
string op; // 保存操作类型
unsigned long long x; // 被操作的元素(仅在push操作时使用)
// 循环执行n次操作
while(n--) {
cin >> op; // 读入操作指令
if(op == "push") {
// 如果是push操作,还需要读入一个元素x,并将其压入栈
cin >> x;
s.push(x);
}
else if(op == "pop") {
// 如果是pop操作,首先判断栈是否为空
if(!s.empty()) s.pop(); // 非空则弹出栈顶元素
else cout << "Empty" << endl; // 空栈时输出Empty
}
else if(op == "query") {
// 如果是query操作,判断栈是否为空
if(!s.empty()) cout << s.top() << endl; // 非空则输出栈顶元素
else cout << "Anguei!" << endl; // 空栈时输出Anguei!
}
else {
// 其他情况就是size操作,直接输出栈中元素个数
cout << s.size() << endl;
}
}
}
int main() {
// 快速输入输出设置,加速cin和cout
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t; // 读入测试数据组数
// 每组数据分别调用solve()函数进行处理
while(t--) {
solve();
}
return 0;
}
import java.util.Scanner;
import java.util.Stack;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt(); // 读入测试数据组数
while (T-- > 0) {
int n = sc.nextInt(); // 读入本组操作次数
Stack<Long> stack = new Stack<>(); // 使用Stack类创建一个栈,存储Long型数据
for (int i = 0; i < n; i++) {
String op = sc.next(); // 读入操作指令
if (op.equals("push")) {
long x = sc.nextLong(); // push操作需要额外读入一个数字
stack.push(x); // 将x压入栈中
}
else if (op.equals("pop")) {
// pop操作,先判断栈是否为空
if (!stack.isEmpty()) {
stack.pop(); // 非空则弹出栈顶元素
} else {
System.out.println("Empty"); // 空栈时输出Empty
}
}
else if (op.equals("query")) {
// query操作,查看栈顶元素
if (!stack.isEmpty()) {
System.out.println(stack.peek()); // 非空则输出栈顶元素
} else {
System.out.println("Anguei!"); // 空栈时输出Anguei!
}
}
else if (op.equals("size")) {
// size操作,输出栈内元素个数
System.out.println(stack.size());
}
}
}
sc.close(); // 关闭Scanner
}
}
import sys
# 使用sys.stdin.readline加速读入
input = sys.stdin.readline
T = int(input()) # 读入测试数据组数
output = [] # 存放所有输出结果,最后统一输出(加速)
for _ in range(T):
n = int(input()) # 读入本组操作次数
stack = [] # 初始化一个空栈(使用列表模拟栈)
for _ in range(n):
line = input().strip() # 读入操作指令,并去除多余空白
if line.startswith("push"):
# push操作,取出要压入的数字
_, x = line.split()
stack.append(x) # 将元素x压入栈中
elif line == "pop":
# pop操作,判断栈是否为空
if stack:
stack.pop() # 非空则弹出栈顶元素
else:
output.append("Empty") # 空栈时输出Empty
elif line == "query":
# query操作,查看栈顶元素
if stack:
output.append(stack[-1]) # 非空则输出栈顶元素
else:
output.append("Anguei!") # 空栈时输出Anguei!
elif line == "size":
# size操作,输出栈的元素数量
output.append(str(len(stack)))
# 最后统一输出所有结果,避免频繁I/O
print('\n'.join(output))
例题 2:模板:括号匹配
题意
给定一个只包含括号和其他普通字符的字符串,判断其中的括号,即字符[
]
(
)
是否按照规则正确配对。
思路分析
这道题是一个带杂字符版的括号匹配问题,我们可以使用栈来解决:
基本策略:
- 从左到右扫描字符串;
- 遇到左括号
(
或[
,就压入栈; - 遇到右括号
)
或]
:- 如果栈为空,说明没有可以配对的左括号,配对失败;
- 否则取出栈顶元素,看它是否和当前右括号匹配;
(
应该对应)
;[
应该对应]
;- 如果匹配成功,就把栈顶元素弹出;
- 否则,配对失败;
- 遇到普通字符(比如
a、b、c
等)直接忽略,继续下一个字符。
从实现的角度来讲,我们当然可以直接用一个存储字符元素的堆栈来解决这个问题。这样做是可以正确完成要求的,但代码稍微繁琐。其实,我们可以把每种括号映射成简单的数字,用数字相加的方法,快速判断是否配对。 具体映射规则如下:
( |
1 |
[ |
2 |
] |
3 |
) |
4 |
其它任意字符 | 0 |
在这种映射条件下,如果两个括号如果能配对,需要满足什么条件呢?
- 栈顶是左括号,当前是右括号;
- 并且它们的数字加起来等于5!
1 ( + 4 ) = 5
→ 成功匹配;2 [ + 3 ] = 5
→ 成功匹配;
- 如果不是加起来5,就表示不匹配! 通过这种处理方法,我们可以做到统一处理不同种类的括号,简化代码实现。
代码实现
#include <bits/stdc++.h>
using namespace std;
// 辅助函数:将括号字符转换为数字编码,方便后续比较
int get_id(char c){
if(c == '(') return 1; // 左小括号映射为1
if(c == '[') return 2; // 左中括号映射为2
if(c == ']') return 3; // 右中括号映射为3
if(c == ')') return 4; // 右小括号映射为4
return 0; // 其他普通字符映射为0
}
int main(){
string str;
cin >> str; // 读入整个字符串
stack<int> s; // 定义一个栈,存括号类型(数字)
bool flag = true; // 标记配对是否正确
// 遍历字符串
for(int i = 0; i < str.size(); i++){
int id = get_id(str[i]); // 将字符转成数字
if(id > 2){
// 当前字符是右括号
if(s.empty() || s.top() + id != 5) {
// 栈空 或 无法配对,失败
flag = false;
break;
}
else {
// 成功配对,弹出栈顶
s.pop();
}
}
else if(id > 0) {
// 当前字符是左括号
s.push(id); // 压入栈中
}
// 其他普通字符直接跳过
}
// 最终判断:配对过程中无误,且栈空
if(flag && s.empty()) cout << "true";
else cout << "false";
return 0;
}
import java.util.Scanner;
import java.util.Stack;
public class Main {
// 辅助函数:将括号字符映射为数字编码
public static int getId(char c) {
if (c == '(') return 1;
if (c == '[') return 2;
if (c == ']') return 3;
if (c == ')') return 4;
return 0; // 普通字符
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.next(); // 读入字符串
Stack<Integer> s = new Stack<>();
boolean flag = true; // 标记配对是否成功
for (int i = 0; i < str.length(); i++) {
int id = getId(str.charAt(i));
if (id > 2) {
// 当前是右括号
if (s.isEmpty() || s.peek() + id != 5) {
flag = false;
break;
} else {
s.pop(); // 成功配对,弹出栈顶元素
}
} else if (id > 0) {
// 当前是左括号
s.push(id);
}
// 普通字符直接跳过
}
// 判断最终结果
if (flag && s.isEmpty()) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
# 辅助函数:将括号字符映射为数字编码
def get_id(c):
if c == '(':
return 1
if c == '[':
return 2
if c == ']':
return 3
if c == ')':
return 4
return 0
def main():
str_ = input() # 读入字符串
s = [] # 使用列表模拟栈
flag = True # 标记括号是否配对成功
for c in str_:
id_ = get_id(c) # 转换成对应数字
if id_ > 2:
# 是右括号
if not s or s[-1] + id_ != 5:
flag = False
break
else:
s.pop() # 配对成功,弹出栈顶
elif id_ > 0:
# 是左括号
s.append(id_)
# 普通字符直接跳过
# 判断最终是否栈空且配对过程中没有出错
if flag and not s:
print("true")
else:
print("false")
if __name__ == "__main__":
main()
课后习题
汗牛充栋,学海无涯。<br/> 内含算法知识点讲解,以及牛客题库精选例题。<br/> 学习算法,从牛栋开始。