前言
在日常编程中,你是否遇到过这样的场景:浏览器中的"后退"按钮、文本编辑器的"撤销"操作、函数调用的层层嵌套、表达式求值的优先级处理?这些看似不同的功能,背后都依赖于同一种数据结构------栈。
栈是计算机科学中最基础、最实用的数据结构之一,它体现了"后进先出"(LIFO)的简单哲学,却支撑着无数复杂的计算过程。本文将带你深入理解栈的奥秘。
一、栈的基本概念
1.1 什么是栈
栈(Stack) 是一种特殊的线性数据结构,它只允许在固定的一端(称为栈顶)进行插入和删除操作。另一端称为栈底。
1.2 栈的类比理解
想象一下现实生活中的几个场景:
| 现实场景 | 栈操作类比 |
|---|---|
| 餐厅盘子叠放 | 新洗的盘子放在最上面,取用时也从最上面拿 |
| 子弹夹装弹 | 最后压入的子弹最先被击发 |
| 书本堆叠 | 最后放上去的书最先被拿走 |
1.3 栈的核心特性:LIFO原则
LIFO(Last In, First Out,后进先出)是栈的灵魂。这意味着:
-
最后一个进入栈的元素,将是第一个被取出的元素
-
第一个进入栈的元素,将是最后一个被取出
二、栈的基本操作
栈支持以下几种基本操作,时间复杂度均为O(1):
2.1 核心操作
class Stack:
def __init__(self):
self.items = []
def push(self, item):
"""入栈:将元素添加到栈顶"""
self.items.append(item)
def pop(self):
"""出栈:移除并返回栈顶元素"""
if not self.is_empty():
return self.items.pop()
return None
def peek(self):
"""查看栈顶元素:返回但不移除"""
if not self.is_empty():
return self.items[-1]
return None
def is_empty(self):
"""判断栈是否为空"""
return len(self.items) == 0
def size(self):
"""返回栈的大小"""
return len(self.items)
2.2 操作示例演示
初始栈:[]
push(10) → 栈:[10]
push(20) → 栈:[10, 20]
push(30) → 栈:[10, 20, 30]
pop() → 返回30,栈:[10, 20]
peek() → 返回20,栈:[10, 20](不变)
三、栈的实现方式
因为栈是操作受限的线性表,因此栈的存储方式也有顺序存储和链式存储两种方式。
3.1 顺序栈及其实现
顺序存储方式使用数组保存栈元素,得到的是顺序栈。
具体的实现可以看这篇文章。
优点:缓存友好,访问速度快
缺点:需要处理扩容问题
3.2 链式栈及其实现
链式存储方式使用单链表保存栈元素,得到的是链式栈。
具体的实现可以看这篇文章。
优点:动态大小,无需预先分配
缺点:额外指针开销,缓存不友好
四、栈的经典应用场景
4.1 函数调用栈(Call Stack)
这是栈在编程语言中最核心的应用。每个函数调用都会创建一个栈帧(Stack Frame):
function factorial(n) {
if (n <= 1) return 1; // 基本情况
return n * factorial(n - 1); // 递归调用
}
// 调用 factorial(3) 的栈帧变化:
// 1. factorial(3) 入栈
// 2. factorial(2) 入栈
// 3. factorial(1) 入栈 → 返回1 → factorial(1) 出栈
// 4. factorial(2) 返回 2 * 1=2 → 出栈
// 5. factorial(3) 返回 3 * 2=6 → 出栈
4.2 表达式求值
中缀表达式转后缀表达式
def infix_to_postfix(expression):
precedence = {'+':1, '-':1, '*':2, '/':2, '^':3}
stack = []
output = []
for token in expression.split():
if token.isdigit():
output.append(token)
elif token == '(':
stack.append(token)
elif token == ')':
while stack and stack[-1] != '(':
output.append(stack.pop())
stack.pop() # 弹出'('
else: # 运算符
while (stack and stack[-1] != '(' and
precedence.get(stack[-1], 0) >= precedence.get(token, 0)):
output.append(stack.pop())
stack.append(token)
while stack:
output.append(stack.pop())
return ' '.join(output)
# 示例:中缀 "3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3"
# 转换后:后缀 "3 4 2 * 1 5 - 2 3 ^ ^ / +"
后缀表达式求值
int evaluatePostfix(const string& expression) {
stack<int> st;
for (char c : expression) {
if (isdigit(c)) {
st.push(c - '0');
} else {
int b = st.top(); st.pop();
int a = st.top(); st.pop();
switch(c) {
case '+': st.push(a + b); break;
case '-': st.push(a - b); break;
case '*': st.push(a * b); break;
case '/': st.push(a / b); break;
}
}
}
return st.top();
}
4.3 括号匹配
这是栈的经典面试题,用于检查括号是否正确闭合:
def is_valid_parentheses(s: str) -> bool:
stack = []
mapping = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in mapping.values(): # 左括号
stack.append(char)
elif char in mapping: # 右括号
if not stack or stack[-1] != mapping[char]:
return False
stack.pop()
return not stack # 栈应为空
# 测试用例
print(is_valid_parentheses("()[]{}")) # True
print(is_valid_parentheses("([)]")) # False
print(is_valid_parentheses("{[]}")) # True
4.4 浏览器历史记录
浏览器的前进后退功能可以用两个栈实现:
class BrowserHistory {
private backStack: string[] = [];
private forwardStack: string[] = [];
private current: string = "home";
// 访问新页面
visit(url: string): void {
this.backStack.push(this.current);
this.current = url;
this.forwardStack = []; // 清空前进栈
}
// 后退
back(): string | null {
if (this.backStack.length === 0) return null;
this.forwardStack.push(this.current);
this.current = this.backStack.pop()!;
return this.current;
}
// 前进
forward(): string | null {
if (this.forwardStack.length === 0) return null;
this.backStack.push(this.current);
this.current = this.forwardStack.pop()!;
return this.current;
}
}
4.5 深度优先搜索(DFS)
栈是实现DFS的基础数据结构:
def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
vertex = stack.pop()
if vertex not in visited:
visited.add(vertex)
print(vertex) # 处理节点
# 将相邻节点入栈
for neighbor in graph[vertex]:
if neighbor not in visited:
stack.append(neighbor)
五、栈的变体与扩展
5.1 最小栈
在O(1)时间内获取栈中最小元素:
class MinStack {
private Stack<Integer> stack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();
public void push(int x) {
stack.push(x);
if (minStack.isEmpty() || x <= minStack.peek()) {
minStack.push(x);
}
}
public void pop() {
if (stack.pop().equals(minStack.peek())) {
minStack.pop();
}
}
public int getMin() {
return minStack.peek();
}
}
5.2 单调栈
用于解决"下一个更大元素"类问题:
def next_greater_element(nums):
result = [-1] * len(nums)
stack = [] # 存储索引
for i in range(len(nums)):
while stack and nums[i] > nums[stack[-1]]:
idx = stack.pop()
result[idx] = nums[i]
stack.append(i)
return result
# 示例:[2, 1, 2, 4, 3] → [4, 2, 4, -1, -1]
六、栈的复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| push | O(1) | O(1) | 最好/最坏/平均情况都是O(1) |
| pop | O(1) | O(1) | 同上 |
| peek | O(1) | O(1) | 同上 |
| 搜索 | O(n) | O(1) | 需要遍历整个栈 |
| 内存 | O(n) | O(n) | 存储n个元素 |
七、参考文章
-
《算法导论》- 栈与队列章节
-
《数据结构与算法分析:C语言描述》
-
相关LeetCode专题:栈与递归