关注
文末的名片达文汐
,回复关键词“力扣源码”,即可获取完整源码!!详见:源码和核心代码的区别
题目详情
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4],
k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6],
k = 4
输出: 4
提示:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
解题思路
要找出数组中第 k
个最大的元素,且时间复杂度为 O(n)
,可以使用快速选择算法(QuickSelect)。该算法基于快速排序的分区思想,但只递归处理包含目标元素的子数组,从而将平均时间复杂度优化至 O(n)
。
核心步骤:
- 问题转换:第
k
个最大元素等价于升序排序后第nums.length - k
小的元素。 - 分区操作:
- 随机选择一个枢轴(pivot)元素,避免最坏时间复杂度。
- 将数组划分为三部分:小于枢轴、等于枢轴、大于枢轴。
- 通过双指针(Lomuto 分区方案)实现分区:
- 指针
i
维护最后一个小于等于枢轴的元素位置。 - 遍历数组,将小于等于枢轴的元素交换到左侧。
- 指针
- 目标定位:
- 分区后,枢轴位于最终位置
pivotIndex
。 - 若
pivotIndex == target
,直接返回结果。 - 若
pivotIndex < target
,在右子数组继续查找。 - 若
pivotIndex > target
,在左子数组继续查找。
- 分区后,枢轴位于最终位置
- 迭代优化:使用迭代代替递归,减少栈空间占用,优化内存消耗。
代码实现(Java版)
class Solution {
public int findKthLargest(int[] nums, int k) {
int target = nums.length - k; // 目标位置(升序排序后第 target 小的元素)
int left = 0;
int right = nums.length - 1;
Random rand = new Random();
while (left <= right) {
// 随机选择枢轴并交换到末尾
int randIndex = left + rand.nextInt(right - left + 1);
swap(nums, randIndex, right);
int pivot = nums[right];
int i = left - 1; // i 指向最后一个小于等于枢轴的元素
// 分区操作:将小于等于枢轴的元素移到左侧
for (int j = left; j < right; j++) {
if (nums[j] <= pivot) {
i++;
swap(nums, i, j);
}
}
// 将枢轴放到正确位置
int pivotIndex = i + 1;
swap(nums, pivotIndex, right);
// 根据枢轴位置调整搜索区间
if (pivotIndex == target) {
return nums[pivotIndex];
} else if (pivotIndex < target) {
left = pivotIndex + 1; // 目标在右子数组
} else {
right = pivotIndex - 1; // 目标在左子数组
}
}
return -1; // 理论上不会执行到此处
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
代码说明
- 问题转换:
target = nums.length - k
将第k
大元素转换为升序排序后第target
小的元素(0-indexed)。
- 随机枢轴:
- 每次迭代随机选择枢轴并交换到数组末尾,避免最坏时间复杂度。
- Lomuto 分区:
- 指针
i
维护左侧小于等于枢轴的子数组边界。 - 遍历指针
j
扫描数组,将小于等于枢轴的元素交换到i
的左侧。
- 指针
- 分区后处理:
- 将枢轴交换到
i + 1
位置,此时左侧元素均≤
枢轴,右侧元素均≥
枢轴。
- 将枢轴交换到
- 迭代搜索:
- 根据
pivotIndex
与target
的关系,调整搜索区间[left, right]
,避免递归栈空间。
- 根据