数组中的第K个最大元素
字数 1914 2025-10-27 22:21:08

数组中的第K个最大元素

题目描述:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

解题过程:

  1. 问题理解与关键点分析

    • 目标:在无需完全排序整个数组的情况下,找到特定顺序(第k大)的元素。
    • 核心挑战:如何高效地定位这个元素,避免进行 O(n log n) 的完整排序。
    • 关键思路:我们可以利用快速排序的分区思想(Partition)或使用堆(Heap)数据结构来更高效地解决这个问题。
  2. 方法一:基于快速选择(QuickSelect)算法

    • 核心思想:快速选择算法是快速排序的变种。在快速排序中,我们每次选择一个基准值(pivot),通过分区操作将数组分为两部分:左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于基准值。此时,基准值在数组中的最终位置就确定了。
      • 如果这个最终位置恰好是我们要找的第 k 个最大元素应该所在的位置(即索引 n - k,其中 n 是数组长度),那么基准值就是答案。
      • 如果这个位置在目标位置右边,说明目标元素在左半部分,我们只需要递归地在左半部分继续寻找。
      • 如果这个位置在目标位置左边,说明目标元素在右半部分,我们只需要递归地在右半部分继续寻找。
    • 步骤分解
      1. 随机化基准值:为了避免在极端输入下(如数组已排序)算法退化为 O(n²) 的时间复杂度,我们在分区前随机选择一个元素作为基准值,并将其与当前区间末尾的元素交换。
      2. 分区操作
        • 设定两个指针,i 初始化为区间左边界 leftj 也从 left 开始遍历到 right - 1(因为基准值在 right)。
        • 遍历过程中,如果 nums[j] 小于等于基准值,就将其与 nums[i] 交换,并让 i 右移一位。这样操作可以保证 i 左边的元素都小于等于基准值。
        • 遍历完成后,将基准值(目前在 right 位置)与 nums[i] 交换。此时,基准值就位于索引 i 处,且左边元素都小于等于它,右边元素都大于它。
      3. 判断与递归
        • 计算当前基准值的索引 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)(递归调用栈)。
  3. 方法二:基于最小堆(Min-Heap)

    • 核心思想:维护一个大小为 k 的最小堆。堆顶是堆中最小的元素,也就是当前已遍历元素中第 k 大的"候选者"(因为堆里存的是目前最大的 k 个数)。
    • 步骤分解
      1. 建堆:使用前 k 个元素构建一个大小为 k 的最小堆。
      2. 遍历与更新:从第 k+1 个元素开始遍历数组。
        • 将当前元素 num 与堆顶元素比较。
        • 如果 num 大于堆顶元素,说明当前这个数有资格进入“最大的k个数”的集合,而堆顶元素应该被淘汰。于是,我们弹出堆顶元素(当前最小的),并将 num 加入堆中。
        • 如果 num 小于等于堆顶元素,则忽略它,因为它不可能进入前 k 大。
      3. 获取结果:遍历完整个数组后,堆中保存的就是整个数组最大的 k 个数。由于这是最小堆,堆顶元素就是这个堆里最小的数,也就是整个数组中第 k 大的数。
    • 时间复杂度:建堆 O(k),后面每个元素最多进行一次堆调整 O(log k),总时间为 O(n log k)。空间复杂度 O(k)(用于存储堆)。
  4. 方法比较与总结

    • 快速选择:平均性能更优(O(n)),是理论上的最佳选择,尤其当数据量非常大且对时间要求苛刻时。但实现稍复杂,且存在理论上的最坏情况。
    • 最小堆:实现简单直观,时间复杂度 O(n log k) 稳定可靠,特别当 k 远小于 n 时(例如找前10大的数),效率非常高。是实践中常用且稳健的方法。

对于这个问题,掌握这两种核心思想及其实现细节至关重要。

数组中的第K个最大元素 题目描述:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 解题过程: 问题理解与关键点分析 目标:在无需完全排序整个数组的情况下,找到特定顺序(第k大)的元素。 核心挑战:如何高效地定位这个元素,避免进行 O(n log n) 的完整排序。 关键思路:我们可以利用快速排序的分区思想(Partition)或使用堆(Heap)数据结构来更高效地解决这个问题。 方法一:基于快速选择(QuickSelect)算法 核心思想 :快速选择算法是快速排序的变种。在快速排序中,我们每次选择一个基准值(pivot),通过分区操作将数组分为两部分:左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于基准值。此时,基准值在数组中的最终位置就确定了。 如果这个最终位置恰好是我们要找的第 k 个最大元素应该所在的位置(即索引 n - k ,其中 n 是数组长度),那么基准值就是答案。 如果这个位置在目标位置右边,说明目标元素在左半部分,我们只需要递归地在左半部分继续寻找。 如果这个位置在目标位置左边,说明目标元素在右半部分,我们只需要递归地在右半部分继续寻找。 步骤分解 : 随机化基准值 :为了避免在极端输入下(如数组已排序)算法退化为 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)(递归调用栈)。 方法二:基于最小堆(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大的数),效率非常高。是实践中常用且稳健的方法。 对于这个问题,掌握这两种核心思想及其实现细节至关重要。