给定整数数组 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
解题思路:
题目要求我们找到「数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素」。「数组排序后的第 k 个最大的元素」换句话说:从右边往左边数第 k 个元素(从 1 开始),那么从左向右数是第几个呢,我们列出几个找找规律就好了。
一共 6 个元素,找第 2 大,下标是 4;
一共 6 个元素,找第 4 大,下标是 2。
因此升序排序以后,目标元素的下标是 N−k,这里 N 是输入数组的长度。
下面方法一使用的是降序排列,要找第K大的数,只需返回数组下标K-1 的值。
1.暴力解法
var findKthLargest = function(nums, k) {
// 降序排列
nums.sort((a,b) => b-a);
return nums[k-1];
};
复杂度分析:
时间复杂度:O(NlogN),这里 N 是数组的长度,算法的性能消耗主要在排序,JDK 默认使用快速排序,因此时间复杂度为 O(NlogN);
空间复杂度:O(logN),这里认为编程语言使用的排序方法是「快速排序」,空间复杂度为递归调用栈的高度,为 logN。
2.优先队列
优先队列的思路是很朴素的。由于找第 K 大元素,其实就是整个数组排序以后后半部分最小的那个元素。因此,我们可以维护一个有 K 个元素的最小堆:
如果当前堆不满,直接添加;
堆满的时候,如果新读到的数小于等于堆顶,肯定不是我们要找的元素,只有新遍历到的数大于堆顶的时候,才将堆顶拿出,然后放入新读到的数,进而让堆自己去调整内部结构。
说明:这里最合适的操作其实是 replace(),即直接把新读进来的元素放在堆顶,然后执行下沉(siftDown())操作。Java 当中的 PriorityQueue 没有提供这个操作,只好先 poll() 再 offer()。
import java.util.Comparator;
import java.util.PriorityQueue;
public class Solution {
public int findKthLargest(int[] nums, int k) {
int len = nums.length;
// 使用一个含有 k 个元素的最小堆,PriorityQueue 底层是动态数组,为了防止数组扩容产生消耗,可以先指定数组的长度
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k, Comparator.comparingInt(a -> a));
// Java 里没有 heapify ,因此我们逐个将前 k 个元素添加到 minHeap 里
for (int i = 0; i < k; i++) {
minHeap.offer(nums[i]);
}
for (int i = k; i < len; i++) {
// 看一眼,不拿出,因为有可能没有必要替换
Integer topElement = minHeap.peek();
// 只要当前遍历的元素比堆顶元素大,堆顶弹出,遍历的元素进去
if (nums[i] > topElement) {
// Java 没有 replace(),所以得先 poll() 出来,然后再放回去
minHeap.poll();
minHeap.offer(nums[i]);
}
}
return minHeap.peek();
}
}
简版java
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->a-b);
for (int x : nums) {
if (q.size() < k || q.peek() < x) q.add(x);
if (q.size() > k) q.poll();
}
return q.peek();
}
}
根据当前队列元素个数或当前元素与栈顶元素的大小关系进行分情况讨论:
当优先队列元素不足 k 个,可将当前元素直接放入队列中;
当优先队列元素达到 k 个,并且当前元素大于栈顶元素(栈顶元素必然不是答案),可将当前元素放入队列中。
复杂度分析:
时间复杂度:O(NlogK),遍历数据 O(N),堆内元素调整 O(logK);
空间复杂度:O(K)。
大根堆、小根堆:
大根堆:
大根堆指在二叉树结构中,根结点>子结点,总是最大的,并且在堆的每一个局部都是如此。例如{3,1,2}可以看作为大根堆,而{3,2,1}亦可以看作为大根堆。大根堆的根结点在整个堆中是最大的元素。
小根堆:
小根堆在二叉树的结构中,根结点<子结点。例如{1,2,3}为小根堆,{1,3,2}同样也是小根堆。小根堆的根结点在整个堆中是最小的元素。
大/小根堆中根结点即是整个序列中最大 /小的元素,那么从堆中获取最大/小的元素则非常快速,只要返回序列中的首元素即可。
更一般的,堆处处局部的有序性,形成了堆整体的有序性。 只要有一处局部没有满足堆的有序性,则可以说堆失序,此时便需要进行相应的调整。