栈 (Stack)
栈 (Stack)
概念:后进先出 (LIFO - Last In First Out) 的数据结构
想象一下,你叠放一摞盘子。 你总是从最上面拿走盘子,最后放上去的盘子总是最先被拿走。 栈 (Stack) 这种数据结构就类似于叠盘子,它遵循 后进先出 (LIFO - Last In First Out) 的原则。 就像一个桶或者一个单端开口的管道,你只能从顶端放入 (压栈) 和取出 (弹栈) 元素。
- 后进先出 (LIFO): Last In First Out。 最后放入栈的元素,将是第一个被取出来的元素。 反之,最先放入栈的元素,将是最后被取出来的元素。
- 单端操作: 栈的操作只在栈顶 (Top) 进行。 你不能直接访问栈中间或底部的元素,必须先将上面的元素取出才能访问下面的。

栈的类型 (Types)
从概念上讲,栈主要关注的是 LIFO 的操作原则。 在具体实现上,栈可以使用不同的底层数据结构来实现,例如:
- 顺序栈 (Array-based Stack): 使用数组作为底层存储结构来实现栈。 由于数组的连续内存特性,顺序栈的实现相对简单,访问速度快,但容量固定或扩展需要一定开销。
- 链式栈 (Linked-list Stack): 使用链表作为底层存储结构来实现栈。 链式栈的优点是容量可以动态扩展,插入和删除操作高效,但访问速度相对顺序栈稍慢,并且需要额外的指针存储空间。
尽管实现方式不同,但它们都遵循栈的 LIFO 原则,对外提供的操作接口(压栈、弹栈、查看栈顶等)是相同的。
因此,在概念上,我们通常不严格区分栈的类型,除非特别强调实现细节。
栈的声明 (Declaration)
不同编程语言中,栈的实现方式和声明语法略有不同。很多语言的标准库中都提供了栈的实现。
Python:
Python 的标准库中没有直接名为 “Stack” 的类,但可以使用
list列表来模拟栈的操作,因为列表提供了append()(压栈) 和pop()(弹栈) 方法,可以方便地实现 LIFO 原则。 或者,可以使用collections.deque双端队列,它也支持高效的栈操作。1
2
3
4
5
6# 使用 list 模拟栈
stack_list = []
# 使用 collections.deque 实现栈 (更高效,尤其在频繁操作栈顶时)
from collections import deque
stack_deque = deque()Java:
Java 提供了
java.util.Stack类来实现栈。 但官方推荐使用Deque接口的实现类,例如ArrayDeque或LinkedList来作为栈使用,因为Stack类是遗留类,效率和设计上存在一些问题。 通常使用ArrayDeque作为栈的实现。1
2
3
4
5
6
7
8
9
10// 使用 java.util.Stack 类 (不推荐)
java.util.Stack<Integer> stackLegacy = new java.util.Stack<>();
// 使用 ArrayDeque 作为栈 (推荐)
import java.util.ArrayDeque;
java.util.Deque<Integer> stackDeque = new java.util.ArrayDeque<>();
// 或者使用 LinkedList 作为栈 (Deque 接口的实现)
import java.util.LinkedList;
java.util.Deque<Integer> stackLinked = new java.util.LinkedList<>();C++:
C++ 标准库提供了
<stack>头文件,其中std::stack是栈的容器适配器。 默认情况下,std::stack使用std::deque作为底层容器实现,也可以指定std::vector或std::list作为底层容器。1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <iostream>
#include <stack> // 栈容器
int main() {
// 使用默认容器 deque 的栈
std::stack<int> stackDefault;
// 使用 vector 作为底层容器的栈
std::stack<int, std::vector<int>> stackVector;
// 使用 list 作为底层容器的栈
std::stack<int, std::list<int>> stackList;
return 0;
}
栈的基本操作 (Operations)
栈的基本操作非常简洁,主要包括以下几种:
压栈 (Push): 将一个元素放入栈顶。 相当于把一个新盘子放到盘子堆的最上面。
- Python:
stack.append(element)(使用list模拟栈),stack.append(element)(使用deque) - Java:
stack.push(element)(使用Stack类),stack.push(element)或stack.addLast(element)(使用Deque接口实现类) - C++:
stack.push(element)
1
2
3
4
5stack = [] # 或者 stack = deque()
stack.append(10) # 压栈元素 10
stack.append(20) # 压栈元素 20
stack.append(30) # 压栈元素 30
# 栈现在内容 (从栈底到栈顶): [10, 20, 30]- Python:
弹栈 (Pop): 从栈顶取出一个元素并删除。 相当于从盘子堆的最上面拿走一个盘子。 注意:弹栈操作通常会返回被弹出的元素。
- Python:
stack.pop()(使用list或deque) - Java:
stack.pop()(使用Stack类),stack.pop()或stack.removeLast()(使用Deque接口实现类) - C++:
stack.pop()
1
2
3
4stack = [10, 20, 30] # 或者 stack = deque([10, 20, 30])
top_element = stack.pop() # 弹栈,取出栈顶元素 30
print(top_element) # 输出: 30
# 栈现在内容: [10, 20]- Python:
查看栈顶 (Peek/Top): 查看栈顶元素,但不取出。 相当于看一下盘子堆最上面的盘子是什么,但不把它拿走。
- Python:
stack[-1](使用list模拟栈,索引 -1 访问最后一个元素),stack[-1](使用deque,索引 -1 也访问最后一个元素), 或者更安全的方式是先判断栈是否为空再访问。 - Java:
stack.peek()(使用Stack类),stack.peek()或stack.getLast()或stack.peekLast()(使用Deque接口实现类) - C++:
stack.top()
1
2
3
4stack = [10, 20, 30] # 或者 stack = deque([10, 20, 30])
top_element = stack[-1] # 查看栈顶元素
print(top_element) # 输出: 30
# 栈内容仍然是: [10, 20, 30]- Python:
判空 (IsEmpty): 检查栈是否为空。
- Python:
len(stack) == 0(使用list或deque) - Java:
stack.isEmpty()(使用Stack类或Deque接口实现类) - C++:
stack.empty()
1
2
3
4
5
6stack = [] # 或者 stack = deque()
is_empty = len(stack) == 0
print(is_empty) # 输出: True
stack.append(10)
is_empty = len(stack) == 0
print(is_empty) # 输出: False- Python:
获取栈大小 (Size): 获取栈中元素的个数。
- Python:
len(stack)(使用list或deque) - Java:
stack.size()(使用Stack类或Deque接口实现类) - C++:
stack.size()
1
2
3stack = [10, 20, 30] # 或者 stack = deque([10, 20, 30])
stack_size = len(stack)
print(stack_size) # 输出: 3- Python:
代码示例
C++顺序栈:
1 | |
C++ 链式栈:
1 | |
Python 顺序栈:
1 | |
Python 链式栈:
1 | |
适用场景 (Use Cases) 及示例
栈的 LIFO 特性使其在很多场景下非常有用,尤其是在需要回溯或反向处理的场合。
函数调用栈 (Function Call Stack): 这是栈最经典的应用之一。 当程序调用一个函数时,会将函数的相关信息(例如,返回地址、局部变量、参数等)压入栈中,当函数执行完毕返回时,再从栈顶弹出这些信息,程序才能正确地返回到调用函数的位置继续执行。 递归调用 也依赖于函数调用栈来管理每一层递归的状态。
示例 (递归阶乘函数):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
result = factorial(3) # 调用 factorial(3)
print(result) # 输出 6
# 函数调用栈的变化 (简化示意):
# 1. 调用 factorial(3),信息入栈 (factorial(3))
# 2. 调用 factorial(2),信息入栈 (factorial(2))
# 3. 调用 factorial(1),信息入栈 (factorial(1))
# 4. 调用 factorial(0),信息入栈 (factorial(0)),返回 1
# 5. factorial(0) 返回,栈顶信息弹出
# 6. factorial(1) 得到返回值 1,计算 1*1=1,返回 1,栈顶信息弹出
# 7. factorial(2) 得到返回值 1,计算 2*1=2,返回 2,栈顶信息弹出
# 8. factorial(3) 得到返回值 2,计算 3*2=6,返回 6,栈顶信息弹出
# 9. 最终结果 6表达式求值 (Expression Evaluation): 编译器和解释器使用栈来解析和计算数学表达式,例如中缀表达式到后缀表达式的转换,以及后缀表达式的求值。
示例 (简单的后缀表达式求值): 假设有后缀表达式 “3 4 + 2 * “
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30# 算法步骤 (使用栈):
# 1. 遇到数字 "3",压栈 (栈: [3])
# 2. 遇到数字 "4",压栈 (栈: [3, 4])
# 3. 遇到运算符 "+",弹栈两次,得到 4 和 3,计算 3+4=7,结果 7 压栈 (栈: [7])
# 4. 遇到数字 "2",压栈 (栈: [7, 2])
# 5. 遇到运算符 "*",弹栈两次,得到 2 和 7,计算 7*2=14,结果 14 压栈 (栈: [14])
# 6. 表达式结束,栈中唯一的值 14 就是结果
# Python 代码 (简化版,仅处理加法和乘法,假设输入是有效的后缀表达式)
def evaluate_postfix_expression(expression):
tokens = expression.split() # 分割成 token 列表
stack = []
for token in tokens:
if token.isdigit(): # 如果是数字
stack.append(int(token)) # 压栈数字 (转换为整数)
else: # 如果是运算符
operand2 = stack.pop() # 弹出第二个操作数 (注意顺序)
operand1 = stack.pop() # 弹出第一个操作数
if token == '+':
result = operand1 + operand2
elif token == '*':
result = operand1 * operand2
else:
raise ValueError("Unsupported operator")
stack.append(result) # 将计算结果压栈
return stack.pop() # 最终结果在栈顶
expression = "3 4 + 2 * "
result = evaluate_postfix_expression(expression)
print(result) # 输出 14浏览器历史记录 (Browser History): 浏览器使用栈来管理用户的浏览历史。 每当你访问一个新的网页时,该网页的 URL 会被压入栈中。 当你点击 “后退” 按钮时,浏览器会从栈顶弹出一个 URL,并加载该网页。 “前进” 功能通常可以用另一个栈来实现,或者在原历史栈的基础上进行操作。
操作流程 (简化示意):
- 访问网页 A: 网页 A 的 URL 压栈 (栈: [A])
- 访问网页 B: 网页 B 的 URL 压栈 (栈: [A, B])
- 访问网页 C: 网页 C 的 URL 压栈 (栈: [A, B, C])
- 点击 “后退”: 栈顶 URL (C) 弹出,加载网页 B (栈: [A, B])
- 再次点击 “后退”: 栈顶 URL (B) 弹出,加载网页 A (栈: [A])
- 点击 “前进” (假设前进栈存在且有内容): 从前进栈弹出一个 URL,压入历史栈,加载该网页。
- 其他应用场景:
- 撤销操作 (Undo/Redo): 编辑器、绘图软件等使用栈来记录用户的操作历史,实现撤销和重做功能。
- 括号匹配 (Parentheses Matching): 编译器使用栈来检查代码中的括号是否正确匹配。
- 深度优先搜索 (DFS - Depth First Search): 图算法中的深度优先搜索可以使用栈来辅助实现。
总结
栈是一种简单但非常强大的数据结构,其核心思想是 后进先出 (LIFO) 。 理解栈的概念、操作和应用场景,对于理解程序运行的底层机制,以及解决特定类型的问题非常有帮助。 在后续的学习中,你会在更多算法和数据结构的应用中看到栈的身影。