数组中的第K个最大元素
字数 1914 2025-10-27 22:21:08
数组中的第K个最大元素
题目描述:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
解题过程:
-
问题理解与关键点分析
- 目标:在无需完全排序整个数组的情况下,找到特定顺序(第k大)的元素。
- 核心挑战:如何高效地定位这个元素,避免进行 O(n log n) 的完整排序。
- 关键思路:我们可以利用快速排序的分区思想(Partition)或使用堆(Heap)数据结构来更高效地解决这个问题。
-
方法一:基于快速选择(QuickSelect)算法
- 核心思想:快速选择算法是快速排序的变种。在快速排序中,我们每次选择一个基准值(pivot),通过分区操作将数组分为两部分:左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于基准值。此时,基准值在数组中的最终位置就确定了。
- 如果这个最终位置恰好是我们要找的第 k 个最大元素应该所在的位置(即索引
n - k,其中 n 是数组长度),那么基准值就是答案。 - 如果这个位置在目标位置右边,说明目标元素在左半部分,我们只需要递归地在左半部分继续寻找。
- 如果这个位置在目标位置左边,说明目标元素在右半部分,我们只需要递归地在右半部分继续寻找。
- 如果这个最终位置恰好是我们要找的第 k 个最大元素应该所在的位置(即索引
- 步骤分解:
- 随机化基准值:为了避免在极端输入下(如数组已排序)算法退化为 O(n²) 的时间复杂度,我们在分区前随机选择一个元素作为基准值,并将其与当前区间末尾的元素交换。
- 分区操作:
- 设定两个指针,
i初始化为区间左边界left,j也从left开始遍历到right - 1(因为基准值在right)。 - 遍历过程中,如果
nums[j]小于等于基准值,就将其与nums[i]交换,并让i右移一位。这样操作可以保证i左边的元素都小于等于基准值。 - 遍历完成后,将基准值(目前在
right位置)与nums[i]交换。此时,基准值就位于索引i处,且左边元素都小于等于它,右边元素都大于它。
- 设定两个指针,
- 判断与递归:
- 计算当前基准值的索引
pivot_index对应的顺序位置。我们要找的是第 k 个最大的,也就是升序排序后索引为n - k的元素。 - 如果
pivot_index == n - k,那么nums[pivot_index]就是答案。 - 如果
pivot_index < n - k,说明目标在右半部分,递归调用快速选择函数,区间为[pivot_index + 1, right],寻找的 k 值不变。 - 如果
pivot_index > n - k,说明目标在左半部分,递归调用快速选择函数,区间为[left, pivot_index - 1],寻找的 k 值不变(因为目标仍在整个数组的第 k 大位置)。
- 计算当前基准值的索引
- 时间复杂度:平均情况 O(n),最坏情况 O(n²)(通过随机化基准值可极大避免),空间复杂度 O(log n)(递归调用栈)。
- 核心思想:快速选择算法是快速排序的变种。在快速排序中,我们每次选择一个基准值(pivot),通过分区操作将数组分为两部分:左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于基准值。此时,基准值在数组中的最终位置就确定了。
-
方法二:基于最小堆(Min-Heap)
- 核心思想:维护一个大小为 k 的最小堆。堆顶是堆中最小的元素,也就是当前已遍历元素中第 k 大的"候选者"(因为堆里存的是目前最大的 k 个数)。
- 步骤分解:
- 建堆:使用前 k 个元素构建一个大小为 k 的最小堆。
- 遍历与更新:从第 k+1 个元素开始遍历数组。
- 将当前元素
num与堆顶元素比较。 - 如果
num大于堆顶元素,说明当前这个数有资格进入“最大的k个数”的集合,而堆顶元素应该被淘汰。于是,我们弹出堆顶元素(当前最小的),并将num加入堆中。 - 如果
num小于等于堆顶元素,则忽略它,因为它不可能进入前 k 大。
- 将当前元素
- 获取结果:遍历完整个数组后,堆中保存的就是整个数组最大的 k 个数。由于这是最小堆,堆顶元素就是这个堆里最小的数,也就是整个数组中第 k 大的数。
- 时间复杂度:建堆 O(k),后面每个元素最多进行一次堆调整 O(log k),总时间为 O(n log k)。空间复杂度 O(k)(用于存储堆)。
-
方法比较与总结
- 快速选择:平均性能更优(O(n)),是理论上的最佳选择,尤其当数据量非常大且对时间要求苛刻时。但实现稍复杂,且存在理论上的最坏情况。
- 最小堆:实现简单直观,时间复杂度 O(n log k) 稳定可靠,特别当 k 远小于 n 时(例如找前10大的数),效率非常高。是实践中常用且稳健的方法。
对于这个问题,掌握这两种核心思想及其实现细节至关重要。