目录

16、剑指 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]

限制:

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

二、解法

2.1、排序(面试时也许会被直接 PASS ,不推荐使用)

核心思路

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

复杂度分析

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

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

Code

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        int[] result = new int[k];

        Arrays.sort(arr);

        for (int i = 0; i < k; ++i) {
            result[i] = arr[i];
        }

        return result;
    }
}

2.2、快速排序思想

核心思路

题目只要求返回最小的 k 个数,对这 k 个数的顺序并没有要求。因此,只需要将数组划分为 最小的 k 个数 和 其他数字 两部分即可,而快速排序的哨兵划分可完成此目标。

根据快速排序原理,如果某次哨兵划分后 基准数正好是第 k+1 小的数字 ,那么此时基准数左边的所有数字便是题目所求的 最小的 k 个数 。

根据此思路,考虑在每次哨兵划分后,判断基准数在数组中的索引是否等于 k ,若 true 则直接返回此时数组的前 k 个数字即可。

复杂度分析

时间复杂度:O(N), 因为我们是要找下标为 k 的元素,第一次切分的时候需要遍历整个数组 (0 ~ n) 找到了下标是 j 的元素,假如 k 比 j 小的话,那么我们下次切分只要遍历数组 (0~k-1)的元素,反之如果 k 比 j 大的话,那下次切分只要遍历数组 (k+1~n) 的元素,总之可以看作每次调用 partition 遍历的元素数目都是上一次遍历的 1/2,因此时间复杂度是 N + N/2 + N/4 + … + N/N = 2N, 因此时间复杂度是 O(N)。

空间复杂度:O(log N),划分函数的平均递归深度为 O(log N)。

Code

class Solution {
    // 入口
    public int[] getLeastNumbers(int[] arr, int k) {
        // 边界条件
        if (k >= arr.length) return arr;
        // 递归调用
        return quickSort(arr, k, 0, arr.length - 1);
    }

    // quick_sort() 的功能不是排序整个数组,而是搜索并返回最小的 k 个数.
    private int[] quickSort(int[] arr, int k, int left, int right) {
        int i = left, j = right;
        while (i < j) {
            while (i < j && arr[j] >= arr[left]) j--;
            while (i < j && arr[i] <= arr[left]) i++;
            swap(arr, i, j);
        }
        swap(arr, i, left);
        if (i > k) return quickSort(arr, k, left, i - 1);
        if (i < k) return quickSort(arr, k, i + 1, right);
        return Arrays.copyOf(arr, k);
    }

    private void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

2.3、堆

核心思路

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

复杂度分析

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

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

Code

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        // 边界情况
        if (k == 0) {
            return new int[]{};
        }

        // 结果
        int[] result = new int[k];

        // 优先级队列
        PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            // 自定义比较器, 使之成为大顶堆.  默认为大顶堆
            public int compare(Integer num1, Integer num2) {
                return num2 - num1;
            }
        });

        // 向优先队列中插入 arr 的前 k 个元素
        for (int i = 0; i < k; ++i) {
            queue.offer(arr[i]);
        }

        // 循环处理剩下的的元素
        for (int i = k; i < arr.length; ++i) {
            // 如果队首元素(也就是队列中权值最大的那个元素)大于当前的值
            if (queue.peek() > arr[i]) {
                // 删除队首元素
                queue.poll();
                // 向优先队列中插入当前元素
                queue.offer(arr[i]);
            }
        }

        // 依次取出优先级队列的元素,构造数据
        for (int i = 0; i < k; ++i) {
            result[i] = queue.poll();
        }

        return result;
    }
}

REF

https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/zui-xiao-de-kge-shu-by-leetcode-solution/

https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/3chong-jie-fa-miao-sha-topkkuai-pai-dui-er-cha-sou/

https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/jian-zhi-offer-40-zui-xiao-de-k-ge-shu-j-9yze/