剑指 Offer 40. 最小的k个数

题目:

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

题解:

这道题目难度是简单,第一眼会想到sort排序,然后找出前k个数:

    public static int[] getLeastNumbers(int[] arr, int k) {
        Arrays.sort(arr);
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }

时间复杂度:O(nlogn)

看到这里,可能有人会想,这道题目真的这么简单!简直侮辱智商!事实上,并不是这样的解法,如果真的这么做,可能会被无情pass,接下来,我们想一想有没有其他的办法。

我们可以思考一下,有没有其他的排序方法可以使用,仔细一想,有很多的排序方法,比如快排,堆排序,这里先从堆排序入手。堆排序分为大顶堆和小顶堆,如果我们维护一个小顶堆,直接取出前k个数,是不是完全ok的?理论上是可以的,java的优先队列PriorityQueue就是使用小顶堆实现的,我们只需要把数组内的数值放入队列中即可(关于堆排序的具体实现,以后会说。在这里,其实并没有必要手动实现堆排序,重复造轮子是一件得不偿失的事情):

    public static int[] getLeastNumbers(int[] arr, int k) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for (int i = 0; i < arr.length; i++) {
            priorityQueue.offer(arr[i]);
        }
        int[] priority = new int[k];
        for (int i = 0; i < k; i++) {
            priority[i] = priorityQueue.poll();
        }
        return priority;
    }

 其实这种解法和前面提到的sort解法没有什么区别,时间复杂度相同,且完全没必要维护n个结点的小顶堆,只需要维护k个结点的小顶堆即可。可以先将k个结点放入堆中进行维护,形成小顶堆,然后将数组中k之后的数值放入小顶堆中,通过和根节点的值比较大小,确定此数值是否加入小顶堆中。

理论如此,但是实现起来是很麻烦的,因为小顶堆的根节点是k个数值中的最小值,当新来的数值x和根节点root的数值进行比较时,假如x<root,x可以加入小顶堆,取代root,但是因为原本root的值比x大,但比小顶堆内其他值小,那么还要找到另一个出堆的值;再假如,x>root,怎么处理呢,x比root大是一定的,但是x和小顶堆内根节点以外的值谁大谁小呢。

所以,换种思路,维护k个结点的大顶堆。大顶堆的根节点为最大值,其他节点小于根节点,所以只需要和根节点比较即可,若x>root,说明不在最小的k个数值范围之内,直接过滤掉;若x<root,x加入大顶堆,root出堆即可。

  PriorityQueue<Integer> priority = new PriorityQueue<>((o1,o2) -> {
                return o2 - o1;
            }
        );

需要重写一下比较器,将小顶堆转化为大顶堆;

 for (int i = k; i < arr.length; i++) {
            if(arr[i] < priority.peek()){
                priority.poll();
                priority.offer(arr[i]);
            }
        }

这是核心代码,即只需要处理当前值小于堆顶元素的情况就行。

完整代码:

 public static int[] getLeastNumbers(int[] arr, int k) {
        if(k > arr.length || k == 0) return new int[]{};
        PriorityQueue<Integer> priority = new PriorityQueue<>((o1,o2) -> {
                return o2 - o1;
            }
        );
        for (int i = 0; i < k; i++) {
            priority.offer(arr[i]);
        }
        for (int i = k; i < arr.length; i++) {
            if(arr[i] < priority.peek()){
                priority.poll();
                priority.offer(arr[i]);
            }
        }
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = priority.poll();
        }
        return res;

    }

这种方法的时间的时间复杂度是O(nlogk);

还有另外一种排序方法:快速排序。快排的思路就不多赘述了,直接贴代码了。

    public static int[] getLeastNumbers(int[] arr, int k) {
        if(arr == null || k == 0) return new int[0];
        quickSort(arr,0,arr.length - 1);
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    public static void quickSort(int[] arr,int i,int j){
        if(i >= j) return;
        int left = i,right = j,tmp = arr[left];//将tmp作为哨兵
        while (left < right){
            while (left < right && arr[right] >= tmp) right--;//如果right指向的值大于等于哨兵,right左移
            arr[left] = arr[right];
            while (left < right && arr[left] <= tmp) left++;//如果left指向的值小于等于哨兵,left右移
            arr[right] = arr[left];
        }
        arr[left] = tmp;
        quickSort(arr,i,left - 1);
        quickSort(arr,left + 1,j);
    }

快排需要排序,时间复杂度为O(nlogn)。这里使用快排,是为了引出快速选择算法。快排中,每一轮排序后,都是左边小右边大,比如5,7,3,4,8,第一轮排序下来就是4,3,5,7,8,显然5就是第三小的数值,同时这道题目对于k个数字的顺序是不做要求的,所以,我们只要找到i==k就可以,不需要全部排序。

    public static int[] getLeastNumbers(int[] arr, int k) {
        if(arr == null || k == 0) return new int[0];
        if(k >= arr.length) return arr;
        quickSearch(arr, 0, arr.length - 1, k );
        return Arrays.copyOf(arr, k);
    }

    public static void quickSearch(int[] arr,int i,int j,int k){
        int left = i,right = j,tmp = arr[left];//将tmp作为哨兵
        while (left < right){
            while (left < right && arr[right] >= tmp) right--;//如果right指向的值大于等于哨兵,right左移
            arr[left] = arr[right];
            while (left < right && arr[left] <= tmp) left++;//如果left指向的值小于等于哨兵,left右移
            arr[right] = arr[left];
//            swap(arr,left,right);
        }
        arr[left] = tmp;
        if(left < k)  quickSearch(arr, left + 1, j, k);
        if(left > k)  quickSearch(arr,i,left - 1,k);
        return;
    }

和快排的代码基本相同,不同的只是改变了结束条件,快排需要排序到仅剩一个数字,而快速选择只要找到i==k即可,当left<k时,在left的右边排序,当left>k时,在left左边排序,当left==k时,直接return(left和right都指向同一个地方)。

时间复杂度:O(n)

还有一种计数排序的方法,直接记录每个数字出现的次数,然后返回:

    public static int[] getLeastNumbers(int[] arr, int k) {
        if(arr == null || k == 0) return new int[0];
        int[] counter = new int[10001];
        int[] res = new int[k];
        for (int num:arr) {
            counter[num]++;
        }
        int idx = 0;
        for (int i = 0; i < counter.length; i++) {
            while (counter[i]-- > 0 && idx < k){
                res[idx++] = i;
            }
            if(idx == k) break;
        }
        return res;
    }

时间复杂度:O(n)

总结:题目简单,但是解法很多,对于排序的掌握也很重要

剑指 Offer 40. 最小的k个数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值