题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。


最容易想到的肯定是用一个容量为4的集合记录当前的最小4个数字,并且不断的维护。这个维护过程就是一个排序过程,因此本题可以复习一些排序方式。
  这个集合可以用数组,堆以及其他数据结构。因为每次加入最小值时,都会替换出集合中的最大,所以用一个大根堆来维护,比较合适。当然也可以用一些其他的排序算法。所以第1个思路就是多种排序算法中任选一个,这里选择的堆排序。
  第2个思路就是,借鉴快排partition()方法,将整个数组分成两部分,左边是最小的4个,右边是比较大的。时间复杂度为O(n),但这种方法修改了原数组的顺序!


写法1:这里的写法是,每次新插入数,都重写建堆。这和建立好堆后,每次调整顶点的写法不同。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        while(input == null || k <= 0 || k > input.length)
            return list ;
        int[] arr = new int[k];  //用于放最小的k个数
        for(int i=0;i<k;i++)
            arr[i] = input[i];//先放入前k个数
        for(int i = k/2-1; i >= 0; i--){
            adjustHeap(arr,i,k-1);//将数组调整成最大堆形式
        }
        for(int i = k;i < input.length; i++){
            if(input[i]<arr[0]){ //存在更小的数字时
                arr[0]=input[i];
                adjustHeap(arr,0,k-1);//重新调整最大堆
            }
        }
        for (int i = 0; i < arr.length; i++) {
            list.add(arr[i]);
        }
        return list ;
    }
    //给一个数组,以及该数组的某个范围,将该范围构造成堆,start为最小值
     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;
    }
}

写法2:借鉴了快排的Partition,但是并不是快速排序。有一个基准,这个基准点的左边都是较小的,右边都是较大值,只要这个基准点的索引为3,就完成了。时间复杂度为O(n)。
  具体的运行流程是,首先任取一个位置的数,以其为基准,分成两边,那么左边的数肯定小于该基准点,但是不可能取的基准点那么准,所以左边小于基准点的数的个数可能大于4,也可能小于4,因此需要近一步的划分。
  如(9,5,1,3,8,7,13,11,12)以9为基准划分后,为(5,1,3,8,7,9,13,11,12),左边小于9的有5个数(9的索引不为3),仍需划分,再对(5,1,3,8,7)进行划分此时以5为划分,则形成(1,3,5,8,7),此时左边小5的只有2个(5的索引不为3),因此对右边进行划分,右边划分(7,8)。此时拿到的值为7的索引,整体上来说,原数组变成了(1,3,5,7,8,9,13,11,12)而7这个地方的索引是3,也就是说,左边的4个较小的数已经拿到了。
  显然这个方法不太容易想到。比排序那一类方法要难想。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        while(input == null || k<=0 || k > input.length)
            return list;
        int start = 0;//数组起始位置
        int end = input.length-1;//数组最后位置
        int index = partition(input,start,end);//任取一个数字,以其为基准,进行比较和划分
//那么index就拿到了一个基准点的位置(数组以及根据这个基准点进行了划分)
        while(index != k-1){//需要判断这个基准点的索引是否为3,为3说明左边有4个数了(完成了)
            if(index < k-1){//小于,说明左边不够4个数,因此还需要在右边部分再进行划分
                start = index+1;
                index = partition(input,start,end);
            }else{
                end = index-1;//大于,说明左边划分的数多余4个,因此对左边要进一步划分
                index = partition(input,start,end);
            }
        }
        for(int i = 0;i < k;i++){
            list.add(input[i]);
        }
        return list;
    }

     private int partition(int[] arr, int start,int end){
        int div = arr[start];//取一个基准点(这里为提升性能,应该随机取一个基准点)
        while(start < end){
            while(start < end && arr[end] >= div){
               end--;
             }       
           //交换位置,使大于div的值位于数组右边  
             int temp = arr[start];
             arr[start] = arr[end];
             arr[end] = temp;
             while(start < end && arr[start] <= div){
                start++;
             }   
           //交换位置,使小于div的值位于数组左边            
             temp = arr[start];
             arr[start] = arr[end];
             arr[end] = temp;
        }
        return start;
    }
}