1. 力扣Hot100栈专题解析
栈(Stack)作为计算机科学中最基础的数据结构之一,其"后进先出"(LIFO)的特性使其在算法题解中具有独特价值。力扣(LeetCode)Hot100榜单中的栈相关题目,往往考察开发者对栈结构的深刻理解以及灵活应用能力。这类题目通常涉及括号匹配、表达式计算、单调栈等经典场景,是面试中高频出现的题型。
栈的操作看似简单——仅包含push(压栈)和pop(弹栈)两种基本操作,但实际应用中需要配合递归、指针等其他技巧。例如在处理树的前序/中序遍历时,递归解法本质上就是利用函数调用栈,而迭代解法则需要显式地维护一个栈结构。
2. 栈的核心操作与实现方式
2.1 基础栈操作分析
栈的标准操作包括:
push(x):将元素x压入栈顶pop():移除并返回栈顶元素peek()/top():获取栈顶元素但不移除isEmpty():判断栈是否为空
在Python中,列表(list)天然支持栈操作:
python复制stack = []
stack.append(1) # push操作
top = stack[-1] # peek操作
val = stack.pop() # pop操作
但需要注意,直接使用列表的pop(0)会变为队列操作,时间复杂度为O(n),这与栈的O(1)特性相违背。Java中更推荐使用Deque接口的实现类:
java复制Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
int top = stack.peek();
int val = stack.pop();
2.2 栈的边界条件处理
实际编码中最易出错的是栈的边界条件。例如在实现最小栈问题时,需要同步维护一个辅助栈来记录最小值,而弹栈操作时两个栈需要保持同步:
python复制class MinStack:
def __init__(self):
self.stack = []
self.min_stack = [float('inf')]
def push(self, val: int) -> None:
self.stack.append(val)
self.min_stack.append(min(val, self.min_stack[-1]))
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
提示:当题目要求实现特殊功能的栈时,考虑使用辅助数据结构是常见思路。同步维护的时间复杂度虽然增加,但保持了每个操作的O(1)特性。
3. Hot100经典栈题型解析
3.1 括号匹配问题(20. Valid Parentheses)
这是栈最典型的应用场景。我们需要检查字符串中的括号是否有效闭合,包括三种括号类型:(), [], {}。
关键点在于:
- 遇到左括号就压栈
- 遇到右括号时检查栈顶是否匹配
- 最终栈应为空
python复制def isValid(s: str) -> bool:
mapping = {')':'(', ']':'[', '}':'{'}
stack = []
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
常见错误是忽略栈提前为空的情况(如输入"]"),以及最后栈不为空的情况(如输入"[")。
3.2 每日温度问题(739. Daily Temperatures)
这道题需要找出每一天之后更高温度出现的间隔天数,是单调栈的典型应用。我们维护一个温度递减的栈,当遇到更高温度时弹出栈中元素并计算间隔。
python复制def dailyTemperatures(T: List[int]) -> List[int]:
stack = []
res = [0] * len(T)
for i, temp in enumerate(T):
while stack and temp > T[stack[-1]]:
prev = stack.pop()
res[prev] = i - prev
stack.append(i)
return res
单调栈的时间复杂度为O(n),其核心思想是及时移除不再需要的元素,减少不必要的比较。类似的问题还有接雨水(42. Trapping Rain Water),需要同时维护左右边界。
4. 栈的进阶应用场景
4.1 表达式求值(224. Basic Calculator)
处理包含加减和括号的算术表达式时,栈能很好地管理运算优先级和括号嵌套。我们需要两个栈分别存储数字和运算符:
python复制def calculate(s: str) -> int:
num_stack, op_stack = [], []
i, n = 0, len(s)
while i < n:
if s[i] == ' ':
i += 1
continue
if s[i] in '+-(':
op_stack.append(s[i])
i += 1
elif s[i] == ')':
while op_stack[-1] != '(':
self.evaluate(num_stack, op_stack)
op_stack.pop() # 弹出左括号
i += 1
else:
num = 0
while i < n and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
num_stack.append(num)
# 如果栈顶是加减号,立即计算
if op_stack and op_stack[-1] in '+-':
self.evaluate(num_stack, op_stack)
return num_stack[0]
def evaluate(self, num_stack, op_stack):
b, a = num_stack.pop(), num_stack.pop()
op = op_stack.pop()
num_stack.append(a + b if op == '+' else a - b)
4.2 二叉树遍历的迭代实现
虽然递归实现更直观,但迭代解法能避免栈溢出风险。以前序遍历为例:
python复制def preorderTraversal(root: TreeNode) -> List[int]:
res = []
stack = []
curr = root
while curr or stack:
while curr:
res.append(curr.val)
stack.append(curr)
curr = curr.left
curr = stack.pop()
curr = curr.right
return res
中序遍历只需调整访问顺序:
python复制def inorderTraversal(root: TreeNode) -> List[int]:
res = []
stack = []
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
后序遍历最为复杂,需要记录前一个访问的节点来判断是否已经处理过右子树:
python复制def postorderTraversal(root: TreeNode) -> List[int]:
res = []
stack = []
prev = None
curr = root
while curr or stack:
while curr:
stack.append(curr)
curr = curr.left
curr = stack[-1]
if not curr.right or curr.right == prev:
res.append(curr.val)
stack.pop()
prev = curr
curr = None
else:
curr = curr.right
return res
5. 栈与其他数据结构的组合应用
5.1 用栈实现队列(232. Implement Queue using Stacks)
使用两个栈可以实现队列的FIFO特性。一个栈负责入队,另一个负责出队:
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._transfer()
return self.out_stack.pop()
def peek(self) -> int:
self._transfer()
return self.out_stack[-1]
def empty(self) -> bool:
return not self.in_stack and not self.out_stack
def _transfer(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
每个元素最多被压入和弹出每个栈各一次,因此平摊时间复杂度为O(1)。
5.2 最大频率栈(895. Maximum Frequency Stack)
这是一道设计题,要求实现一个特殊栈,pop时返回出现频率最高的元素,频率相同时返回最近添加的。这需要同时维护频率哈希表和频率桶:
python复制class FreqStack:
def __init__(self):
self.freq = defaultdict(int)
self.group = defaultdict(list)
self.max_freq = 0
def push(self, val: int) -> None:
self.freq[val] += 1
freq = self.freq[val]
self.group[freq].append(val)
self.max_freq = max(self.max_freq, freq)
def pop(self) -> int:
val = self.group[self.max_freq].pop()
self.freq[val] -= 1
if not self.group[self.max_freq]:
self.max_freq -= 1
return val
这种设计将相同频率的元素保存在栈结构中,保证了LIFO特性,而max_freq则跟踪当前最高频率。
6. 栈问题解题技巧总结
经过对Hot100栈专题的深入分析,我总结出以下几点解题经验:
-
括号匹配类问题:立即想到栈结构,注意处理三种括号类型和嵌套情况。可以先用少量示例手动模拟栈操作。
-
单调栈应用:当问题涉及"下一个更大/小元素"时,考虑维护一个单调递增或递减的栈。及时弹出不再需要的元素是关键。
-
表达式求值:使用双栈法(操作数栈和运算符栈),注意处理括号带来的优先级变化。可以先将中缀表达式转为后缀表达式再计算。
-
递归转迭代:任何递归算法都可以用栈+循环改写,这在处理树结构时特别有用。需要显式模拟函数调用栈。
-
边界条件:始终考虑栈为空时的操作,特别是在处理pop/peek操作时。防御性编程可以避免许多运行时错误。
在实际面试中,建议先明确说明栈的选用理由,再逐步构建解法。例如:"由于这个问题需要反向处理最近遇到的元素,栈的LIFO特性非常适合这个场景..."。同时可以画出栈的操作示意图,帮助面试官理解你的思路。
