(LeetCode-Hot100)215. 数组中的第K个最大元素

问题简介

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

复制代码
题解github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

题目描述

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

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

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例说明

示例 1:

复制代码
输入: nums = [3,2,1,5,6,4], k = 2
输出: 5
解释: 排序后数组为 [6,5,4,3,2,1],第2个最大元素是5

示例 2:

复制代码
输入: nums = [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
解释: 排序后数组为 [6,5,5,4,3,3,2,2,1],第4个最大元素是4

解题思路

方法一:快速选择算法(QuickSelect)✅

💡 核心思想:基于快速排序的分区思想,但只递归处理包含目标元素的那一部分。

步骤详解:

  1. 选择一个基准元素(pivot)
  2. 将数组分为两部分:大于等于pivot的在左边,小于pivot的在右边
  3. 根据pivot的位置与k的关系决定下一步:
    • 如果pivot位置正好是k-1,返回pivot
    • 如果pivot位置大于k-1,在左半部分继续查找
    • 如果pivot位置小于k-1,在右半部分继续查找

方法二:堆排序(最小堆)✅

💡 核心思想:维护一个大小为k的最小堆,堆顶就是第k大的元素。

步骤详解:

  1. 创建一个容量为k的最小堆
  2. 遍历数组中的每个元素:
    • 如果堆的大小小于k,直接加入
    • 如果当前元素大于堆顶,弹出堆顶并加入当前元素
  3. 最终堆顶就是第k大的元素

方法三:排序后直接取值 ❌

💡 核心思想:直接对数组排序,然后取第k个最大元素。

步骤详解:

  1. 对数组进行降序排序
  2. 返回索引为k-1的元素

⚠️ 注意:虽然这种方法简单,但时间复杂度为O(n log n),不满足题目要求的O(n)时间复杂度。

代码实现

java 复制代码
// 方法一:快速选择算法
class Solution {
    public int findKthLargest(int[] nums, int k) {
        return quickSelect(nums, 0, nums.length - 1, k - 1);
    }
    
    private int quickSelect(int[] nums, int left, int right, int k) {
        if (left == right) {
            return nums[left];
        }
        
        // 随机选择pivot以避免最坏情况
        Random random = new Random();
        int pivotIndex = left + random.nextInt(right - left + 1);
        pivotIndex = partition(nums, left, right, pivotIndex);
        
        if (k == pivotIndex) {
            return nums[k];
        } else if (k < pivotIndex) {
            return quickSelect(nums, left, pivotIndex - 1, k);
        } else {
            return quickSelect(nums, pivotIndex + 1, right, k);
        }
    }
    
    private int partition(int[] nums, int left, int right, int pivotIndex) {
        int pivotValue = nums[pivotIndex];
        // 将pivot移到末尾
        swap(nums, pivotIndex, right);
        int storeIndex = left;
        
        // 将所有大于pivot的元素移到左边
        for (int i = left; i < right; i++) {
            if (nums[i] > pivotValue) {
                swap(nums, storeIndex, i);
                storeIndex++;
            }
        }
        
        // 将pivot放到正确位置
        swap(nums, storeIndex, right);
        return storeIndex;
    }
    
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

// 方法二:最小堆
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();
    }
}
go 复制代码
// 方法一:快速选择算法
import (
    "math/rand"
    "time"
)

func findKthLargest(nums []int, k int) int {
    rand.Seed(time.Now().UnixNano())
    return quickSelect(nums, 0, len(nums)-1, k-1)
}

func quickSelect(nums []int, left, right, k int) int {
    if left == right {
        return nums[left]
    }
    
    pivotIndex := left + rand.Intn(right-left+1)
    pivotIndex = partition(nums, left, right, pivotIndex)
    
    if k == pivotIndex {
        return nums[k]
    } else if k < pivotIndex {
        return quickSelect(nums, left, pivotIndex-1, k)
    } else {
        return quickSelect(nums, pivotIndex+1, right, k)
    }
}

func partition(nums []int, left, right, pivotIndex int) int {
    pivotValue := nums[pivotIndex]
    // 将pivot移到末尾
    nums[pivotIndex], nums[right] = nums[right], nums[pivotIndex]
    storeIndex := left
    
    // 将所有大于pivot的元素移到左边
    for i := left; i < right; i++ {
        if nums[i] > pivotValue {
            nums[storeIndex], nums[i] = nums[i], nums[storeIndex]
            storeIndex++
        }
    }
    
    // 将pivot放到正确位置
    nums[storeIndex], nums[right] = nums[right], nums[storeIndex]
    return storeIndex
}

// 方法二:最小堆
import "container/heap"

type MinHeap []int

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

func findKthLargest(nums []int, k int) int {
    minHeap := &MinHeap{}
    heap.Init(minHeap)
    
    for _, num := range nums {
        if minHeap.Len() < k {
            heap.Push(minHeap, num)
        } else if num > (*minHeap)[0] {
            heap.Pop(minHeap)
            heap.Push(minHeap, num)
        }
    }
    
    return (*minHeap)[0]
}

示例演示

📌 以示例1为例:nums = [3,2,1,5,6,4], k = 2

快速选择过程:

  1. 初始数组:[3,2,1,5,6,4],寻找第2大元素(索引1)
  2. 假设选择pivot=4,分区后:[6,5,4,3,2,1],pivot位置=2
  3. 由于k-1=1 < 2,所以在左半部分[6,5]中继续查找
  4. 在[6,5]中选择pivot=5,分区后:[6,5],pivot位置=1
  5. k-1=1 == pivot位置,返回5

最小堆过程:

  1. 初始化空堆
  2. 依次加入:3→[3], 2→[2,3], 1→[1,2,3](但堆大小限制为2,实际为[2,3])
  3. 加入5:5>2,弹出2,加入5→[3,5]
  4. 加入6:6>3,弹出3,加入6→[5,6]
  5. 加入4:4<5,不加入
  6. 最终堆顶为5

答案有效性证明

快速选择算法正确性:

  • 基于快速排序的分区性质:每次分区后,pivot元素都在其最终排序位置
  • 通过比较pivot位置与目标位置k-1的关系,可以确定目标元素在哪个子区间
  • 递归缩小搜索范围,最终必然找到第k大元素

最小堆算法正确性:

  • 维护大小为k的最小堆,保证堆中始终包含当前遍历过的k个最大元素
  • 堆顶是这k个元素中的最小值,也就是第k大的元素
  • 遍历完整个数组后,堆中包含全局最大的k个元素,堆顶即为答案

复杂度分析

方法 时间复杂度 空间复杂度 是否满足O(n)要求
快速选择 平均O(n),最坏O(n²) O(1) ✅ 平均情况下满足
最小堆 O(n log k) O(k) ❌ 不严格满足O(n)
排序 O(n log n) O(1)或O(n) ❌ 不满足

💡 关键点说明:

  • 快速选择的平均时间复杂度为O(n),因为每次期望能排除一半的元素
  • 通过随机选择pivot,可以避免最坏情况(已排序数组)的发生
  • 当k较小时,堆方法的实际性能可能更好,因为log k很小

问题总结

📌 核心要点:

  1. 快速选择是最佳解法:平均时间复杂度O(n),空间复杂度O(1)
  2. 堆方法实用性强:虽然理论复杂度不是O(n),但在k较小时表现优秀
  3. 随机化很重要:避免快速选择在特殊输入下的最坏情况

💡 面试建议:

  • 首先提出快速选择算法,展示对分治思想的理解
  • 讨论堆方法作为备选方案,体现多角度思考能力
  • 强调随机化的重要性,展示对算法鲁棒性的考虑

适用场景:

  • 快速选择:需要严格O(n)时间复杂度的场景
  • 最小堆:k相对较小,或者需要在线处理数据流的场景
相关推荐
晔子yy2 小时前
ReAct范式全流程详解
java·ai·react
渣瓦攻城狮2 小时前
互联网大厂Java面试实战:核心技术与场景分析
java·大数据·redis·spring·微服务·面试·技术分享
We་ct2 小时前
LeetCode 112. 路径总和:两种解法详解
前端·算法·leetcode·typescript
敲代码的哈吉蜂2 小时前
haproxy的算法——静态算法
linux·运维·服务器·算法
艾醒2 小时前
打破信息差——2月21日AI全域热点全复盘
后端·算法
tankeven2 小时前
自创小算法00:数据分组
c++·算法
yzx9910132 小时前
蓝桥杯智能体开发:从入门到实战经验分享
职场和发展·蓝桥杯
wuqingshun3141592 小时前
说一下JVM内存结构
java·开发语言·jvm
样例过了就是过了2 小时前
LeetCode热题100 矩阵置零
算法·leetcode·矩阵