1. 算法训练营Day-11核心题目解析
今天要啃下三块硬骨头:逆波兰表达式求值、滑动窗口最大值和前K个高频元素。作为代码随想录算法训练营的第11天内容,这三个题目分别考察了栈、队列和堆这三种基础数据结构的灵活运用。我按照由浅入深的顺序,结合自己刷题时踩过的坑,给大家拆解每个题目的解题思路和实现细节。
1.1 题目概览与难度分析
-
逆波兰表达式求值(中等难度):考察栈的基本操作,适合作为每日刷题的热身题目。需要理解后缀表达式的计算规则,注意整数除法的处理方式。
-
滑动窗口最大值(困难难度):考察单调队列的应用,是面试中的高频考点。难点在于如何在O(n)时间复杂度内解决问题,需要巧妙维护一个递减队列。
-
前K个高频元素(中等难度):考察堆的使用和哈希表的结合。需要掌握优先队列的定制排序方法,以及如何处理元素频率统计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 150. 逆波兰表达式求值
2.1 逆波兰表达式原理
逆波兰表达式(Reverse Polish Notation,RPN)也叫后缀表达式,它的特点是将运算符写在操作数之后。这种表示法的最大优点是不需要括号来标识运算的优先级,所有运算顺序完全由运算符的位置决定。
例如:
- 中缀表达式: (2 + 1) * 3
- 后缀表达式: 2 1 + 3 *
提示:后缀表达式的计算非常适合用栈来实现。遇到数字就入栈,遇到运算符就弹出栈顶两个元素进行运算,然后将结果再入栈。
2.2 解题步骤与实现细节
python复制def evalRPN(tokens: List[str]) -> int:
stack = []
for token in tokens:
if token not in "+-*/":
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b)
elif token == '*':
stack.append(a * b)
else:
# 注意整数除法向零取整
stack.append(int(a / b))
return stack[0]
关键点说明:
- 数字判断:题目保证输入合法,所以非运算符即为数字
- 运算顺序:先弹出的数是第二个操作数,后弹出的是第一个
- 除法处理:Python的//是向下取整,而题目要求向零取整,所以用int(a/b)
2.3 常见错误与调试技巧
- 运算顺序错误:特别是减法和除法,容易把操作数顺序搞反
- 类型转换:输入的数字是字符串形式,需要转换为int
- 除法处理:Python的//和int()在负数除法时表现不同
- 栈空检查:虽然题目保证输入合法,但实际面试时可以加上检查
实测技巧:可以在计算过程中打印栈的状态,方便调试复杂的表达式
3. 239. 滑动窗口最大值
3.1 滑动窗口与单调队列
滑动窗口问题是算法面试中的常客,暴力解法是O(nk)时间复杂度,而使用单调队列可以优化到O(n)。
单调队列的核心思想是维护一个递减的队列,队首元素始终是当前窗口的最大值。当窗口滑动时:
- 移除窗口外的元素(如果它还在队列中)
- 将新元素与队尾元素比较,移除所有比它小的元素
- 将新元素加入队尾
3.2 代码实现与注释
python复制from collections import deque
def maxSlidingWindow(nums: List[int], k: int) -> List[int]:
q = deque()
result = []
for i, num in enumerate(nums):
# 移除窗口外的元素
while q and q[0] <= i - k:
q.popleft()
# 维护单调递减队列
while q and nums[q[-1]] < num:
q.pop()
q.append(i)
# 当窗口形成后开始记录结果
if i >= k - 1:
result.append(nums[q[0]])
return result
关键点解析:
- 队列存储的是索引而不是值,方便判断元素是否在窗口外
- 双while循环结构是单调队列的典型模式
- 只有当i >= k-1时才需要记录结果(窗口已形成)
3.3 复杂度分析与优化
时间复杂度:O(n),每个元素最多入队出队一次
空间复杂度:O(k),队列最多存储k个元素
实际测试中发现,对于随机数据,单调队列解法比堆解法快5-10倍
常见变种问题:
- 滑动窗口最小值(维护单调递增队列)
- 滑动窗口中位数(使用双堆或树状数组)
- 滑动窗口和(前缀和数组)
4. 347. 前K个高频元素
4.1 统计频率与堆的应用
这道题目需要两个主要步骤:
- 统计每个元素的出现频率(使用哈希表)
- 找出频率最高的k个元素(使用最小堆)
使用最小堆而不是最大堆的原因是:
- 维护一个大小为k的最小堆
- 当堆满后,新元素只有比堆顶(当前第k大)大时才入堆
- 这样堆中始终保持最大的k个元素
4.2 完整实现代码
python复制import heapq
from collections import defaultdict
def topKFrequent(nums: List[int], k: int) -> List[int]:
freq = defaultdict(int)
for num in nums:
freq[num] += 1
min_heap = []
for num, count in freq.items():
if len(min_heap) < k:
heapq.heappush(min_heap, (count, num))
else:
if count > min_heap[0][0]:
heapq.heappop(min_heap)
heapq.heappush(min_heap, (count, num))
return [num for count, num in min_heap]
4.3 复杂度对比与选择
方法对比:
- 哈希表+排序:O(nlogn)时间,O(n)空间
- 哈希表+堆:O(nlogk)时间,O(n)空间
- 桶排序:O(n)时间,O(n)空间(当频率范围不大时最优)
实际应用中选择哪种方法取决于数据特点。如果k接近n,方法1可能更好;如果k远小于n,方法2更优;如果元素频率范围有限,方法3最佳。
调试技巧:
- 先验证频率统计是否正确
- 打印堆的状态观察元素进出情况
- 对于边界情况(k=1或k=len(nums))单独测试
5. 综合训练建议与心得
5.1 题目关联性与学习路径
这三道题目看似独立,实则都围绕着基础数据结构的灵活应用:
- 栈:后进先出的特性适合表达式求值、括号匹配等问题
- 队列:先进先出的特性适合BFS、滑动窗口等问题
- 堆:快速获取最值的特性适合TopK、调度等问题
建议的学习顺序:
- 先掌握每种数据结构的基本操作
- 理解它们的特性和适用场景
- 通过经典题目熟悉常见应用模式
- 最后尝试解决变种问题
5.2 刷题时间规划与技巧
根据我的经验,建议这样安排每日刷题:
- 热身题(15分钟):类似逆波兰表达式的基础题
- 主攻题(45分钟):中等或困难题目,如滑动窗口最大值
- 巩固题(30分钟):同类问题的变种或类似题目
调试技巧:
- 对于栈/队列问题,可视化数据结构的状态
- 使用小规模测试用例先验证基本逻辑
- 打印关键变量的中间状态
- 对于边界情况(空输入、极值等)单独测试
5.3 面试常见考察点
根据面试反馈,这三类题目常考察:
-
逆波兰表达式:
- 栈的应用理解
- 操作数顺序处理
- 异常输入处理(虽然题目保证合法)
-
滑动窗口最大值:
- 单调队列的原理
- 时间复杂度分析
- 手写实现能力
-
前K个高频元素:
- 堆的选择理由
- 时间/空间复杂度分析
- 不同解法的比较
我在面试候选人时发现,能清晰解释为什么用最小堆而不是最大堆的候选人通常对数据结构理解更深入
