1. 算法训练营第十一天内容概览
今天要啃下三道经典LeetCode题目:逆波兰表达式求值、滑动窗口最大值和前K个高频元素。这三道题分别来自栈与队列、堆/优先队列这两个重要的数据结构专题,也是面试中的高频考点。作为过来人,我建议先把每道题的底层原理吃透,再动手写代码,这样效率会高很多。
先说说这三道题在面试中的分量:逆波兰表达式考察栈的经典应用,滑动窗口最大值是单调队列的典型场景,而前K个高频元素则是堆结构的完美用例。掌握它们不仅能帮你解决具体问题,更能培养用合适数据结构解决特定问题的思维模式。
2. 150. 逆波兰表达式求值详解
2.1 逆波兰表达式原理剖析
逆波兰表达式(Reverse Polish Notation,RPN)也叫后缀表达式,它的核心特点是操作符位于两个操作数之后。比如常规的"3 + 4"写成逆波兰形式就是"3 4 +"。这种表示法的最大优势是完全消除了括号的歧义,计算时只需要一个栈就能轻松处理。
关键点:遇到数字就压栈,遇到运算符就弹出栈顶两个元素运算,结果再压回栈中
实际工程中,这种表达式计算方式在计算器程序、虚拟机指令集设计中都有应用。比如Java虚拟机(JVM)的字节码执行就采用了类似的栈式结构。
2.2 代码实现与边界处理
用Python实现时,要注意几个细节:
- 除法处理:题目要求整数除法向零截断,所以要用
int(a/b)而不是a//b - 运算顺序:先弹出的元素是右操作数,这点在减法和除法时要特别注意
- 输入验证:确保最后栈中只剩一个元素
python复制def evalRPN(tokens):
stack = []
ops = {
'+': lambda a,b: a+b,
'-': lambda a,b: a-b,
'*': lambda a,b: a*b,
'/': lambda a,b: int(a/b)
}
for t in tokens:
if t in ops:
b = stack.pop()
a = stack.pop()
stack.append(ops[t](a,b))
else:
stack.append(int(t))
return stack[0]
2.3 复杂度分析与优化
时间复杂度O(n),空间复杂度O(n)。当表达式很长时,可以考虑以下优化:
- 预分配栈空间减少动态扩容开销
- 使用数组模拟栈提升访问速度
- 对于纯数字表达式可以提前计算常量部分
3. 239. 滑动窗口最大值深入解析
3.1 暴力解法与问题分析
最直观的解法是遍历每个窗口,找出最大值。这样时间复杂度是O(n*k),当k很大时性能会很差。比如对于数组大小为10^5,k=10^4的情况,这种解法显然不适用。
3.2 单调队列的精妙设计
最优解使用双端队列维护一个单调递减的序列,队首始终是当前窗口最大值。这个数据结构有三大关键操作:
- 入队时移除所有小于当前元素的队尾元素
- 出队时检查队首是否已离开窗口
- 只在窗口形成后才开始记录结果
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
3.3 实际应用场景
滑动窗口最大值算法在以下场景很有用:
- 股票分析中的移动最高价计算
- 网络流量监控中的峰值检测
- 时序数据分析中的局部最大值提取
4. 347. 前K个高频元素全面解析
4.1 多种解法对比
这道题至少有三种主流解法:
- 哈希表统计+排序:O(nlogn)
- 最小堆维护TopK:O(nlogk)
- 桶排序:O(n)但空间可能较大
对于面试来说,最小堆解法是最值得掌握的,因为它平衡了时间复杂度和实现难度。
4.2 最小堆实现细节
Python的heapq模块默认是最小堆,所以存储频率时要取负数。关键步骤:
- 先用哈希表统计每个元素出现次数
- 维护一个大小为k的最小堆
- 最后反转输出结果
python复制import heapq
from collections import defaultdict
def topKFrequent(nums, k):
freq = defaultdict(int)
for num in nums:
freq[num] += 1
heap = []
for num, count in freq.items():
if len(heap) < k:
heapq.heappush(heap, (count, num))
else:
if count > heap[0][0]:
heapq.heappop(heap)
heapq.heappush(heap, (count, num))
return [num for count, num in heap]
4.4 工程实践中的优化
在实际工程中,如果数据量特别大:
- 可以考虑使用外部排序
- 对于流式数据,可以使用空间效率更高的Count-Min Sketch算法
- 分布式环境下可以用MapReduce分治处理
5. 综合训练建议与避坑指南
5.1 常见错误排查
-
逆波兰表达式:
- 运算顺序搞反(特别是减法和除法)
- 忘记处理负数输入
- 除法截断方式错误
-
滑动窗口最大值:
- 队列中存值而不是索引,导致无法判断是否在窗口内
- 过早开始记录结果(窗口未完全形成时)
- 忘记处理空输入情况
-
前K高频元素:
- 堆的大小控制不当
- 频率统计时类型错误
- 处理并列频率时输出顺序不稳定
5.2 调试技巧分享
对于这类问题,我习惯用以下测试用例验证:
- 极端情况:空输入、单个元素、所有元素相同
- 边界情况:k=1、k=数组长度
- 随机生成的大规模数据测试性能
5.3 进阶学习路线
掌握这三题后,可以继续挑战:
- 逆波兰表达式生成(中缀转后缀)
- 滑动窗口最小值/平均值
- 基于快速选择的前K元素算法
- 处理数据流中的TopK问题
这三道题虽然解法不同,但都体现了数据结构的选择对算法效率的决定性影响。在实际编码时,建议先在白板上画出操作流程,再转化为代码,这样能大大减少错误率。