剑指Offer——最小的K个数(Java)

https://www.nowcoder.com/questionTerminal/6a296eb82cf844ca8539b57c23e6e9bf

题目描述

输入整数数组 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]

解题思路

方法1:

对于该题最简单的思路就是把输入的n个整数进行排序,排序之后位于最前面的K个数就是最小的K个数。

时间复杂度为:O(n*log(n))——排序的时间复杂度

空间复杂度:O(log(n))

方法2:

可以借鉴快速排序中Partition的思想来解决。——只有当我们可以修改输入的数组时,该方法可用

如果基于数组的第K个数字来进行调整,使得比第K个数字小的所有数字都位于数组的左侧,比第K个数字大的所有数字都位于数组的右边。

经过调整之后,位于数组左边的K个数字就是最小的K 个数字。

index > k-1 :表示k个最小的数字一定在index的左边;只需要对index的左边进行划分即可

index  < k-1:index及index左边数字还没能满足k个数字,需要继续对k右边进行划分

时间复杂度:O(n),  最坏情况下是O(n^2)

方法3:大根堆实现

使用一个容量为K的容器来存储最小的K个数字。

每次从输入的n个数中读入一个数:

如果容器中已有的数字少于K个,直接将读入的整数放入容器中

如果容器中已有K个数自,就不能再插入数字。

拿待插入的数字和容器中K个数字中的最大值进行比较,

如果该数字大于容器中的最大值,抛弃该数字

否则,交换该数与容器中的最大值

容器的实现应该用数据结构中的大根堆其根结点的值永远是最大的结点值

  • 优先级队列(PriorityQueue)中维护的是堆,使用时传入比较器,让数据从大到小排列,堆顶的元素为最大的。
  • 红黑树来实现最大堆容器。TreeSet类实现了红黑树的功能,它的底层是通过TreeMap实现的,TreeSet中的数据会按照插入数据自动升序排序。我们只需要将数据放入TreeSet中,其就会为我们实现排序。

时间复杂度:O(nlogk)

空间复杂度:O(k),因为大根堆里最多 k 个数。

代码实现:

方法1:

public class Solution2 {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list = new ArrayList<>();
//一定要注意将特殊情况考虑到
        if (input == null || input.length == 0 || input.length < k){
            return list;
        }
        Arrays.sort(input);//时间复杂度是O(n*log(n))
        for (int i = 0; i < k; i++) {
            list.add(input[i]);
        }
        return list;
    }

    public static void main(String[] args) {
        //输入的数组中没有相同的数字
        int[] array = {4,5,1,6,2,7,3,8};
        Solution2 solution2  =new Solution2();
        //边界值测试
        System.out.println(solution2.GetLeastNumbers_Solution(array, 1));//k=1
        System.out.println(solution2.GetLeastNumbers_Solution(array, 8));//k=array.length
        //特殊值测试:k<1,k>array.length()
        System.out.println(solution2.GetLeastNumbers_Solution(array, 0));
        System.out.println(solution2.GetLeastNumbers_Solution(array, 10));


        //输入的数字中相同的数字
        int[] array1 = {1,2,3,4,5,3,2};
        System.out.println(solution2.GetLeastNumbers_Solution(array1, 1));//k=1
        System.out.println(solution2.GetLeastNumbers_Solution(array1, array1.length));//k=array.length
        //特殊值测试:
        System.out.println(solution2.GetLeastNumbers_Solution(array1, 0));//k<1
        System.out.println(solution2.GetLeastNumbers_Solution(array1, 10));//k>array.length()

    }
}

方法2:

/**
 * 基于Partition函数的解法
 */
public class Solution {
    public  static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        //时间复杂度:O(n)
        ArrayList<Integer> list = new ArrayList<>();
        if (input == null  || input.length ==0 || input.length < k){
            return list;
        }
        int n = input.length;
        int start =0;
        int end = input.length;
        int index = Partition(input,n,start,end);
        while (index != k-1){
            if (index > k-1){
                end = index-1;
                index = Partition(input,n,start,end);
            }else {
                start  =index+1;
                index = Partition(input,n,start,end);
            }
        }

        for (int i=0;i < k;i++){
            list.add(input[i]);
        }
        return list;
    }

    private static int Partition(int[] input, int n, int start, int end) {
        //挖坑法
        int leftIndex= start;
        int rightIndex = end-1;
        int key = input[leftIndex];
        while (leftIndex < rightIndex){
            while (leftIndex < rightIndex && input[rightIndex] >= key){
                rightIndex--;
            }
            input[leftIndex] = input[rightIndex];
            while (leftIndex < rightIndex && input[leftIndex] <= key){
                leftIndex++;
            }
            input[rightIndex] = input[leftIndex];
        }
        input[leftIndex] = key;
        return leftIndex;
    }

    public static void main(String[] args) {
        int[] array = {1,2,3,4,3,5,6};
        System.out.println(GetLeastNumbers_Solution(array,4));
    }
}

方法3:

/**
 * 基于堆或者红黑树的解法
 */
public class Solution3_Best {
    public static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list = new ArrayList<>();
        
        if (input == null || input.length == 0 || input.length<k|| k==0){
            return list;
        }
        /**
         * PriorityQueue实现
         */
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;//由大到小进行排序
            }
        });
        for (int i = 0; i < input.length; i++) {
            if (priorityQueue.size()< k){
                priorityQueue.offer(input[i]);
            }else if(input[i] < priorityQueue.peek()){//和堆顶的元素进行比较
                priorityQueue.poll();
                priorityQueue.offer(input[i]);
            }
        }
        for (int i =0;i<k;i++){
            list.add(priorityQueue.poll());
        }
        /**
         * TreeSet实现
         */
        TreeSet<Integer> kSet = new TreeSet<>();
        for (int i=0;i<input.length;i++){
            if (kSet.size() < k){
                kSet.add(input[i]);
            }else if(input[i] < kSet.last()){//和kSet中的最后一个元素进行比较
                    kSet.remove(kSet.last());
                    kSet.add(input[i]);
            }
        }
        for (Integer e :kSet){
            list.add(e);
        }
        
        
        return list;
    }
}

比较

由于第一种排序的方法比较简单,一般面试还是会让写出更优的算法,所以需要着重关注后面两种解决方案。

后面两种方法的特点比较:

  1. 基于Partiton的解法的平均时间复杂度是O(n),但是它有明显的限制,会修改输入的数组。
  2. 基于堆或红黑树的解法,有两个明显的优点:
    1. 没有修改输入的数据
    2. 该算法适合海量数据的输入。

假设要求从海量的数据中找出最小的k个数,由于内存的大小是有限的,不可能把这些数据一次性全部载入内存。此时可以从辅存中每次读入一个数字,判断能不能放入容器中。

这种方式最适合的情形就是n很大并且k较小的问题。

 基于Partition的解法基于堆或红黑树的解法
时间复杂度O(n)O(nlogk)

是否需要修改数组

是否适用于海量的数据
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值