1. 栈数据结构基础认知
栈(Stack)这个数据结构就像我们日常生活中叠放的盘子——最后放上去的盘子总是最先被取用。这种后进先出(LIFO)的特性使其在算法领域有着不可替代的价值。我在处理浏览器历史记录、函数调用栈等场景时,深刻体会到栈结构的精妙之处。
栈的核心操作主要包含三个:
- push(入栈):将元素添加到栈顶
- pop(出栈):移除并返回栈顶元素
- peek(查看栈顶):获取但不移除栈顶元素
在算法题中,栈常用于解决需要"最近相关性"的问题。比如括号匹配问题,当遇到右括号时,我们只需要检查栈顶元素是否与之匹配即可,这种特性大幅提升了算法效率。
2. 高频栈问题解题框架
2.1 单调栈解题模式
单调栈是我在解决"下一个更大元素"这类问题时最常用的技巧。其核心是维护一个栈内元素单调递增或递减的结构。以LeetCode 496题为例,我们需要找到nums2中每个元素右侧第一个比它大的数。
python复制def nextGreaterElement(nums1, nums2):
stack = []
mapping = {}
for num in nums2:
while stack and num > stack[-1]:
mapping[stack.pop()] = num
stack.append(num)
return [mapping.get(num, -1) for num in nums1]
这个解法的时间复杂度是O(n),空间复杂度也是O(n)。关键在于理解while循环的条件——只有当当前元素大于栈顶元素时才持续出栈,这保证了栈内元素的单调性。
2.2 括号匹配类问题
括号匹配是栈的经典应用场景。以LeetCode 20题为例,我们需要验证输入的括号字符串是否有效。我的解题思路是:
- 遇到左括号就入栈
- 遇到右括号就检查栈顶是否匹配
- 最后检查栈是否为空
python复制def isValid(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
top = stack.pop() if stack else '#'
if mapping[char] != top:
return False
else:
stack.append(char)
return not stack
这里使用字典存储括号对映关系是个小技巧,可以避免写多个if-else判断。时间复杂度O(n),空间复杂度O(n)。
3. 栈的进阶应用场景
3.1 表达式求值
处理表达式求值(如LeetCode 224题)时,我们需要同时使用数值栈和运算符栈。这是我总结的解题步骤:
- 初始化两个空栈和运算符优先级字典
- 遍历表达式字符:
- 遇到数字:完整读取后入数值栈
- 遇到左括号:入运算符栈
- 遇到右括号:持续计算直到遇到左括号
- 遇到运算符:比较优先级后决定是否先计算栈内运算符
- 最后清空运算符栈
python复制def calculate(s: str) -> int:
def compute(ops, nums):
op = ops.pop()
b = nums.pop()
a = nums.pop()
if op == '+': nums.append(a + b)
else: nums.append(a - b)
ops, nums = [], []
i = 0
while i < len(s):
c = s[i]
if c.isdigit():
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
nums.append(num)
continue
elif c in '+-':
while ops and ops[-1] != '(':
compute(ops, nums)
ops.append(c)
elif c == '(':
ops.append(c)
elif c == ')':
while ops[-1] != '(':
compute(ops, nums)
ops.pop()
i += 1
while ops:
compute(ops, nums)
return nums[0]
这个解法处理了加减法和括号,可以扩展到乘除法。时间复杂度O(n),空间复杂度O(n)。
3.2 最小栈问题
实现一个能在O(1)时间内获取最小值的栈(LeetCode 155题)是个经典面试题。我的解决方案是使用辅助栈同步存储最小值:
python复制class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
这里的关键点在于push操作时的条件判断——只有当新值小于等于当前最小值时才入辅助栈。这样能保证pop时只需检查是否等于当前最小值即可。所有操作的时间复杂度都是O(1)。
4. 栈与其他数据结构的组合应用
4.1 栈与队列的相互实现
用栈实现队列(LeetCode 232题)和用队列实现栈(LeetCode 225题)是考察数据结构理解的经典题目。我的实现方案如下:
用栈实现队列:
python复制class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x: int) -> None:
self.in_stack.append(x)
def pop(self) -> int:
self._move()
return self.out_stack.pop()
def peek(self) -> int:
self._move()
return self.out_stack[-1]
def empty(self) -> bool:
return not self.in_stack and not self.out_stack
def _move(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
用队列实现栈:
python复制from collections import deque
class MyStack:
def __init__(self):
self.queue = deque()
def push(self, x: int) -> None:
self.queue.append(x)
for _ in range(len(self.queue) - 1):
self.queue.append(self.queue.popleft())
def pop(self) -> int:
return self.queue.popleft()
def top(self) -> int:
return self.queue[0]
def empty(self) -> bool:
return not self.queue
这两种实现的关键区别在于:
- 栈→队列:需要两个栈来回倒换数据
- 队列→栈:每次push后需要旋转队列元素
时间复杂度分析:
- 栈实现队列:均摊O(1)时间复杂度
- 队列实现栈:push操作O(n),其他O(1)
4.2 栈与DFS的结合应用
在二叉树遍历中,栈可以用来实现迭代式的DFS。以中序遍历为例(LeetCode 94题):
python复制def inorderTraversal(root):
stack = []
res = []
curr = root
while curr or stack:
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
res.append(curr.val)
curr = curr.right
return res
这个解法的时间复杂度是O(n),空间复杂度是O(h),h是树的高度。相比递归解法,迭代式DFS能避免递归栈溢出的风险,特别适合处理深度很大的树结构。
5. 栈问题实战技巧与避坑指南
5.1 边界条件处理经验
在解决栈相关问题时,我总结了几类常见的边界条件:
- 空栈处理:pop/peek操作前必须检查栈是否为空
- 输入为空:特别是括号匹配类问题
- 栈未清空:如表达式求值最后需要检查运算符栈
- 特殊字符:如表达式中的空格需要跳过
以LeetCode 32最长有效括号为例,我的解法中就特别注意了边界处理:
python复制def longestValidParentheses(s):
stack = [-1]
max_len = 0
for i, char in enumerate(s):
if char == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i)
else:
max_len = max(max_len, i - stack[-1])
return max_len
这里初始化栈为[-1]是个关键技巧,用于处理第一个字符就是')'的情况。时间复杂度O(n),空间复杂度O(n)。
5.2 性能优化策略
对于栈问题,我常用的优化方法包括:
- 提前终止:如括号匹配中发现不匹配立即返回
- 空间优化:如最小栈问题可以只存储差值
- 并行处理:如同时维护多个栈解决复杂问题
- 预处理:如先处理特殊字符或转换输入格式
以空间优化版的最小栈为例:
python复制class MinStack:
def __init__(self):
self.stack = []
self.min_val = float('inf')
def push(self, val):
if val <= self.min_val:
self.stack.append(self.min_val)
self.min_val = val
self.stack.append(val)
def pop(self):
if self.stack.pop() == self.min_val:
self.min_val = self.stack.pop()
def top(self):
return self.stack[-1]
def getMin(self):
return self.min_val
这个版本在最坏情况下空间复杂度仍是O(n),但平均情况下能节省空间。不过可读性有所下降,需要权衡使用。
6. 栈的扩展应用与变种问题
6.1 最大矩形问题
LeetCode 84题柱状图中最大矩形是栈的高级应用。我的解法基于单调栈:
python复制def largestRectangleArea(heights):
stack = []
max_area = 0
heights.append(0) # 添加哨兵值
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
这个解法的时间复杂度是O(n),关键在于理解宽度计算方式:
- 当栈为空时,宽度就是当前索引i
- 否则宽度是i - stack[-1] - 1
6.2 雨水收集问题
LeetCode 42题接雨水也可以用单调栈解决:
python复制def trap(height):
stack = []
res = 0
for i, h in enumerate(height):
while stack and h > height[stack[-1]]:
bottom = height[stack.pop()]
if not stack:
break
left = stack[-1]
distance = i - left - 1
bounded_height = min(height[left], h) - bottom
res += distance * bounded_height
stack.append(i)
return res
这个解法同样保持栈内高度单调递减,当遇到较高柱子时计算积水面积。时间复杂度O(n),空间复杂度O(n)。
在实际编码中,我发现这类问题的关键在于:
- 正确维护单调栈的性质
- 准确计算宽度和高度
- 处理好边界条件(如空栈情况)