[剑指 offer]--大顶堆 ➕ 快速选择 --面试题40. 最小的k个数

1 题目描述

输入整数数组 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]

限制:

0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

2 解题思路

瞎说八道系列:调用库函数
这是我一开始想到的方法,这也是偷懒的方法,真正的最优解看下面的解答

要找到TOP K元素,有两种经典的解法:
Top K 问题有两种不同的解法,一种解法使用堆(优先队列),另一种解法使用类似快速排序的分治法。

  • 方法一:大顶堆

比较直观的想法是使用堆数据结构来辅助得到最小的 k 个数。堆的性质是每次可以找出最大或最小的元素。我们可以使用一个大小为 k 的最大堆(大顶堆),将数组中的元素依次入堆,当堆的大小超过 k 时,便将多出的元素从堆顶弹出。我们以数组 [5,4,1,3,6,2,9],k=3 为例展示元素入堆的过程,如下面动图所示:

在这里插入图片描述

这样,由于每次从堆顶弹出的数都是堆中最大的,最小的 k 个元素一定会留在堆里。这样,把数组中的元素全部入堆之后,堆中剩下的 k 个元素就是最大的 k 个数了。代码的可优化的地方在于,如果当前数字不小于堆顶元素,数字可以直接丢掉,不入堆。

  • 算法的复杂度分析:

由于使用了一个大小为 k 的堆,空间复杂度为 O(k)
入堆和出堆操作的时间复杂度均为 O(logk),每个元素都需要进行一次入堆操作,故算法的时间复杂度为 O(nlogk)

作者:nettee
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/tu-jie-top-k-wen-ti-de-liang-chong-jie-fa-you-lie-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 方法二:快速选择(类似快速选择,分治思想)
    在这里插入图片描述
    算法的复杂度分析:

空间复杂度 O(1),不需要额外空间。
时间复杂度的分析方法和快速排序类似。由于快速选择只需要递归一边的数组,时间复杂度小于快速排序,期望时间复杂度为
O(n),最坏情况下的时间复杂度为 O(n2 )。

两种方法的优劣性比较

在面试中,另一个常常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:
第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。
第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。

作者:nettee
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/tu-jie-top-k-wen-ti-de-liang-chong-jie-fa-you-lie-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3 解决代码

  • 调用库函数 java代码
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        
        Arrays.sort(arr);
        int[] res = new int[k];
        for(int i = 0; i < k; i++){
            res[i] = arr[i];
        }
        return res;
    }
}
  • 调用库函数 python3代码
class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        arr.sort()
        return arr[:k]
  • 方法一:堆的java代码
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(k == 0 || arr.length == 0){
            return new int[0];
        }
        // 默认是小根堆,实现大根堆需要重写一下比较器。
        Queue<Integer> heap = new PriorityQueue<>((v1, v2) -> v2 - v1);
        for(int num:arr){
            // 当前数字小于堆顶元素才会入堆
            if(heap.isEmpty() || heap.size() < k || num < heap.peek()){
                heap.offer(num);
            }
            //如果超过了堆内的容量,删除栈顶最大元素
            if(heap.size() > k){
                heap.poll();
            }
        }
        // 将堆中的元素存入数组
        int[] res = new int[heap.size()];
        int index = 0;
        for(int num: heap){
            res[index++] = num;
        }
        return res;

    }
}
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(k == 0 || arr.length == 0){
            return new int[0];
        }else if(arr.length <= k){
            return arr;
        }
        // 原地不断划分数组
        partitionArray(arr, 0, arr.length - 1, k);
    
        // 数组的前 k 个数此时就是最小的 k 个数,将其存入结果
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }
    void partitionArray(int[] arr, int low, int high, int k){
        // 做一次 partition 操作
        int m = partition(arr, low, high);
        // 此时数组前 m 个数,就是最小的 m 个数
        if( k == m){
            // 正好找到最小的 k(m) 个数
            return;
        }
         // 最小的 k 个数一定在前 m 个数中,递归划分
        else if(k < m){
            partitionArray(arr, low, m -1, k);
        }
        // 在右侧数组中寻找最小的 k-m 个数
        else{
            partitionArray(arr, m+1, high,k);
        }
        
    }
    // partition 函数和快速排序中相同,
    int partition(int num[], int low, int high){
        int i = low;
        int j = high + 1;
        int v = num[low];
        while(true){
            while (++i <= high && num[i] < v);
            while (--j >= low && num[j] > v);
            if(i >= j){
               break; 
            }
            swap(num, i, j);
        }
        swap(num, low, j);
        // a[lo .. j-1] <= a[j] <= a[j+1 .. hi]
        return j;
    }
    void swap(int[] num, int low, int high){
        int tmp = num[low];
        num[low] = num[high];
        num[high] = tmp;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值