本次三道题*堆*
数组中的第K个最大元素
前 K 个高频元素
数据流的中位数
215. 数组中的第K个最大元素

这题我的思路和灵茶山艾府大佬的差不多,不过细节没处理好就直接搬了大佬更好的代码了
class Solution {
private static final Random rand = new Random();
public int findKthLargest(int[] nums, int k) {
int n = nums.length;
int targetIndex = n - k; // 第 k 大元素在升序数组中的下标是 n - k
int left = 0;
int right = n - 1; // 闭区间
while (true) {
int i = partition(nums, left, right);
if (i == targetIndex) {
// 找到第 k 大元素
return nums[i];
}
if (i > targetIndex) {
// 第 k 大元素在 [left, i - 1] 中
right = i - 1;
} else {
// 第 k 大元素在 [i + 1, right] 中
left = i + 1;
}
}
}
// 在子数组 [left, right] 中随机选择一个基准元素 pivot
// 根据 pivot 重新排列子数组 [left, right]
// 重新排列后,<= pivot 的元素都在 pivot 的左侧,>= pivot 的元素都在 pivot 的右侧
// 返回 pivot 在重新排列后的 nums 中的下标
// 特别地,如果子数组的所有元素都等于 pivot,我们会返回子数组的中心下标,避免退化
private int partition(int[] nums, int left, int right) {
// 1. 在子数组 [left, right] 中随机选择一个基准元素 pivot
int i = left + rand.nextInt(right - left + 1);
int pivot = nums[i];
// 把 pivot 与子数组第一个元素交换,避免 pivot 干扰后续划分,从而简化实现逻辑
swap(nums, i, left);
// 2. 相向双指针遍历子数组 [left + 1, right]
// 循环不变量:在循环过程中,子数组的数据分布始终如下图
// [ pivot | <=pivot | 尚未遍历 | >=pivot ]
// ^ ^ ^ ^
// left i j right
i = left + 1;
int j = right;
while (true) {
while (i <= j && nums[i] < pivot) {
i++;
}
// 此时 nums[i] >= pivot
while (i <= j && nums[j] > pivot) {
j--;
}
// 此时 nums[j] <= pivot
if (i >= j) {
break;
}
// 维持循环不变量
swap(nums, i, j);
i++;
j--;
}
// 循环结束后
// [ pivot | <=pivot | >=pivot ]
// ^ ^ ^ ^
// left j i right
// 3. 把 pivot 与 nums[j] 交换,完成划分(partition)
// 为什么与 j 交换?
// 如果与 i 交换,可能会出现 i = right + 1 的情况,已经下标越界了,无法交换
// 另一个原因是如果 nums[i] > pivot,交换会导致一个大于 pivot 的数出现在子数组最左边,不是有效划分
// 与 j 交换,即使 j = left,交换也不会出错
swap(nums, left, j);
// 交换后
// [ <=pivot | pivot | >=pivot ]
// ^
// j
// 返回 pivot 的下标
return j;
}
// 交换 nums[i] 与 nums[j]
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
作者:灵茶山艾府
链接:https://leetcode.cn/problems/kth-largest-element-in-an-array/solutions/3799769/on-kuai-su-xuan-ze-suan-fa-pythonjavaccg-lh7c/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
解题思路:快速选择(Quickselect)
快速选择基于快速排序的「分区」思想:
-
随机选择一个 pivot,将数组划分为:
[小于pivot] + [pivot] + [大于pivot]。 -
比较 pivot 的最终位置
pos与目标索引target = nums.length - k:-
若
pos == target:pivot 即为答案; -
若
pos > target:答案在左半区间[l, pos-1],递归处理; -
若
pos < target:答案在右半区间[pos+1, r],递归处理。
-
-
随机选择 pivot 可避免最坏情况(如已排序数组),保证平均时间复杂度为 O (n)。
347. 前 K 个高频元素
注意:面试可能要求手写堆 
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 1. 统计元素频率
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 2. 小根堆:存储 (频率, 元素),按频率升序
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
int num = entry.getKey();
int freq = entry.getValue();
if (minHeap.size() < k) {
minHeap.offer(new int[]{freq, num});
} else if (freq > minHeap.peek()[0]) {
minHeap.poll();
minHeap.offer(new int[]{freq, num});
}
}
// 3. 提取结果
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = minHeap.poll()[1];
}
return res;
}
}
解题思路1:小根堆法(优先队列)
统计频率 :用 HashMap 统计数组中每个元素的出现次数。
维护 小根堆 :创建大小为 k 的小根堆,堆中存储「(频率,元素)」,按频率升序排序:
-
若堆大小 <
k:直接将当前元素入堆。 -
若堆大小 ==
k:比较当前元素频率与堆顶元素频率:-
若当前频率 > 堆顶频率:弹出堆顶,将当前元素入堆。
-
否则:跳过。
-
提取结果 :堆中剩余的 k 个元素即为频率前 k 高的元素。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 1. 统计频率
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 2. 创建桶:索引为频率,值为对应元素列表
int n = nums.length;
List<Integer>[] bucket = new List[n + 1];
for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
int num = entry.getKey();
int freq = entry.getValue();
if (bucket[freq] == null) {
bucket[freq] = new ArrayList<>();
}
bucket[freq].add(num);
}
// 3. 逆序遍历桶,收集前 k 个高频元素
int[] res = new int[k];
int idx = 0;
for (int i = n; i >= 0 && idx < k; i--) {
if (bucket[i] != null) {
for (int num : bucket[i]) {
res[idx++] = num;
if (idx == k) break;
}
}
}
return res;
}
}
解题思路2:桶排序法(O (n) 时间)
利用「频率范围有限」的特性(最大频率 ≤ 数组长度 n),用桶数组存储不同频率的元素:
-
统计频率:同小根堆法。
-
创建桶 :
List<Integer>[] bucket = new List[n+1],bucket[freq]存储所有频率为freq的元素。 -
逆序遍历桶 :从最高频率开始遍历,收集元素直到数量达到
k。
295. 数据流的中位数

class MedianFinder {
// 大顶堆:存储前半部分较小的元素(堆顶是前半部分最大值)
private PriorityQueue<Integer> maxHeap;
// 小顶堆:存储后半部分较大的元素(堆顶是后半部分最小值)
private PriorityQueue<Integer> minHeap;
// 初始化
public MedianFinder() {
maxHeap = new PriorityQueue<>(Collections.reverseOrder());
minHeap = new PriorityQueue<>();
}
// 添加元素
public void addNum(int num) {
// 先插入到对应的堆
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.offer(num);
} else {
minHeap.offer(num);
}
// 调整堆大小,保证平衡
// 情况1:大顶堆比小顶堆多超过1个元素
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.offer(maxHeap.poll());
}
// 情况2:小顶堆比大顶堆元素多
else if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
// 查找中位数
public double findMedian() {
// 奇数个元素:中位数是大顶堆堆顶
if (maxHeap.size() > minHeap.size()) {
return maxHeap.peek();
}
// 偶数个元素:中位数是两堆顶的平均值
else {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
}
}
}
解题思路1:双堆法(大顶堆 + 小顶堆)
维护两个堆:
-
大顶堆(maxHeap) :存储前半部分较小的元素,堆顶是前半部分的最大值。
-
小顶堆(minHeap) :存储后半部分较大的元素,堆顶是后半部分的最小值。
平衡规则:
-
元素总数为奇数时:大顶堆比小顶堆多 1 个元素(中位数 = 大顶堆堆顶)。
-
元素总数为偶数时:两个堆大小相等(中位数 = 两堆顶的平均值)。
插入规则:
-
新元素
num先与大顶堆堆顶比较:-
若
num ≤ 大顶堆堆顶:插入大顶堆; -
否则:插入小顶堆。
-
-
调整堆大小,保证平衡规则:
-
若大顶堆大小 > 小顶堆大小 + 1:将大顶堆堆顶弹出,插入小顶堆;
-
若小顶堆大小 > 大顶堆大小:将小顶堆堆顶弹出,插入大顶堆。
-
这里灵大佬的方法实在妙哉,我再次搬用学习了
class MedianFinder {
private final PriorityQueue<Integer> left = new PriorityQueue<>((a, b) -> b - a); // 最大堆
private final PriorityQueue<Integer> right = new PriorityQueue<>(); // 最小堆
public void addNum(int num) {
if (left.size() == right.size()) {
right.offer(num);
left.offer(right.poll());
} else {
left.offer(num);
right.offer(left.poll());
}
}
public double findMedian() {
if (left.size() > right.size()) {
return left.peek();
}
return (left.peek() + right.peek()) / 2.0;
}
}
作者:灵茶山艾府
链接:https://leetcode.cn/problems/find-median-from-data-stream/solutions/3015873/ru-he-zi-ran-yin-ru-da-xiao-dui-jian-ji-4v22k/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
解题思路2:双堆法的极简优化版
-
双堆分工:
-
大顶堆
left:存储数据流中前半部分较小的元素,堆顶是前半部分最大值; -
小顶堆
right:存储数据流中后半部分较大的元素,堆顶是后半部分最小值。
-
-
插入规则(隐式平衡):
-
若两堆大小相等(偶数个元素):先将新元素插入
right,再弹出right堆顶插入left,最终left比right多 1 个元素(奇数状态); -
若
left更大(奇数个元素):先将新元素插入left,再弹出left堆顶插入right,最终两堆大小相等(偶数状态)。
-
-
中位数 查询:
-
奇数个元素:直接返回
left堆顶(前半部分最大值); -
偶数个元素:返回
left和right堆顶的平均值。
-
核心要点
-
无需判断新元素该插入哪个堆,通过「先插另一堆、再弹回目标堆」的中转操作,天然保证堆的平衡;
-
插入操作时间复杂度 O (log n),查询中位数 O (1),适配动态数据流场景。
