zoukankan      html  css  js  c++  java
  • 剑指 Offer 40. 最小的k个数

    思路

    方法一:排序

    对原数组从小到大排序后取出前 k 个数即可。

    时间复杂度:O(nlogn),其中 n 是数组 arr 的长度。算法的时间复杂度即排序的时间复杂度。

    方法二:堆

    我们用一个大根堆实时维护数组的前 kk 小值。首先将前 kk 个数插入大根堆中,随后从第 k+1k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。

    时间复杂度:O(nlogk),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要O(nlogk) 的时间复杂度。

    空间复杂度:O(k),因为大根堆里最多 k 个数。

     1 class Solution {
     2 public:
     3     vector<int> getLeastNumbers(vector<int>& arr, int k) {
     4         vector<int> res;
     5         if(k <= 0)
     6             return res;
     7             
     8         priority_queue<int, vector<int>, less<int>> Q;
     9         
    10         //先把前k个数放入大顶堆
    11         for(int i = 0; i < k; ++i)
    12             Q.push(arr[i]);
    13         
    14         for(int i = k; i < arr.size(); ++i) {
    15             if(arr[i] < Q.top()) {
    16                 Q.pop();
    17                 Q.push(arr[i]);
    18             }
    19         }
    20 
    21         while(!Q.empty()) {
    22             res.push_back(Q.top());
    23             Q.pop();
    24         }
    25 
    26         return res;
    27     }
    28 };

    方法三:利用快排的思想进行划分

    这里可以参考:《算法导论(第3版)》的9.2和9.3节。

    我们可以借鉴快速排序的思想。我们知道快排的划分函数每次执行完后都能将数组分成两个部分,小于等于分界值 pivot 的元素的都会被放到数组的左边,大于pivot的都会被放到数组的右边。

    与快速排序不同的是,快速排序会根据分界值的下标递归处理划分的两侧,而这里我们只处理划分的一边,并返回分界值pivot的下标i。但是划分代码和快速排序中的代码是一模一样的。

    所以算法步骤如下:
    (1)编写partition()函数,与快速排序的元素划分代码一模一样,选择区间最左侧的元素为pivot,将一个区间内所有小于pivot的数放在左边,大于pivot的数放在右边,然后返回pivot的下标i。
    (2)对每次划分之后pivot的下标和k进行判断:
    如果划分之后的下标pivotIndex刚好等于要找的第k个数的下标(下标从零开始,所以此处为k-1), 说明arr[pivotIndex]就是要找的第k个数,又由于pivotIndex左边的数都小于arr[pivotIndex],

    所以左边的数就是最小的k个数,则将这k个数装入答案数组;

     

    如果划分之后的下标pivotIndex大于要找的第k个数的下标,则递归处理左半区间[left, pivotIndex-1] ;
    如果划分之后的下标pivotIndex小于要找的第k个数的下标,则递归处理右半区间[pivotIndex+1, right] 。
     

    时间复杂度:期望为 O(n),具体证明可以参考《算法导论 (第3版)》第 9.2节。

    对于最好的情况:每次所选的pivot划分之后正好在数组的正中间,那么递归方程为T(n) = T(n/2) + n,解得T(n) = O(n),所以此时此算法是O(n)线性复杂度的。

    对于最坏的情况:每次的划分点都是最大值或最小值,即每次所选的pivot划分之后都好在数组最边上,一共需要划分 n - 1次,而每次划分需要O(n)的时间复杂度,所以此时此算法时间复杂度为O(n2)。

    可以改进:改进选取主元的方法,使每次选出的主元在划分之后都能接近数组的中间位置,这样每次划分都能减少当前区间一半元素的工作量,可以使最坏情况下的时间复杂度降为O(n)。关于这种改进后的主元选取方法,见《算法导论(第3版)》的9.3节和这篇BFPRT算法的文章

    空间复杂度:期望为 O(logn),递归调用的期望深度为 O(logn),每层需要的空间为 O(1),只有常数个变量。最坏情况下的空间复杂度为 O(n)。最坏情况下需要划分 n次,而每层由于需要 O(1)的空间,所以一共需要 O(n) 的空间复杂度。

     1 class Solution {
     2 public:
     3     vector<int> getLeastNumbers(vector<int>& arr, int k) {
     4         if(k <= 0 || arr.empty())  return vector<int>();
     5         return partition(arr, k, 0, arr.size()-1);
     6     }
     7 
     8     /*和快速排序的划分代码一模一样*/
     9     vector<int> partition(vector<int>& arr, int k, int left, int right) {
    10         int pivot = arr[left];
    11 
    12         int i = left, j = right;
    13         while(i < j) {
    14             while(j > i && arr[j] >= pivot) --j;   
    15             while(i < j && arr[i] <= pivot) ++i;
    16 
    17             swap(arr[i], arr[j]);
    18         }
    19 
    20         swap(arr[left], arr[i]);
    21 
    22         //先判断,有选择的对左半边或者右半边进行递归
    23         if(i == k-1) {
    24             vector<int> res;
    25             for(int t = 0; t <= i; ++t) {
    26                 res.push_back(arr[t]);
    27             }
    28             return res;
    29         } else if(i > k-1) {
    30             return partition(arr, k, left, i-1);
    31         } else {
    32             return partition(arr, k, i+1, right);
    33         }
    34     }
    35 };

  • 相关阅读:
    验证foreach 能否操做更改原表
    asp.net post/get 公共方法
    C# json日期转换
    学数学
    2742: [HEOI2012]Akai的数学作业
    BZOJ2208
    树状数组求逆序对
    网络流复习计划
    SG函数学(hua)习(shui)记录
    SPLAY板子
  • 原文地址:https://www.cnblogs.com/FengZeng666/p/13927812.html
Copyright © 2011-2022 走看看