题目
题目:https://leetcode.cn/problems/kth-largest-element-in-an-array/description/
解法
快排
这道题目目前快排可以直接过,但是时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)。
想要 O ( n ) O(n) O(n),这就涉及到408考研知识点了😂(bushi)。快排每次 partition 过程一定会确定 pivot 的位置,我们根据 k 的值对其中一侧继续 partition 直到 pivot = k。
理想情况,假设每次 pivot 都在中间位置,那么时间复杂度为 n + n / 2 + n / 4 + ⋯ = O ( n ) n+n/2+n/4+\cdots=O(n) n+n/2+n/4+⋯=O(n)。
最差情况是数组已有序,每次 pivot 都在数组末尾,而 k 在另一侧,时间复杂度为 n + ( n − 1 ) + ( n − 2 ) + ⋯ = O ( n 2 ) n+(n-1)+(n-2)+\cdots=O(n^2) n+(n−1)+(n−2)+⋯=O(n2)。为了避免这种情况,引入随机化。
因为找最大值,所以降序排列。
class Solution {
public:
int partition(vector<int>& nums, int start, int end) {
// 随机化确定 num[end]
int ran = rand() % (end-start+1) + start;
swap(nums[ran], nums[end]);
// p 指针右边的数都小于(大于)pivot
int p = start-1, x = nums[end];
for (int i = start; i < end; i++) {
if (nums[i] >= x) {
swap(nums[++p], nums[i]);
}
}
swap(nums[++p], nums[end]);
// 返回 pivot
return p;
}
int qsort(vector<int>& nums, int start, int end, int k) {
// 无需最顶层栈的 return,partition 函数已经涵盖
int pivot = partition(nums, start, end);
if (pivot < k) {
return qsort(nums, pivot+1, end, k);
} else if (pivot > k) {
return qsort(nums, start, pivot-1, k);
} else {
return nums[k];
}
}
int findKthLargest(vector<int>& nums, int k) {
int len = nums.size();
return qsort(nums, 0, len-1, k-1);
}
};
进阶:双路快排
如果数组中有很多重复元素,那么即使随机化也无法减少最坏情况 O ( n 2 ) O(n^2) O(n2) 的出现,因此在 partition 函数中可以使用双路快排,具体看这里:https://www.runoob.com/data-structures/2way-quick-sort.html
但是性能和上面的差不多好像…
class Solution {
public:
int partition(vector<int>& nums, int start, int end) {
int ran = rand() % (end - start + 1) + start;
swap(nums[start], nums[ran]);
int pivot = nums[start];
int low = start, high = end;
while(low < high) {
while(low < high && nums[high] <= pivot) {
high--;
}
nums[low] = nums[high];
while(low < high && nums[low] >= pivot) {
low++;
}
nums[high] = nums[low];
}
nums[low] = pivot;
return low;
}
int quicksort(vector<int>& nums, int start, int end, int k) {
int pivot = partition(nums, start, end);
if (pivot < k) {
return quicksort(nums, pivot+1, end, k);
} else if (pivot > k) {
return quicksort(nums, start, pivot-1, k);
} else {
return nums[k];
}
}
int findKthLargest(vector<int>& nums, int k) {
int len = nums.size();
return quicksort(nums, 0, len-1, k-1);
}
};
堆
大顶堆:将所有元素进行堆排序,然后 pop k 次即可,top 即为第 k 大元素。
小顶堆:维护一个大小为 k 的小顶堆,依次将数组元素 push 入堆,然后 pop,数据遍历完后,top 即为第 k 大元素。
引用 leetcode 上一句评论
大小顶堆的方案虽然都是堆,但是思路是完全不同的。大顶堆是典型的排序思路(即堆排序),建堆后,逐个pick堆顶元素,就能获得全序数组;小顶堆则是利用堆的偏序性质,因为我们并不需要全序数组,所以“第k大”这样的偏序元素可以通过小根堆堆顶来直接确定,然而比较可惜的是其仍然达不到quick-select的O(n)。
大顶堆
小顶堆
假设数组很大很大,内存无法全部存放,如何找到第 k 个最大值(不要求时间复杂度 O ( n ) O(n) O(n),但要保证耗时尽可能少)?
内存无法存放,那就不能用排序(涉及到内存和外存的交换,速度会很慢)。可以维护一个大小为 k 的小顶堆,遍历数组时 pop 出 len - k 个最小元素。时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))
这个堆具体是优先队列,其内部用堆实现。优先队列 push 新元素后,在 O ( l o g n ) O(logn) O(logn) 时间内维护好,通过 pop 可以删除堆顶元素,在 O ( l o g n ) O(logn) O(logn) 时间内维护好。
priority_queue 默认是大顶堆。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> que;
int len = nums.size();
for (int i = 0; i < k; i++) {
que.push(nums[i]);
}
for (int i = k; i < len; i++) {
if (nums[i] <= que.top()) {
continue ;
}
que.push(nums[i]);
que.pop();
}
return que.top();
}
};