LeetCode 热题 100 之 215. 数组中的第K个最大元素 347. 前 K 个高频元素 295. 数据流的中位数

本次三道题*堆*

  1. 数组中的第K个最大元素

  2. 前 K 个高频元素

  3. 数据流的中位数

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)

快速选择基于快速排序的「分区」思想:

  1. 随机选择一个 pivot,将数组划分为:[小于pivot] + [pivot] + [大于pivot]

  2. 比较 pivot 的最终位置 pos 与目标索引 target = nums.length - k

    1. pos == target:pivot 即为答案;

    2. pos > target:答案在左半区间 [l, pos-1],递归处理;

    3. pos < target:答案在右半区间 [pos+1, r],递归处理。

  3. 随机选择 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),用桶数组存储不同频率的元素:

  1. 统计频率:同小根堆法。

  2. 创建桶List<Integer>[] bucket = new List[n+1]bucket[freq] 存储所有频率为 freq 的元素。

  3. 逆序遍历桶 :从最高频率开始遍历,收集元素直到数量达到 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:双堆法(大顶堆 + 小顶堆)

维护两个堆:

  1. 大顶堆(maxHeap) :存储前半部分较小的元素,堆顶是前半部分的最大值。

  2. 小顶堆(minHeap) :存储后半部分较大的元素,堆顶是后半部分的最小值。

平衡规则

  • 元素总数为奇数时:大顶堆比小顶堆多 1 个元素(中位数 = 大顶堆堆顶)。

  • 元素总数为偶数时:两个堆大小相等(中位数 = 两堆顶的平均值)。

插入规则

  1. 新元素 num 先与大顶堆堆顶比较:

    1. num ≤ 大顶堆堆顶:插入大顶堆;

    2. 否则:插入小顶堆。

  2. 调整堆大小,保证平衡规则:

    1. 若大顶堆大小 > 小顶堆大小 + 1:将大顶堆堆顶弹出,插入小顶堆;

    2. 若小顶堆大小 > 大顶堆大小:将小顶堆堆顶弹出,插入大顶堆。

这里灵大佬的方法实在妙哉,我再次搬用学习了

复制代码
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:双堆法的极简优化版
  1. 双堆分工

    1. 大顶堆 left:存储数据流中前半部分较小的元素,堆顶是前半部分最大值;

    2. 小顶堆 right:存储数据流中后半部分较大的元素,堆顶是后半部分最小值。

  2. 插入规则(隐式平衡)

    1. 若两堆大小相等(偶数个元素):先将新元素插入 right,再弹出 right 堆顶插入 left,最终 leftright 多 1 个元素(奇数状态);

    2. left 更大(奇数个元素):先将新元素插入 left,再弹出 left 堆顶插入 right,最终两堆大小相等(偶数状态)。

  3. 中位数 查询

    1. 奇数个元素:直接返回 left 堆顶(前半部分最大值);

    2. 偶数个元素:返回 leftright 堆顶的平均值。

核心要点

  • 无需判断新元素该插入哪个堆,通过「先插另一堆、再弹回目标堆」的中转操作,天然保证堆的平衡;

  • 插入操作时间复杂度 O (log n),查询中位数 O (1),适配动态数据流场景。

相关推荐
凤年徐2 小时前
优选算法——滑动窗口
c++·算法
DDzqss2 小时前
3.14打卡day35
算法
WHS-_-20222 小时前
mCore: Achieving Sub-millisecond Scheduling for 5G MU-MIMO Systems
java·算法·5g
浅念-2 小时前
C++11 核心知识点整理
开发语言·数据结构·c++·笔记·算法
炽烈小老头2 小时前
【 每天学习一点算法 2026/03/14】二叉搜索树中第K小的元素
学习·算法
一条大祥脚2 小时前
WQS二分(Alien Trick)
算法
xiaoye-duck2 小时前
《算法题讲解指南:递归,搜索与回溯算法--二叉树中的深搜》--6.计算布尔二叉树的值,7.求根节点到叶节点数字之和
c++·算法·深度优先·递归
greatofdream2 小时前
VIP和普通用户排队
算法
abant22 小时前
leetcode 84 单调栈
算法·leetcode·职场和发展