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;
}
}