LeetCode 215. 数组中的第K个最大元素
描述:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个
最大的元素,而不是第 k 个不同的元素。
方法 0 : 直接 std::sort 排序 时间O(n*logn) 空间O(1)
求 数组中的第K个最大元素 直观解法就是,先将数组 nums 排好序,然后取nums[n - k]即可。
int findKthLargest(vector<int>& nums, int k) { std::sort(nums.begin(),nums.end()); return nums[nums.size()-k]; }
分析:时间复杂度就是 std::sort 的时间复杂度 O(n*logn)。
方法 1 :快速选择算法 时间O(n) 空间O(1)
求解 k-th Element 问题 通常还可以使用快速选择算法,即快速排序的 “partition 算法思想” 。
1 class Solution { 2 public: 3 //time:O(n*logn) space:O(1); 4 // int findKthLargest(vector<int>& nums, int k) { 5 // std::sort(nums.begin(),nums.end()); 6 // return nums[nums.size()-k]; 7 // } 8 9 int findKthLargest(vector<int>& nums, int k) 10 { 11 random_shuffle(nums.begin(), nums.end());//洗牌算法 12 int l = 0,r = nums.size() - 1; 13 int target_index = nums.size() - k; 14 while(l <= r) 15 { 16 int mid = quickSelcetion(nums,l,r); 17 if(mid == target_index) 18 { 19 return nums[mid]; 20 } 21 else if(mid < target_index) 22 { 23 l = mid + 1; 24 } 25 else 26 { 27 r = mid - 1; 28 } 29 } 30 return INT_MAX; 31 } 32 //快速排序的partition思想 33 //以nums[l] 为 pivot ,把 nums[l] 放在它最终的位置上 34 //快速排序就是执行一次partition划分,确定一个元素的最终位置, 35 //直到最后所有的元素到放到最终的位置,排序完成。 36 int quickSelcetion(vector<int>& nums,int l,int r) 37 { 38 int i = l + 1, j = r; 39 while(true) 40 { 41 //i 没有和j 交错 且 遇到的 nums[i] 都不大于 pivot, 42 //就一直往右走,走过的路径上的所有元素以后都会放在 pivot的左边 43 while(i <= j && nums[i] <= nums[l]) 44 { 45 ++i; 46 } 47 //j走过的路径上的所有元素以后都会放在pivot的右边 48 while(j >= i && nums[j] >= nums[l]) 49 { 50 --j; 51 } 52 //i和j交错了,直接跳出外层循环 53 if(j < i) 54 { 55 break; 56 } 57 std::swap(nums[i],nums[j]); 58 } 59 //此时nums [j+1,r]一定都 >= nums[l], 60 std::swap(nums[l],nums[j]); 61 return j; 62 //或者 63 // std::swap(nums[l],nums[i - 1]); 64 // return i - 1; 65 } 66 };
分析:在 方法 一 中使用了 c++ STL 中提供的std::sort ,std::sort 是个复杂同时也非常高效的排序算法,
主要使用了 快速排序的思想,整体性能很高。快速排序的逻辑是:
若要对nums[lo..hi]
进行排序,我们先找一个分界点p
,通过交换元素使得nums[lo..p-1]
都小于等于nums[p]
,
且nums[p+1..hi]
都大于nums[p]
,然后递归地去nums[lo..p-1]
和nums[p+1..hi]
中寻找新的分界点,
最后整个数组就被排序了。
但是本题只需要知道 在 nums 排序好后,在target_index = nums.size() - k 的位置上的元素。
不需要再继续对区间[0,target_index-1]和区间 [target_index+1,nums.size()-1 ]排序了。所以只需要借助 快速排序的 partition 划分思想,
partition 的算法框架需要记住!!!
疑问 :
1. 为什么时间 是 O(n)?如何分析证明?
答: O(N)
的时间复杂度是个均摊复杂度,需要在算法开始的时候对nums
数组来一次随机打乱。上面代码加了
random_shuffle(nums.begin(), nums.end()); 直接从12% 提升到 99%。
2. 为什么在上面的代码中 返回 j 或 i - 1,而不能返回 i ?
答:细节问题。下面代码代码参考《算法4》,是众多写法中最漂亮简洁的一种。可以先背下 partition框架。
再慢慢体会。
1 int partition(int[] nums, int lo, int hi) { 2 if (lo == hi) return lo; 3 // 将 nums[lo] 作为默认分界点 pivot 4 int pivot = nums[lo]; 5 // j = hi + 1 因为 while 中会先执行 -- 6 int i = lo, j = hi + 1; 7 while (true) { 8 // 保证 nums[lo..i] 都小于 pivot 9 while (nums[++i] < pivot) { 10 if (i == hi) break; 11 } 12 // 保证 nums[j..hi] 都大于 pivot 13 while (nums[--j] > pivot) { 14 if (j == lo) break; 15 } 16 if (i >= j) break; 17 // 如果走到这里,一定有: 18 // nums[i] > pivot && nums[j] < pivot 19 // 所以需要交换 nums[i] 和 nums[j], 20 // 保证 nums[lo..i] < pivot < nums[j..hi] 21 swap(nums, i, j); 22 } 23 // 将 pivot 值交换到正确的位置 24 swap(nums, j, lo); 25 // 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi] 26 return j; 27 } 28 29 // 交换数组中的两个元素 30 void swap(int[] nums, int i, int j) { 31 int temp = nums[i]; 32 nums[i] = nums[j]; 33 nums[j] = temp; 34 }
方法 2:遍历nums,维护一个大小为 k 的 小顶堆 时间O(n* logK) 空间O(k)
小顶堆可以使用 c++ STL 中的 priority_queue
priority_queue:最大值先出的数据结构,默认基于vector实现堆结构。它可以在O(n log n)
的时间排序数组,O(log n) 的时间插入任意值,O(1) 的时间获得最大值,O(log n) 的时
间删除最大值。priority_queue 常用于维护数据结构并快速获取最大或最小值。
维护1 个大小为 k 的小顶堆,堆顶就是堆中的最小元素,遍历nums,要是堆内的元素个数 小于
k,直接将 nums[i] 元素 push 到堆中。
要是堆的大小已经是 k 了,将堆顶元素和 nums[i] 比较,要是nums[i] <= 堆顶元素,则nums[i]
肯定不在 nums 的前 k 个最大元素中,不用考虑,直接跳过。否则才将堆中元素pop 掉一个,再插入nums[i] 到堆中。
代码如下:
int findKthLargest(vector<int>& nums, int k) 2 { 3 //基于vector 的 小顶堆,默认是大顶堆的 ,需加上greater<int> std::priority_queue<int,vector<int>,greater<int>> pq; 4 for(auto num : nums) 5 { 6 // 不在前k个最大元素内的,直接跳过 if(pq.size() == k && pq.top() >= num ) continue; 7 if(pq.size() == k) 8 { 9 pq.pop(); 10 } 11 pq.push(num); 12 } 13 return pq.top(); 14 } 15