剑指offer面试题40:最小的k个数(Java 实现)

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

测试用例:

  1. 功能测试:输入的数组中有或者没有相同的数字。
  2. 边界测试:输入的 k 等于 1 或者等于数组的长度。
  3. 负面测试:k 小于1;k 大于数组的长度;输入的数组为空。

方法一:时间复杂度为 O(nlogn)

思路:直接对数组进行排序。

方法二:时间复杂度为 O(n),需要修改数组。

思路:利用快速排序中对数组进行快速划分的思想,构造一个划分函数,假如选取的分区点为index,第一次进行划分后位于index前面的元素都比位于index的元素小,位于index后面的元素都比位于index的元素大。

然后再用index与k-1作比较,如果index > k-1,说明最小的k个数都位于index的前面,利用递归的思想对index前面的元素再一次进行划分即可;如果index <= k-1,说明index前面的最小数还不够k个,同样利用递归对index后面的元素再一次划分即可。

public class test_forty {
    // 方法二
    public ArrayList<Integer> GetLeastNumbers(int[] input, int k){
        ArrayList<Integer> list = new ArrayList<>();
        int length = input.length;

        if (input == null || length == 0 || k <= 0 || k > length)return list;

        int start = 0;
        int end = length-1;
        //index表示划分函数以index为下标的元素为分区点
        int index = partition(input, start, end);

        while (index != k-1){
            //表示k个最小元素一定在index左边,需要对index的左边再一次划分
            if (index > k-1){
                end = index-1;
                index = partition(input, start, end);
            }
            //表示index左边不够k个最小元素,需要对index的右边再一次划分
            else {
                start = index+1;
                index = partition(input, start, end);
            }
        }
        //遍历找到前k个最小元素
        for (int i=0; i < k; i++){
            list.add(input[i]);
        }
        return list;
    }
    //构造划分函数(利用快排的思想对数组进行排序)
    private int partition(int[] nums, int start, int end) {
        int pivot = nums[start];  //选取数组的第一个元素作为分区点

       while (start < end){     //当区间缩小为1,说明数组已经排好序
           while (start < end && pivot <= nums[end]){  //比分区点元素值大的不用交换
               end--;
           }
           swap(nums, start, end);    //交换比分区点元素值大的元素到分区点前面

           while (start < end && pivot >= nums[start]){  //比分区点小的不用交换
               start++;
           }
           swap(nums, start, end);    //交换比分区点值大的元素到分区点后面
       }
       return start;
    }
    //交换函数
    private void swap(int[] nums, int start, int end) {
        int temp = nums[start];
        nums[start] = nums[end];
        nums[end] = temp;
    }
}

方法三:最优解。时间复杂度为O(nlogk),不用对数组进行修改,特别合适处理海量数据。

思路:

我们可以创建一个容量为k的数据容器来存储最小的k个数字,接下来我们每次从输入的n个整数中读入一个数。如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中;如果容器中已有k个数字了(即容器满了),我们就不能再插入数字了,只能去替换容器中已有的数字。替换的规则是,我们拿待插入的数字和容器中k个数字中的最大值进行比较,如果大于容器中的最大值,则抛弃这个整数,否则用这个整数去替换这个数字。

故容器满了之后,我们需要做3件事:一是在k个整数中找到最大数;二是有可能在这个容器中删除这个最大数;三是有可能会在这个容器中插入一个新数字。用二叉树实现这个容器,我们能在O(logk)时间内实现这三步操作。因此对于n个数字而言,总的时间效率就是O(nlogk)。

容器的实现用数据结构中的最大堆,因为其根结点的值永远是最大的结点值。我们用红黑树来实现我们的最大堆容器。而TreeSet类实现了红黑树的功能,它的底层是通过TreeMap实现的,TreeSet中的数据会按照插入数据自动升序排序。我们只需要将数据放入TreeSet中即可。

    //方法三:利用最大堆
    public ArrayList<Integer> GetLeastNumbers(int[] input, int k){
        ArrayList<Integer> list = new ArrayList<>();
        int length = input.length;

        if (input == null || length == 0 || k <= 0 || k > length)return list;

        //利用TreeSet容器来实现红黑树
        TreeSet<Integer> treeSet = new TreeSet<>();
        for (int i = 0; i<length; i++){
            if (treeSet.size() < k){
                treeSet.add(input[i]);
            }
            else if (input[i] < treeSet.last()){
                treeSet.remove(treeSet.last());
                treeSet.add(input[i]);
            }
        }
        //通过容器迭代器来遍历TreeSet容器
        Iterator<Integer> iterator = treeSet.iterator();
        while (iterator.hasNext()){
            list.add(iterator.next());
        }
        return list;
    }

猜你喜欢

转载自blog.csdn.net/weixin_41163113/article/details/86608796