题目
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例
输入:[3,2,3,1,2,4,5,5,6] 和 k = 4
输出:4
TopK问题是一道高频面试题!
解法一:排序+查找
由于数组是未排序的,最直接粗暴的方法就是先对数组进行排序,排序后直接通过索引返回满足条件的元素即可。代码简洁到感人。
class Solution {
public int findKthLargest(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length - k];
}
}
复杂度:总的时间复杂度就是排序的复杂度:\\(O(NlogN)\\),没有使用额外的空间,空间复杂度为 \\(O(1)\\)。
解法二:快速排序(单边)
快速排序中,每次partion操作将数组分为两部分,左边小于切分元素,右边大于切分元素。这样我们如果找到这样的一个切分点,partion操作完它右边有 k-1 个元素,那这个切分点就是我们需要的元素。
class Solution {
Random rand = new Random();
public int findKthLargest(int[] nums, int k) {
return findK(nums, 0, nums.length - 1, k);
}
private int findK(int[] nums, int lo, int hi, int k){
int j = partion(nums, lo, hi);
if(j == k -1) return nums[j];
else if(j > k - 1){
return findK(nums, lo, j - 1, k);
}
else{
return findK(nums, j + 1, hi, k);
}
}
private int partion(int[] nums, int lo, int hi){
int r = rand.nextInt(hi - lo + 1) + lo; // 切分点的位置,随机选择很关键
int tmp = nums[r]; // 哨兵
nums[r] = nums[lo];
nums[lo] = tmp;
while(lo < hi){
while(lo < hi && nums[hi] <= tmp){
hi--;
}
nums[lo] = nums[hi];
while(lo < hi && nums[lo] >= tmp){
lo++;
}
nums[hi] = nums[lo];
}
nums[lo] = tmp;
return lo;
}
}
复杂度:快速排序的复杂度为 \\(O(NlogN)\\),但是上面的代码仅仅是快速排序的部分实现,其期望时间复杂度为 \\(O(N)\\),《算法导论》里给出了具体的复杂度分析;空间复杂度为 \\(O(logN)\\),即递归栈所使用的额外空间。
注意:选择切分点对于极端情况下程序的计算时间影响很大。如下图,对于leetcode给的测试样例,不使用随机操作程序执行时间会增加5倍以上。
解法三:堆排序
堆排序有两种思路:
- 使用最大堆进行原地排序,每次从堆中删去堆顶元素(堆中最大元素)并重新调整堆使其有序,在删去 k-1 个元素并堆有序后,堆顶元素就是所求元素。
- 建立大小为 k 的最小堆,接下来每次将一个元素和堆顶元素比较,如果大于堆顶元素,则替换堆顶元素并调整使堆有序。遍历完所有元素后堆顶就是所求元素。
下面是思路一的实现,自己在数组原地实现了最大堆。也可以借助java的PriorityQueue类实现堆。
class Solution {
public int findKthLargest(int[] nums, int k) {
int heapSize = nums.length;
for(int i = heapSize/2; i >= 0; i--){
adjust(nums, i, heapSize); // 通过调整使得初始堆有序
}
while(--heapSize > nums.length - k){
swap(nums, 0, heapSize);
adjust(nums, 0, heapSize);
}
return nums[heapSize + 1];
}
private void adjust(int[] nums, int i, int size){
while(2 * i + 1 < size){
int j = 2 * i + 1;
if(j + 1 < size && nums[j] < nums[j + 1]){ // 左右孩子中值最大的索引
j++;
}
if(arr[i] > arr[j]) break;
swap(nums, i, j);
i = j;
}
}
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
复杂度:使用思路一原地排序,空间复杂度为 \\(O(1)\\);时间复杂度为 \\(O(NlogN)\\),其中构建堆的时间复杂度为 \\(O(N)\\),删除k个元素的时间代价为 \\(O(klogN)\\),渐进时间复杂度为 \\(O(NlogN)\\)。使用思路二的最小堆,时间复杂度为 \\(O(Nlog(k))\\),如果不使用递归不考虑递归栈的空间,空间复杂度为 \\(O(k)\\)。
!!!解法二快速排序和解法三堆排序是TopK问题推荐的解法,一定要掌握!