1. 题目解析与核心思路
这三道LeetCode题目都是经典的Top K问题变种,考察的核心能力是高效数据筛选与排序策略。我们先拆解每道题的核心需求:
1.1 215. 数组中的第K个最大元素
给定整数数组nums和整数k,找到排序后第k个最大的元素。注意不是第k个不同的元素,而是排序后的第k个最大元素。例如[3,2,1,5,6,4]中第2大元素是5。
1.2 347. 前 K 个高频元素
给定整数数组nums和整数k,返回出现频率前k高的元素。例如[1,1,1,2,2,3]中前2高频元素是[1,2]。
1.3 295. 数据流的中位数
设计一个数据结构,支持两种操作:
- addNum(int num):从数据流中添加一个整数到数据结构
- findMedian():返回当前所有元素的中位数
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法分析与代码实现
2.1 215题:快速选择算法
最直接解法是排序后取第k个元素,时间复杂度O(nlogn)。更优解是快速选择算法(Quickselect),平均时间复杂度O(n)。
python复制import random
def findKthLargest(nums, k):
def partition(left, right, pivot_index):
pivot = nums[pivot_index]
nums[pivot_index], nums[right] = nums[right], nums[pivot_index]
store_index = left
for i in range(left, right):
if nums[i] < pivot:
nums[store_index], nums[i] = nums[i], nums[store_index]
store_index += 1
nums[right], nums[store_index] = nums[store_index], nums[right]
return store_index
def select(left, right, k_smallest):
if left == right:
return nums[left]
pivot_index = random.randint(left, right)
pivot_index = partition(left, right, pivot_index)
if k_smallest == pivot_index:
return nums[k_smallest]
elif k_smallest < pivot_index:
return select(left, pivot_index - 1, k_smallest)
else:
return select(pivot_index + 1, right, k_smallest)
return select(0, len(nums) - 1, len(nums) - k)
注意事项:快速选择的最坏时间复杂度是O(n²),但通过随机化pivot可以极大降低这种情况的概率。
2.2 347题:堆与桶排序
方法一:最小堆
- 统计每个元素频率O(n)
- 维护大小为k的最小堆O(nlogk)
python复制import heapq
from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
方法二:桶排序
- 统计频率O(n)
- 按频率分桶O(n)
- 从高频率桶开始收集元素O(n)
python复制from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
buckets = [[] for _ in range(len(nums)+1)]
for num, freq in count.items():
buckets[freq].append(num)
res = []
for i in range(len(buckets)-1, -1, -1):
res.extend(buckets[i])
if len(res) >= k:
break
return res[:k]
实操心得:当k远小于n时堆方法更优,当k接近n时桶排序更高效。
2.3 295题:双堆法
维护两个堆:
- 最大堆存储较小的一半数字
- 最小堆存储较大的一半数字
保持两个堆的大小差不超过1
python复制import heapq
class MedianFinder:
def __init__(self):
self.max_heap = [] # 存储较小的一半(Python默认最小堆,通过存储负数模拟最大堆)
self.min_heap = [] # 存储较大的一半
def addNum(self, num):
if not self.max_heap or num <= -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
# 平衡两个堆的大小
if len(self.max_heap) > len(self.min_heap) + 1:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def findMedian(self):
if len(self.max_heap) == len(self.min_heap):
return (-self.max_heap[0] + self.min_heap[0]) / 2
else:
return -self.max_heap[0]
关键点:每次插入后必须重新平衡堆的大小,确保中位数计算条件成立。
3. 复杂度分析与优化对比
3.1 时间复杂度对比
| 题目 | 暴力解法 | 最优解法 | 最优时间复杂度 |
|---|---|---|---|
| 215 | O(nlogn) | 快速选择 | O(n)平均 |
| 347 | O(nlogn) | 桶排序 | O(n) |
| 295 | O(n)每次 | 双堆法 | O(logn)每次 |
3.2 空间复杂度对比
| 题目 | 最优解法 | 空间复杂度 |
|---|---|---|
| 215 | 快速选择 | O(1) |
| 347 | 桶排序 | O(n) |
| 295 | 双堆法 | O(n) |
4. 常见问题与调试技巧
4.1 快速选择算法常见问题
- 无限递归:忘记处理left == right的基本情况
- 错误分区:partition函数没有正确处理等于pivot的元素
- 随机性缺失:固定选择第一个元素作为pivot导致最坏情况
调试方法:
- 打印每次递归的left, right, pivot_index参数
- 检查partition后的数组是否满足左边<=pivot<右边
4.2 堆应用的典型错误
- 堆大小维护错误:在347题中可能错误地维护了最大堆而非最小堆
- 频率统计遗漏:未考虑负数或0的情况
- 堆顶元素访问:忘记Python的heapq是最小堆实现
调试技巧:
- 打印堆内容验证堆性质
- 使用heapq.nlargest/nsmallest简化代码
4.3 双堆法的平衡问题
- 堆大小差超过1:忘记在addNum后重新平衡堆
- 元素分配错误:错误地将元素分配到相反的堆中
- 中位数计算错误:未处理偶数个元素时的平均值计算
调试建议:
- 每次操作后打印两个堆的内容
- 单独测试添加1-5个元素的情况
5. 实际应用场景扩展
5.1 大数据处理中的应用
Top K问题在大数据处理中非常常见:
- 实时统计热门商品
- 监控系统异常检测
- 日志分析高频错误
优化思路:
- 对于超大数据可以使用抽样+精确计算的混合方法
- 考虑使用布隆过滤器等概率数据结构预处理
5.2 系统设计中的应用
295题的变种常见于:
- 实时监控系统指标
- 金融交易系统中价格分析
- 性能监控中的延迟统计
工程实现技巧:
- 添加滑动窗口限制统计范围
- 使用近似算法换取更高吞吐量
5.3 算法选择决策树
面对类似问题时可以这样选择:
- 是否需要实时更新?是→考虑堆结构
- 数据是否适合完全加载到内存?否→考虑外部排序
- 是否需要精确结果?否→考虑抽样估算
6. 进阶挑战与变种问题
6.1 多维Top K问题
例如:"找出销售额前K高且评分也前K的商品"
解法:可以使用多条件排序或分层筛选
6.2 分布式Top K计算
当数据量太大无法单机处理时:
- 使用MapReduce框架分割数据
- 每个节点计算局部Top K
- 聚合节点结果得到全局Top K
6.3 滑动窗口Top K
要求只统计最近N个元素中的Top K:
- 结合滑动窗口和堆结构
- 使用双向链表+哈希表实现O(1)删除过期元素
python复制from collections import deque
from heapq import heappush, heappop
class SlidingWindowTopK:
def __init__(self, window_size, k):
self.window = deque()
self.count = {}
self.heap = []
self.window_size = window_size
self.k = k
def add(self, num):
if len(self.window) >= self.window_size:
old_num = self.window.popleft()
self.count[old_num] -= 1
if self.count[old_num] == 0:
del self.count[old_num]
self.window.append(num)
self.count[num] = self.count.get(num, 0) + 1
# 更新堆
self.heap = []
for num in self.count:
heappush(self.heap, (-self.count[num], num))
def get_top_k(self):
result = []
temp_heap = self.heap.copy()
for _ in range(min(self.k, len(temp_heap))):
if temp_heap:
result.append(heappop(temp_heap)[1])
return result
在实际编码面试中,理解这些问题的核心思想比死记硬背更重要。我建议在掌握基础解法后,多思考各种变种情况,并尝试用不同的数据结构解决问题。比如295题还可以用自平衡二叉搜索树实现,虽然代码更复杂但理论时间复杂度相同。
