LeetCode215: 数组中的第K个最大元素 —— 从快速选择到堆排

题目描述

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

text

复制代码
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

text

复制代码
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 10^5

  • -10^4 <= nums[i] <= 10^4

解法一:快速选择(Quick Select)

思想

快速选择是快速排序的变种。核心步骤:

  1. 随机选择一个基准元素 pivot

  2. 将数组划分为三部分:大于 pivot等于 pivot小于 pivot

  3. 根据各部分的大小与 k 的关系,确定第 k 大的元素在哪一部分,并递归处理该部分。

由于每次只需处理一侧,平均时间复杂度为 O(n),且可原地分区优化空间。

代码实现(Java,非原地分区,易于理解)

java 复制代码
public class Solution {
    private int quickSelect(List<Integer> nums, int k) {
        // 随机选择基准数
        Random rand = new Random();
        int pivot = nums.get(rand.nextInt(nums.size()));
        // 将大于、小于、等于 pivot 的元素划分至 big, small, equal 中
        List<Integer> big = new ArrayList<>();
        List<Integer> equal = new ArrayList<>();
        List<Integer> small = new ArrayList<>();
        for (int num : nums) {
            if (num > pivot)
                big.add(num);
            else if (num < pivot)
                small.add(num);
            else
                equal.add(num);
        }
        // 第 k 大元素在 big 中,递归划分
        if (k <= big.size())
            return quickSelect(big, k);
        // 第 k 大元素在 small 中,递归划分
        if (big.size() + equal.size() < k)
            return quickSelect(small, k - (big.size()+equal.size()));
        // 第 k 大元素在 equal 中,直接返回 pivot
        return pivot;
    }
    
    public int findKthLargest(int[] nums, int k) {
        List<Integer> numList = new ArrayList<>();
        for (int num : nums) {
            numList.add(num);
        }
        return quickSelect(numList, k);
    }
}

原地分区优化版本(推荐)

java 复制代码
public class Solution {
    private Random rand = new Random();
    
    private int quickSelect(int[] nums, int left, int right, int k) {
        int pivotIdx = left + rand.nextInt(right - left + 1);
        int pivot = nums[pivotIdx];
        
        // 三路分区 (荷兰国旗问题)
        int lt = left, i = left, gt = right;
        while (i <= gt) {
            if (nums[i] > pivot) {
                swap(nums, lt++, i++);
            } else if (nums[i] < pivot) {
                swap(nums, i, gt--);
            } else {
                i++;
            }
        }
        // 此时: [left, lt-1] > pivot, [lt, gt] == pivot, [gt+1, right] < pivot
        
        if (k <= lt - left) {
            return quickSelect(nums, left, lt - 1, k);
        } else if (k <= gt - left + 1) {
            return pivot;
        } else {
            return quickSelect(nums, gt + 1, right, k - (gt - left + 1));
        }
    }
    
    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
    
    public int findKthLargest(int[] nums, int k) {
        return quickSelect(nums, 0, nums.length - 1, k);
    }
}

复杂度分析

  • 时间复杂度:平均 O(n),最坏 O(n²)(但随机化后概率极低)

  • 空间复杂度:递归栈深度 O(log n),原地分区额外 O(1)


解法二:最小堆(大小为 k)

思想

维护一个大小为 k 的最小堆,堆顶始终是当前扫描过的元素中第 k 大的候选值。

  • 遍历数组,若堆大小 < k,直接入堆。

  • 否则,若当前元素 > 堆顶,则弹出堆顶并压入当前元素(说明当前元素更有资格成为第 k 大)。

  • 遍历结束后,堆顶就是整个数组的第 k 大元素。

代码实现

java 复制代码
import java.util.PriorityQueue;

public class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 最小堆
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int num : nums) {
            if (minHeap.size() < k) {
                minHeap.offer(num);
            } else if (num > minHeap.peek()) {
                minHeap.poll();
                minHeap.offer(num);
            }
        }
        return minHeap.peek();
    }
}

复杂度分析

  • 时间复杂度:O(n log k),每个元素最多一次入堆和出堆操作。

  • 空间复杂度:O(k),用于存储堆。

为什么不用最大堆?

最大堆需要将所有元素入堆(O(n) 建堆),然后弹出 k-1 次,总时间 O(n + k log n),当 k 接近 n 时性能不错,但空间 O(n) 较大。最小堆更节省内存,且对 k 较小的情况更优。

总结

  • 快速选择:分治思想,平均线性时间,需要处理分区细节,推荐掌握。

  • 最小堆:代码简单,适合 k 较小时,空间占用低。

  • 排序法:适合快速原型,生产环境如果 n 不大也可以用。

面试时,建议先说出快速选择原理,然后写出堆排序作为兜底实现。两者都能通过 LeetCode 测试,重点在于分析不同解法的优劣。

相关推荐
天若有情6732 小时前
用动态规划思路,一步一步实现响应式数据(从本质到落地)
算法·动态规划·代理模式
isNotNullX2 小时前
数据挖掘是什么?数据挖掘算法有哪些?
人工智能·算法·数据挖掘
剑挑星河月2 小时前
73.矩阵置零
数据结构·算法·leetcode·矩阵
MicroTech20252 小时前
MLGO微算法科技:面向大规模量子网络的通用纠错方案实现关键进展
网络·科技·算法
Rabitebla2 小时前
【C++】手撕日期类——运算符重载完全指南(含易错点+底层逻辑分析)
java·c语言·开发语言·数据结构·c++·算法·链表
做cv的小昊2 小时前
【TJU】应用统计学——第六周作业(3.3 两个正态总体参数的假设检验、3.4 非正态总体参数的假设检验、4.1 一元线性回归分析)
笔记·算法·数学建模·矩阵·回归·线性回归·学习方法
_深海凉_2 小时前
LeetCode热题100-单词拆分
算法·leetcode·职场和发展
wearegogog1232 小时前
基于蚁群算法的无人机三维航路规划(MATLAB实现)
算法·matlab·无人机
旖-旎2 小时前
递归(快速幂)(5)
c++·算法·力扣·递归