题目
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
数据范围
1 <= k <= nums.length <= 105
-104 <= nums[i] <= 104
测试用例
示例1
java
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例2
java
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
题解1(官解1 ,时间On,空间Ologn)
java
class Solution {
// 快速选择核心函数:在 [l, r] 的区间内,寻找最终排好序后索引为 k 的元素
int quickselect(int[] nums, int l, int r, int k) {
// 递归终止条件:如果区间内只有一个元素,说明找到了目标,直接返回
if (l == r) return nums[k];
// 选取基准值(Pivot),这里简单地取了区间最左侧的元素
int x = nums[l];
// 初始化双指针,i 在区间左侧的前一个位置,j 在区间右侧的后一个位置
// 这是为了配合后面的 do-while 循环
int i = l - 1, j = r + 1;
// 开始 Hoare 分区操作
while (i < j) {
// i 指针向右移动,直到找到一个大于等于基准值 x 的数
do i++; while (nums[i] < x);
// j 指针向左移动,直到找到一个小于等于基准值 x 的数
do j--; while (nums[j] > x);
// 如果两个指针还没有相遇,说明 nums[i] 放错了左边,nums[j] 放错了右边
// 交换它们,让小于基准值的去左边,大于基准值的去右边
if (i < j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
// 分区结束后,整个区间被分成了两部分:[l, j] 和 [j+1, r]
// 并且满足:[l, j] 中的所有元素 <= [j+1, r] 中的所有元素
// 判断目标索引 k 落在哪一半
if (k <= j) {
// 如果 k 在左半部分,递归左半区
return quickselect(nums, l, j, k);
} else {
// 如果 k 在右半部分,递归右半区
return quickselect(nums, j + 1, r, k);
}
}
public int findKthLargest(int[] _nums, int k) {
int n = _nums.length;
// 问题转换:求"第 k 大"的元素,等价于求升序排列后的"第 n-k 小"的元素
// 它的索引也就是 n-k。例如长度为 5 的数组,第 1 大的元素,排好序后索引是 5-1=4。
return quickselect(_nums, 0, n - 1, n - k);
}
}
题解2(官解2,堆排序)
java
class Solution {
// 主函数:寻找数组中的第 k 个最大元素
public int findKthLargest(int[] nums, int k) {
int heapSize = nums.length;
// 步骤 1:将整个数组构建成一个大根堆
buildMaxHeap(nums, heapSize);
// 步骤 2:执行 k-1 次删除堆顶元素的操作
// i 表示当前要和堆顶交换的末尾位置
for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {
// 将当前堆顶(最大值)交换到数组末尾,相当于把它从堆中"移除"并存起来
swap(nums, 0, i);
// 堆的大小减 1,刚才换到末尾的最大值不再参与后续的堆调整
--heapSize;
// 因为新的堆顶元素可能是个较小的值,破坏了堆的性质,所以需要从堆顶(索引 0)向下重新调整堆
maxHeapify(nums, 0, heapSize);
}
// 经过 k-1 次交换和调整后,此时的堆顶 nums[0] 就是第 k 大的元素
return nums[0];
}
// 建堆函数:从最后一个非叶子节点开始,逆序向上依次调整
public void buildMaxHeap(int[] a, int heapSize) {
// heapSize / 2 - 1 是完全二叉树中最后一个非叶子节点的索引
for (int i = heapSize / 2 - 1; i >= 0; --i) {
maxHeapify(a, i, heapSize);
}
}
// 堆化函数(核心):负责维护大根堆的性质(父节点的值 >= 子节点的值)
// 作用是将索引 i 处的元素"下沉"到合适的位置
public void maxHeapify(int[] a, int i, int heapSize) {
// 在数组表示的完全二叉树中,节点 i 的左孩子索引是 2*i + 1,右孩子是 2*i + 2
int l = i * 2 + 1, r = i * 2 + 2;
// largest 记录当前节点及其左右孩子中,值最大的那个节点的索引
int largest = i;
// 如果左孩子在堆的范围内,且左孩子的值大于当前最大值,更新 largest
if (l < heapSize && a[l] > a[largest]) {
largest = l;
}
// 如果右孩子在堆的范围内,且右孩子的值大于当前最大值,更新 largest
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
// 如果最大值不是当前的父节点 i,说明破坏了堆的性质
if (largest != i) {
// 将父节点和最大的孩子交换
swap(a, i, largest);
// 交换后,原来的父节点落到了 largest 的位置,可能会继续破坏下一层的堆性质
// 所以对 largest 位置递归调用 maxHeapify 继续向下调整(下沉)
maxHeapify(a, largest, heapSize);
}
}
// 辅助函数:交换数组中的两个元素
public void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
思路
这道题如果严格要求时间复杂度on的话,还是比较考察积累的,我们都知道快速排序是onlogn的时间复杂度,但快速选择的时间复杂度on,

理解这个之后,再把这个方法积累起来就可以了,因为实现并不难。
第二个方法堆排序时间复杂度是onlogn,但有些面试官要考察堆排序(手写),所以这里大家一定要学会其对应写法,并不难,积累即可。