第二十八题:最小的k个数
题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
解析:
此题属于topK问题,还有几种其他的问法,大量数据中取出最小的k个数(或者取出最大k个数)
表面上看使用排序可以解决这个问题,但是当数据量足够大的时候,此时还能单纯的使用排序解决这个问题么?
话说老王又到了一家公司面试,又到了跟面试官日常吹bi的时刻了。
面试官:简单的介绍一下自己吧!
老王:我叫老王,隔壁老王的老王。。。。平时对写代码非常的执着。很喜欢跟计算机打交道.等等.........
面试官:我大概了解了你的一些基本情况,内心想想自己的老婆(这小子竟然叫老王,还是隔壁老王.....我必须要让他知难而退)
咱们先写一道算法题吧,题目上面描述所示。
老王:看了看题,直接就跟面试官说先给这些数字排好序,再取出前4个就是要的结果啦!!!此时老王深邃的眼神凝视着面试官。
而面试官并没有回答他,而是说先手写一下吧。于是老王迅速的写下了下面的代码:
很黄很暴力实现代码如下,(平均)时间复杂度O(NlogN):
public ArrayList<Integer> GetLeastNumbers_Solution(ArrayList<Integer> list, int k) {
ArrayList<Integer> result = new ArrayList();
if (list == null || input.length <= 0 || k<=0 || list.size()< k){
return list;
}
//排序
Collections.sort(list);
for (int i = 0; i < k; i++){
result.add(list.get(i));
}
return result;
}
面试官:看了一下老王写的代码,便说:你这种解法虽然巧妙,但是时间复杂度还是很高啊O(n^2),能不能再改善一下呢?
老王:(老子就知道你会跟我说时间复杂度太高的问题,哼!看我的另一种写法)便回答面试官:啊!假装的在思考。。。
有了,我想到该怎么解决了。说完便又写起了风骚的代码。。。。
利用堆结构,建立最大堆k个,再将k到最后一个数字添加到堆中,与堆顶值进行比较,(比堆顶值小就交换)
这里使用的是优先级队列(PriorityQueue),下面链接详细介绍大顶堆,小顶堆,堆排序相关一系列
基于堆的解法O(NlogK),空间复杂度:
public ArrayList<Integer> GetLeastNumbers_Solution2(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if (input == null || input.length <= 0 || k<=0 || input.length < k){
return list;
}
//大顶堆
Queue<Integer> heap = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
for (int i = 0;i < input.length;i++){
if (heap.size() < k){
heap.add(input[i]);
}else {
if (input[i] < heap.peek()){
heap.remove();
heap.add(input[i]);
}
}
}
while (!heap.isEmpty()){
list.add(heap.remove());
}
return list;
}
面试官:看了一下老王写的代码,便说:时间复杂度是低了,但是你使用了额外的空间堆,明显的空间换时间,如果你能不使用额外空间
那就很完美了,说完面试官很自信的低下了头,继续看着老王的简历
老王:若有所思的想了想(这人还真是个奇葩,我用空间换取了时间,现在又让我节省空间,那时间肯定要慢下来啊,于是老王就利用快排来解决)
写完下面的代码后,面试官便问老王你的代码时间复杂度是多少?
老王:T(N) = O(NlogN) 说完老王脸红了下来,自己确实节省了空间,但是时间上有花费了更多。。。。。
面试官:你知道BFPRT算法么?
BFPRT算法解决了这样一个问题:在时间复杂度O(N)内,从无序数组中找到第k小的数
1.《算法导论》
2.《程序员代码面试指南 IT企业算法与数据结构题目最优解》
这两本书中都有介绍,下面链接是2书中的详细介绍
基于快排partition的改进解法 O(NlogN):
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if (input == null || input.length <= 0 || k<=0 || input.length < k){
return list;
}
int start = 0;
int end = input.length-1;
int index = partition(input,start,end);
while (index != (k-1)){
if (index > k-1){
end = index -1;
index = partition(input,start,end);
}else {
start = index+1;
index = partition(input,start,end);
}
}
for(int i=0;i<k;++i){
list.add(input[i]);
}
return list;
}
//切分
private static int partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
//最后将最大区的最后一个数换到最大区的第一个数
swap(arr, more, r);
return less+1;
}
扩展:本题的题目是在数据量非常小的数组中找出最大或者最小的k个数,如果在100万个数字中查找最大或者最小的100个呢?
显然第一种方法和第二种方法直接就被抛弃了,个人还是推荐使用大顶堆或小顶堆去解的,毕竟使用堆空间所占用的并不是很多,时间复杂度上也是比较低的O(NlogK),用少量的空间换取时间还是可取的方案