# 剑指 Offer 40. 最小的k个数
Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
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 <= 100000 <= arr[i] <= 10000
2、思路🧠
方法一:快排排序遍历前k个
利用快速排序O(NlogN) 的时间复杂度进行排序,取数组中前k个元素,即可得到结果。
方法二:堆
使用JDK8自带的堆 PriorityQueue 默认是小根堆,需要重新compare 方法定义为大根堆。
大根堆声明:
PriorityQueue<Integer> pq = new PriorityQueue<>(arr.length, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return (o2 - o1) > 0 ? 1 : -1;
}
});
-
特判:
arr == null || k == 0直接返回空的数组。 -
将前k个数组中的数字依次进入堆中
- 当堆中的数字个数大于k个时,判断堆顶元素数字与下一个待进入堆元素的数字的大小
- 如果堆顶元素数字比下一个待进入堆元素的数字大,则将其元素弹出,将新的元素入堆,在这过程中一直保持前k个元素为目前扫描到的最小元素。
-
直到扫描到最后一个元素位置,将堆中的前k个元素依次弹出,得到答案。
方法三:优化快速排序
首先,需要理解和明白快速排序的根本思想,才能对其进行灵活变换。
代码和快排代码一样,不过多了几行判断,所以对于根本思想来说,非常重要
if (i > k) quick_sort(q, l, j, k)i 对应的是每次二分的左下标,如果i比k大的话,说明此时i在k的左边,此时我们只需要将数组的前j进行排序,就结束else quick_sort(q,j + 1, r, k)如果k比i大的话,说明此时i在k的右边,说明此时需要的不够,需要继续对右边的进行二分,直到找到我们需要的再次进行if操作。
废话少说 ~~~~~ 上代码!
3、代码👨💻
第一次commit AC
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
quickSort(arr, 0, arr.length - 1);
int res [] = new int[k];
for(int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
public static void quickSort(int arr[], int l, int r) {
if (l >= r) return ;
int i = l - 1;
int j = r + 1;
int mid = arr[(l + r) >> 1];
while(i < j) {
do i++; while(arr[i] < mid);
do j--; while(arr[j] > mid);
if(i < j) swap(arr, i, j);
}
quickSort(arr, l, j);
quickSort(arr, j + 1, r);
}
public static void swap(int arr[], int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
时间复杂度:O(NlogN) N为数组 arr 的长度
空间复杂度:O(N) 快速排序的递归深度平均为 O(logN) ,最差情况为 O(N)。
第二次commit AC
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if(arr == null || k == 0) {
return new int [0];
}
PriorityQueue<Integer> pq = new PriorityQueue<>(arr.length, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return (o2 - o1) > 0 ? 1 : -1;
}
});
for(int num : arr) {
if(pq.size() < k) {
pq.offer(num);
}else if(pq.peek() > num) {
pq.poll();
pq.offer(num);
}
}
int res[] = new int [k];
for(int i = 0; i < k; i++) {
res[i] = pq.poll();
}
return res;
}
}
时间复杂度:O(N) N为堆的大小 K
空间复杂度:O(N log N) 入堆和出堆操作的时间复杂度均为 O(logk),每个元素都需要进行一次入堆操作。
第三次commit AC
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
quick_sort(arr, 0, arr.length - 1, k);
int res [] = new int[k];
for(int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
public static void quick_sort(int q[], int l, int r, int k) {
if (l >= r) return ;
int i = l - 1;
int j = r + 1;
int mid = q[(l + r) >> 1];
while(i < j){
do i++; while (q[i] < mid);
do j--; while (q[j] > mid);
if(i < j) swap(q, i, j);
}
if (i > k) quick_sort(q, l, j, k);
else quick_sort(q,j + 1, r, k);
}
public static void swap(int arr[], int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
时间复杂度:O(N) N为数组 arr 的长度,每次二分一共有N + N/2 + N/4 + …… = 2N - 1 ,即时间复杂度为 O(N)。
空间复杂度:O(log N) 划分函数的平均递归深度为 O(log N)。
4、总结
该题目数组,底层的数据结构要有了解,其次还要熟悉快排的底层思想,如何进行每一次的操作。熟悉堆的结构,在JDK8如果使用大根堆、小根堆多要熟练掌握。
快排模板:
public static void quick_sort(int q[], int l, int r, int k) {
if (l >= r) return ;
int i = l - 1;
int j = r + 1;
int mid = q[(l + r) >> 1];
while(i < j){
do i++; while (q[i] < mid);
do j--; while (q[j] > mid);
if(i < j) swap(q, i, j);
}
quick_sort(q, l, j, k);
quick_sort(q,j + 1, r, k);
}
public static void swap(int arr[], int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
❤️来自专栏《LeetCode基础算法题》欢迎订阅❤️
厂长写博客目的初衷很简单,希望大家在学习的过程中少走弯路,多学一些东西,对自己有帮助的留下你的赞赞👍或者关注➕都是对我最大的支持,你的关注和点赞给厂长每天更文的动力。
对文章其中一部分不理解,都可以评论区回复我,我们来一起讨论,共同学习,一起进步!
原题链接:剑指 Offer 40. 最小的k个数