算法多解——JZ40 最小的K个数(大根堆模拟及手撕)

题面


解法1(快排)

复杂度

时间复杂度:O(nlongn),取决于排序的快慢

空间复杂度:O(n)

思路

  • 由于逻辑关系和常理,k<n成立,不用特判。
  • 要求最小的k个数,最能直接想到的思路就是:
  • 先将这些数排好序,前k个最小的数压入ans数组,最后返回就好了。
  • 根据常用习惯,下面就用了sort快排和Vector的push_back解决了。

也可以排序后直接 return vector<int>({input.begin(), input.begin()+k});

代码

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> ans; //开辟存放结果的数组
        sort(input.begin(),input.end());
        for(int i=0; i<k; i++){
            ans.push_back(input[i]); //压入合法的数
        }
        return ans; //返回存放结果的数组
    }
};

解法2(STL进阶:大根堆)

复杂度

时间复杂度:O(nlongk),堆的每次操作时间复杂度不超过O(logk)

空间复杂度:O(k)

思路

  • 优先队列模拟大根堆来维护最小的k个数字
  • 有很多排序算法,我们很容易想到通过选择复杂度更低的排序算法来优化我们的解法
  • 这里就要用到大根堆维护最小的k个数字:
  • 依次将n个数字放入堆中,若堆中数字超过k个,就说明此时堆中最大的数一定不属于前k个最小数,不断将堆顶元素出堆,就能得到想要的结果了。

大小根堆的介绍:

代码

class Solution {
public:
  priority_queue<int> q;
  vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
      // 将原数组的数放入优先队列
      for(auto x:input){
          q.push(x);
          if(q.size()>k) q.pop();
      }
      input.clear();
      //若达不到k个数字
      if(q.size()<k) return input;
      while(!q.empty()){
          input.push_back(q.top());
          q.pop();
      }
      return input;
  }
};

手写大根堆代码

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list = new ArrayList<>();
        if(input==null || input.length==0 || k>input.length || k==0)
            return list;
        int[] arr = new int[k+1];//数组下标0的位置作为哨兵,不存储数据
        //初始化数组
        for(int i=1; i<k+1; i++)
            arr[i] = input[i-1];
        buildMaxHeap(arr, k + 1);//构造大根堆
        for(int i=k; i<input.length; i++) {
            if(input[i] < arr[1]) {
                arr[1] = input[i];
                adjustDown(arr, 1, k+1);//将改变了根节点的二叉树继续调整为大根堆
            }
        }
        for(int i=1; i<arr.length; i++) {
            list.add(arr[i]);
        }
        return list;
    }
 
    public void buildMaxHeap(int[] arr, int length) {
        if (arr == null || arr.length == 0 || arr.length == 1)
            return;
        for (int i = (length - 1) / 2; i > 0; i--) {
            adjustDown(arr, i, arr.length);
        }
    }

    // 堆排序中对一个子二叉树进行堆排序 
    public void adjustDown(int[] arr, int k, int length) {
        arr[0] = arr[k];//哨兵
        for (int i=2*k; i<=length; i*=2) {
            if(i<length-1 && arr[i]<arr[i+1])
                i++;//取k较大的子结点的下标
            if(i>length-1 || arr[0]>=arr[i])
                break;
            else {
                arr[k] = arr[i];
                k = i; //向下筛选
            }
        }
        arr[k] = arr[0];
    }
};

解法3(快速选择算法

复杂度

时间复杂度:期望O(n),n为数组长度,最坏时间复杂度为O(n^2)
空间复杂度:期望O(logn),递归调用的期望深度为logn,但最坏情况为O(n)

思路

  1. 根据题目可知,返回的数组不必是有序的,故我们可以想到快速选择算法来解决本问题,不熟悉快速选择算法的可以先看看第k个数这题,与其思路一致,我们可以利用快速排序的思想来实现
  2. 快速排序每次根据一个pivot点对区间进行划分,使得pivot左侧的数都比它小,右侧的数都比它大,然后再对两部分就行划分,而快速选择只需要选择其中一部分进行划分即可,根据k可以选择进入那个递归入口,这样的期望时间复杂度是线性的,但最坏情况下是O(n^2)的
  3. 算法步骤,首先选择数组中点为pivot,再使用双指针算法交换左侧大于等于pivot的数和右边小于等于pivot的数,最后我们可以保证数组被分为了以pivot划分的两部分,接着根据k选择递归入口即可,算法结束后我们可以保证数组的前k个数是最小的k个数,但是并不是升序的

代码

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        quickSelect(input, 0, input.size() - 1, k);
        return vector<int>(input.begin(), input.begin() + k);
    }

    void quickSelect(vector<int>& input, int l, int r, int k){
        if(l >= r) return;
        int i = l - 1, j = r + 1, pivot = input[l + r >> 1];
        // 令小于pivot的数都在左侧, 大于pivot的数都在右侧
        while(i < j){
            while(input[++i] < pivot);
            while(input[--j] > pivot);
            if(i < j) swap(input[i], input[j]);
        }
        int cnt = j - l + 1;    // cnt表示小于等于pivot的数的数量
        if(cnt >= k){    // 若cnt >= k, 说明答案在左区间
            return quickSelect(input, l, j, k);
        }
        return quickSelect(input, j + 1, r, k - cnt);    // 左边cnt个数已经确定, 只要确定右边的即可
    }
};

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

米莱虾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值