微软面试题: LeetCode 215. 数组中的第K个最大元素 出现次数:6

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             

 

 

 

       

 

posted @ 2020-11-12 18:08  谁在写西加加  阅读(150)  评论(0编辑  收藏  举报