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

    剑指 Offer 40. 最小的k个数

    输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

    示例 1:

      输入:arr = [3,2,1], k = 2
      输出:[1,2] 或者 [2,1]
    示例 2:

      输入:arr = [0,1,2,1], k = 1
      输出:[0]

    方法一:排序

    思路和算法

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

    class Solution {
    public:
        vector<int> getLeastNumbers(vector<int>& arr, int k) {
            vector<int> vec(k, 0);
            sort(arr.begin(), arr.end());
            for (int i = 0; i < k; ++i) vec[i] = arr[i];
            return vec;
        }
    };

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

    空间复杂度:O(logn),排序所需额外的空间复杂度为 O(logn)。

     

    方法二:优先队列

    #include<iostream>
    #include <queue>
    #include<vector>
    using namespace std;
    int main(){
        priority_queue<int,vector<int> ,greater<int> >q;
        int num,n;
        cin>>num;
        q.push(num);
        while (cin.get()!='
    ')
        {
            cin>>num;
            q.push(num);
        }
        cin>>n;
        for (int i = 0; i < n; i++)
        {
            cout<<q.top()<<" ";
            q.pop();
        }
    }

     

    思路和算法

    我们用一个大根堆实时维护数组的前 k小值。首先将前 k 个数插入大根堆中,随后从第 k+1个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。在下面的代码中,由于 C++ 语言中的堆(即优先队列)为大根堆,我们可以这么做。

    class Solution {
    public:
        vector<int> getLeastNumbers(vector<int>& arr, int k) {
            vector<int>vec(k, 0);
            if (k == 0) return vec; // 排除 0 的情况
            priority_queue<int>Q;
            for (int i = 0; i < k; ++i) Q.push(arr[i]);
            for (int i = k; i < (int)arr.size(); ++i) {
                if (Q.top() > arr[i]) {
                    Q.pop();
                    Q.push(arr[i]);
                }
            }
            for (int i = 0; i < k; ++i) {
                vec[i] = Q.top();
                Q.pop();
            }
            return vec;
        }
    };

    复杂度分析

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

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

    方法三:快排思想
    思路和算法

    我们可以借鉴快速排序的思想。我们知道快排的划分函数每次执行完后都能将数组分成两个部分,小于等于分界值 pivot 的元素的都会被放到数组的左边,大于的都会被放到数组的右边,然后返回分界值的下标。与快速排序不同的是,快速排序会根据分界值的下标递归处理划分的两侧,而这里我们只处理划分的一边。

    我们定义函数 randomized_selected(arr, l, r, k) 表示划分数组 arr 的 [l,r] 部分,使前 k 小的数在数组的左侧,在函数里我们调用快排的划分函数,假设划分函数返回的下标是 pos(表示分界值 pivot 最终在数组中的位置),即 pivot 是数组中第 pos - l + 1 小的数,那么一共会有三种情况:

    如果 pos - l + 1 == k,表示 pivot 就是第 kk 小的数,直接返回即可;

    如果 pos - l + 1 < k,表示第 kk 小的数在 pivot 的右侧,因此递归调用 randomized_selected(arr, pos + 1, r, k - (pos - l + 1));

    如果 pos - l + 1 > k,表示第 kk 小的数在 pivot 的左侧,递归调用 randomized_selected(arr, l, pos - 1, k)。

    函数递归入口为 randomized_selected(arr, 0, arr.length - 1, k)。在函数返回后,将前 k 个数放入答案数组返回即可。

     

    class Solution {
        int partition(vector<int>& nums, int l, int r) {
            int pivot = nums[r];
            int i = l - 1;
            for (int j = l; j <= r - 1; ++j) {
                if (nums[j] <= pivot) {
                    i = i + 1;
                    swap(nums[i], nums[j]);
                }
            }
            swap(nums[i + 1], nums[r]);
            return i + 1;
        }
        // 基于随机的划分
        int randomized_partition(vector<int>& nums, int l, int r) {
            int i = rand() % (r - l + 1) + l;
            swap(nums[r], nums[i]);
            return partition(nums, l, r);
        }
        void randomized_selected(vector<int>& arr, int l, int r, int k) {
            if (l >= r) return;
            int pos = randomized_partition(arr, l, r);
            int num = pos - l + 1;
            if (k == num) return;
            else if (k < num) randomized_selected(arr, l, pos - 1, k);
            else randomized_selected(arr, pos + 1, r, k - num);   
        }
    public:
        vector<int> getLeastNumbers(vector<int>& arr, int k) {
            srand((unsigned)time(NULL));
            randomized_selected(arr, 0, (int)arr.size() - 1, k);
            vector<int>vec;
            for (int i = 0; i < k; ++i) vec.push_back(arr[i]);
            return vec;
        }
    };

    复杂度分析

    时间复杂度:期望为 O(n) ,由于证明过程很繁琐,所以不再这里展开讲。具体证明可以参考《算法导论》第 9 章第 2 小节。最坏情况下的时间复杂度为 O(n^2)

    。情况最差时,每次的划分点都是最大值或最小值,一共需要划分 n - 1次,而一次划分需要线性的时间复杂度,所以最坏情况下时间复杂度为 O(n^2)。

    空间复杂度:期望为 O(logn),递归调用的期望深度为 O(logn),每层需要的空间为 O(1),只有常数个变量。

    最坏情况下的空间复杂度为 O(n)。最坏情况下需要划分 n 次,即 randomized_selected 函数递归调用最深 n - 1层,而每层由于需要 O(1) 的空间,所以一共需要 O(n) 的空间复杂度。

     

    因上求缘,果上努力~~~~ 作者:每天卷学习,转载请注明原文链接:https://www.cnblogs.com/BlairGrowing/p/13534444.html

  • 相关阅读:
    1014 Waiting in Line (30)(30 point(s))
    1013 Battle Over Cities (25)(25 point(s))
    1012 The Best Rank (25)(25 point(s))
    1011 World Cup Betting (20)(20 point(s))
    1010 Radix (25)(25 point(s))
    1009 Product of Polynomials (25)(25 point(s))
    1008 Elevator (20)(20 point(s))
    1007 Maximum Subsequence Sum (25)(25 point(s))
    1006 Sign In and Sign Out (25)(25 point(s))
    1005 Spell It Right (20)(20 point(s))
  • 原文地址:https://www.cnblogs.com/BlairGrowing/p/13534444.html
Copyright © 2011-2022 走看看