1. 算法复健训练的价值与意义
最近在算法复健训练中完成了Day10的栈与队列专题,重点攻克了LeetCode上的三道压轴难题:150(逆波兰表达式求值)、239(滑动窗口最大值)和347(前K个高频元素)。这三道题可以说是栈与队列应用的经典代表,涵盖了从基础应用到高级技巧的多个层面。
算法复健这个概念可能有些朋友不太熟悉,简单来说就是像运动员保持竞技状态一样,通过定期训练来维持和提升算法解题能力。对于准备面试或者需要保持编程敏感度的开发者来说,这种训练方式非常有效。我自己坚持算法复健已经半年多,明显感觉到解题思路更加清晰,编码速度也有了显著提升。
2. 三道压轴题的核心思路解析
2.1 LC 150 - 逆波兰表达式求值
这道题是栈结构的经典应用场景。逆波兰表达式(后缀表达式)的特点是操作符位于两个操作数之后,比如"2 1 + 3 *"对应的是常规表达式"(2 + 1) * 3"。
解题的核心思路是:
- 初始化一个空栈
- 遍历表达式中的每个元素
- 遇到数字就压入栈中
- 遇到运算符就从栈顶弹出两个数字进行计算,然后将结果压回栈中
- 最后栈中剩下的唯一元素就是最终结果
注意:减法和除法要注意操作数的顺序,先弹出的是右操作数,后弹出的是左操作数。
实际编码时,我建议使用一个哈希表来存储运算符和对应的lambda函数,这样代码会更加简洁:
python复制operators = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: int(a / b)
}
2.2 LC 239 - 滑动窗口最大值
这道题考察的是如何在每个滑动窗口中找到最大值。暴力解法的时间复杂度是O(n*k),而使用单调队列可以将复杂度优化到O(n)。
单调队列的核心思想是维护一个双端队列,保证队列中的元素从大到小排列,且都在当前窗口范围内。具体实现步骤:
- 初始化双端队列和结果列表
- 遍历数组中的每个元素
- 移除队列中不在当前窗口范围内的元素(从队首)
- 移除队列中比当前元素小的元素(从队尾)
- 将当前元素加入队尾
- 如果窗口已经形成,将队首元素加入结果
python复制from collections import deque
def maxSlidingWindow(nums, k):
q = deque()
res = []
for i, num in enumerate(nums):
while q and nums[q[-1]] <= num:
q.pop()
q.append(i)
if q[0] == i - k:
q.popleft()
if i >= k - 1:
res.append(nums[q[0]])
return res
2.3 LC 347 - 前K个高频元素
这道题要求找出数组中出现频率前k高的元素。解题思路可以分为三步:
- 统计每个元素的出现频率
- 将统计结果存入优先队列(堆)
- 从堆中取出前k个元素
Python中的heapq模块默认实现的是最小堆,所以我们需要将频率取负数来实现最大堆的效果:
python复制import heapq
from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
对于Java等语言,可以自定义比较器来实现相同的功能。
3. 解题过程中的关键技巧与优化
3.1 栈与队列的选择策略
在实际解题中,选择合适的数据结构至关重要:
- 当需要"最近相关性"时(如括号匹配、函数调用栈),优先考虑栈
- 当需要"先进先出"时(如BFS、缓存),优先考虑队列
- 对于滑动窗口最大值这类问题,单调队列是最佳选择
3.2 边界条件的处理经验
在实现这些算法时,有几个常见的边界条件需要特别注意:
- 空输入的处理
- 单个元素的特殊情况
- 运算符的优先级和结合性
- 滑动窗口大小k为1或等于数组长度的情况
- 频率相同元素的处理顺序
3.3 复杂度分析与优化思路
对于每道题,都要清楚其时间复杂度和空间复杂度:
- LC 150:O(n)时间,O(n)空间
- LC 239:O(n)时间,O(k)空间
- LC 347:O(n log k)时间,O(n)空间
在实际面试中,面试官可能会要求进一步优化。比如对于LC 347,当k接近n时,可以使用快速选择算法将时间复杂度优化到O(n)。
4. 常见错误与调试技巧
4.1 栈操作中的常见陷阱
- 忘记检查栈是否为空就执行pop操作
- 操作数顺序错误(特别是减法和除法)
- 整数除法向零取整的处理
- 数字可能是多位数的情况
4.2 单调队列的实现细节
- 队列中存储的是索引还是值
- 窗口滑动时如何正确移除过期元素
- 何时将结果加入结果列表
- 处理数组长度小于k的情况
4.3 堆使用的注意事项
- 最大堆还是最小堆的选择
- 自定义比较器的实现
- 处理频率相同元素的顺序
- 堆的大小控制(特别是对于大数据量的情况)
5. 扩展练习与相关题目推荐
为了巩固栈与队列的应用能力,我推荐以下LeetCode题目作为扩展练习:
- 基础练习:20(有效的括号)、1047(删除字符串中的所有相邻重复项)
- 中等难度:71(简化路径)、402(移掉K位数字)
- 高级应用:84(柱状图中最大的矩形)、862(和至少为K的最短子数组)
对于想挑战更高难度的同学,可以尝试面试中较少出现但很有价值的题目,如85(最大矩形)、321(拼接最大数)等。