题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
解题思路参考:https://www.cnblogs.com/edisonchou/p/4799678.html
1 需要修改数据源的O(n)解法
基于快速排序中的Partition函数来解决这个问题。如果基于数组的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的k个数字就是最小的k个数字(这k个数字不一定是排序的)。
But,采用这种思路是有限制的。我们需要修改输入的数组,因为函数Partition会交换数组中数字的顺序。
具体代码如下:
//与上一道找中位数的题目写法类似,就是把中位数array.length>>1替换为k-1而已
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> leastNumbers = new ArrayList<Integer>();
while(input==null || k<=0 || k>input.length)
return leastNumbers;
int start=0;
int end=input.length-1;
int index=partition(input,start,end);
while(index!=k-1){
if(index<k-1){
start=index+1;
index=partition(input,start,end);
}else{
end=index-1;
index=partition(input,start,end);
}
}
//index=k-1的时候,直接将前k位赋值给链表即可
for(int i=0;i<k;i++){
leastNumbers.add(input[i]);
}
return leastNumbers;
}
private int partition(int[] arr, int start,int end){
int pivotKey=arr[start];
while(start<end){
while(start<end && arr[end]>=pivotKey)
end--;
swap(arr,start,end);
while(start<end && arr[start]<=pivotKey)
start++;
swap(arr,start,end);
}
return start;
}
private void swap(int[] arr, int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
2 适合处理海量数据的O(nlogk)解法
可以先创建一个大小为k的数据容器来存储最小的k个数字,接下来我们每次从输入的n个整数中读入一个数。
如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中;
如果容器中已有k个数字了,也就是容器已满,此时我们不能再插入新的数字而只能替换已有的数字。
找出这已有的k个数中的最大值,然后拿这次待插入的整数和最大值进行比较。如果待插入的值比当前已有的最大值小,则用这个数替换当前已有的最大值;如果待插入的值比当前已有的最大值还要大,那么这个数不可能是最小的k个整数之一,于是我们可以抛弃这个整数。
因此当容器满了之后,我们要做3件事情:一是在k个整数中找到最大数;二是有可能在这个容器中删除最大数;三是有可能要插入一个新的数字。如果用一个二叉树来实现这个数据容器,那么我们能在O(logk)时间内实现这三步操作。因此对于n个输入数字而言,总的时间效率就是O(nlogk)。容器可以使用最大堆或者红黑树来实现。本文根据堆排序的原理来实现。
此解法虽然要慢一点,但它有两个明显的优点:
一是没有修改输入的数据(代码中的变量data)。我们每次只是从data中读入数字,所有的写操作都是在容器leastNumbers中进行的。
二是该算法适合海量数据的输入(包括百度在内的多家公司非常喜欢与海量输入数据相关的问题)。
假设题目是要求从海量的数据中找出最小的k个数字,由于内存的大小是有限的,有可能不能把这些海量的数据一次性全部载入内存。这个时候,我们可以从辅助存储空间(比如硬盘)中每次读入一个数字,根据GetLeastNumbers的方式判断是不是需要放入容器leastNumbers即可。这种思路只要求内存能够容纳leastNumbers即可,因此它最适合的情形就是n很大并且k较小的问题。
借助于java中的PriorityQueue,我们可以很简单的实现:
import java.util.PriorityQueue;
import java.util.Comparator;
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> reslist=new ArrayList<Integer>();
if(input==null||input.length==0||k<=0||k>input.length){
return reslist;
}
PriorityQueue<Integer> maxHeap=new PriorityQueue<Integer>(k,new Comparator<Integer>(){
public int compare(Integer a,Integer b){
return b-a;
}
});
for(int i=0;i<input.length;i++){
if(i<k){
maxHeap.offer(input[i]);
}
else{
int temp=maxHeap.poll();
if(temp>input[i]){
maxHeap.offer(input[i]);
}
else{
maxHeap.offer(temp);
}
}
}
for(int i=0;i<k;i++){
reslist.add(maxHeap.poll());
}
return reslist;
}
}
如果是底层java实现的话,非常复杂,需要建立最大堆,并每次调整。
最大堆调整的思路如下:
序列对应一个完全二叉树;从最后一个分支结点(n/2)开始,到根(1)为止,依次对每个分支结点进行调整(下沉),以便形成以每个分支结点为根的堆,当最后对树根结点进行调整后,整个树就变成了一个堆。
时间:O(n)
对如图的序列,要使其成为堆,我们从最后一个分支结点(10/2),其值为72开始,依次对每个分支节点53,18,36 45进行调整(下沉).
先写出代码中的最大堆的调整方法及注释:
private void adjustHeap(int[] arr,int start,int end){
int temp=arr[start];
int child=start*2+1;
while(child<=end){
if(child+1<=end && arr[child+1]>arr[child])
child++;//遍历一遍,如果右边节点比左边大,就child++到了右边
if(arr[child]<temp)
break;//arr[start]>arr[child]那么后面也无需调整,直接break
arr[start]=arr[child];//前一句没有跳出的话,就说明是arr[start]<=arr[child],那就child上浮
start=child;
child=child*2+1;//start与child的递增方式
}
arr[start]=temp;//在所有必要的上浮结束后,最后一轮的start点被赋值为需要下沉的初始start点
}
总的最大堆方法求K最小代码如下:
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> leastNumbers = new ArrayList<Integer>();
//特殊情况处理
while(input==null || k<=0 || k>input.length)
return leastNumbers;
int[] numbers=new int[k]; //新建一个大小为k的数组,用于放最小的k个数
for(int i=0;i<k;i++)
numbers[i]=input[i];//先放入前k个数
//将由底及上的将前k个值构造成最大堆形式,这时numbers[0]存储前k里最大值
for(int i=k/2-1;i>=0;i--){
adjustHeap(numbers,i,k-1);
}
for(int i=k;i<input.length;i++){
if(input[i]<numbers[0]){ //存在更小的数字时,把numbers[0]替换并调整堆
numbers[0]=input[i];
adjustHeap(numbers,0,k-1);//重新调整最大堆
}
}
for(int n:numbers)
leastNumbers.add(n);
return leastNumbers;
}
//最大堆的调整方法
private void adjustHeap(int[] arr,int start,int end){
int temp=arr[start];
int child=start*2+1;
while(child<=end){
if(child+1<=end && arr[child+1]>arr[child])
child++;
if(arr[child]<temp)
break;
arr[start]=arr[child];
start=child;
child=child*2+1;
}
arr[start]=temp;
}
}