LeetCode刷题笔记01-最小的K个数

LeetCode刷题笔记01-最小的K个数

1、题目分析

  • 来自于剑指 Offer 40. 最小的k个数
  • 如果熟悉优先队列的话,本题首先想到的可能是用Java中的优先队列这一数据结构来做。
  • 另外,如果熟悉快排,那么可以探索出另外一种思路,由于题目仅要求找到最小的K个数,并未要求这K个数的顺序,因此,如果我们通过快排的partition函数找到第K小的数的位置,那么它连同他前面的K-1个数就是最小的K个数。
  • 此外,面试中也可能会让我们手动写一个大根堆来解这道题。
  • 下面分别给出以上三种解法。

2、优先队列PriorityQueue

  • 在这里需要注意优先队列的实现,尤其是比较器的写法,由于是求最小的K个,因此我们将优先队列实现为大根堆,(注意如果求的是最大的K个数则需要小根堆)而优先队列默认是小根堆的:

    标准的写法,重写比较器的compare方法:

    Queue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2-o1;
        }
    });
    

    利用lambda表达式(Java8新特性):

    Queue<Integer> pq = new PriorityQueue<>((o1, o2) -> o2-o1);
    
  • 代码思路即为若队列大小小于K,就往里加数pq.offer(num),若等于K,则判断接下来要加入的数num和队头的元素(队列中最大的元素)pq.peek()的大小关系,若num<pq.peek(),则将队头元素出队,num入队。

    class Solution {
        public int[] getLeastNumbers(int[] arr, int k) {
            int n = arr.length;
            if (k == 0 || n == 0) {
                return new int[0];
            }
            Queue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>(){
                @Override
                public int compare(Integer o1, Integer o2){
                    return o2 - o1;
                }
            });
            for(int num:arr){
                if(pq.size()<k){
                    pq.offer(num);
                }else if (pq.peek()>num){
                    pq.poll();
                    pq.offer(num);
                }
            }
            int[] res = new int[k];
            for(int i=0;i<k;i++){
                res[i] = pq.poll();
            }
            return res;
        }
    }
    
  • 时间复杂度分析
    在这里插入图片描述

    要遍历N个数,每次需要维护容量为K的大根堆,因此时间复杂度为 O(NlogK)

3、快排partition()

  • 由于题目只让求最小的K个数而不规定顺序如何,因此,基于上一种优先队列的思路其实是获得了本不用获得的信息,因此必不可能是最优解法,而快排的思路在于将比某个数小的元素放在其左边,将比它大的放在右边,如果能直接通过快排切分排好第 K 小的数(下标为 K-1),那么它左边的数就是比它小的另外 K-1 个数。

  • 在代码实现方面,我们每次的pivot元素的选择采取随机的形式以避免极端的测试用例。

    class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
            randomizedSelected(arr, 0, arr.length - 1, k);
            int[] vec = new int[k];
            for (int i = 0; i < k; ++i) {
                vec[i] = arr[i];
            }
            return vec;
        }
    
        private void randomizedSelected(int[] arr, int l, int r, int k) {
            if (l >= r) {
                return;
            }
            int pos = randomizedPartition(arr, l, r);
            //表示找到的是[l,r]区间中第num小的元素
            int num = pos - l + 1;
            if (k == num) {
                return;
            } else if (k < num) {
                //找大了,再去pos前面找
                randomizedSelected(arr, l, pos - 1, k);
            } else {
                //找小了,再去pos后面找第(k-num)小的元素,比如[2,1,3,4,5],要找第4小的,但第一次找了个第2小的,即数组变为[1,2,3,4,5],则需要去[3,4,5]子数组再去找第(4-2)小的元素。
                randomizedSelected(arr, pos + 1, r, k - num);
            }
        }
    
        private int randomizedPartition(int[] nums, int l, int r) {//首先随机处理,new Random().nextInt(r - l + 1) + l 即为在[l,r]闭区间随机选择一个数与边界r位置元素做交换。
            int i = new Random().nextInt(r - l + 1) + l;
            swap(nums, r, i);
            return partition(nums, l, r);
        }
    	
        //返回nums[r]元素所该在的位置,经典的快排partition方法实现
        private int partition(int[] nums, int l, int r) {
            int pivot = nums[r];
            int i = l - 1;
            for (int j = l; j <= r - 1; ++j) {
                if (nums[j] <= pivot) {
                    i++;
                    swap(nums, i, j);
                }
            }
            //循环结束后,其他元素中小于等于nums[r]的元素都被移动到了[l,i]的位置,因此i++就是nums[r]该在的位置下标
            i++;
            swap(nums, i, r);
            return i;
        }
    
        private void swap(int[] nums, int i, int j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
    
    }
    
  • 时间复杂度分析

在这里插入图片描述

第一次调用partition方法的时候需要遍历整个数组 (0 ~ n) 找到了下标是pos的元素,而如果pos - l + 1并非是k,那么要么去前面找,要么去后面找,不妨设每次近似都是对半分,那么时间复杂度可以表示为 O(N+N/2+N/4+...+N/N)=O(N),因此利用快排的时间复杂度是 O(N)

4、自建大顶堆

  • 使用大顶堆的代码思路跟使用优先队列是相同的,因为它们就是一个东西,重要的是如何建堆并维护堆,堆其实就是利用完全二叉树的结构来维护的一维数组,因此我们不必建树,直接用一个数组表示堆即可。

  • 建堆的过程即为从最后一个非叶子结点开始,判断其与左右孩子结点值的大小关系,然后做交换,在交换过后,还要保证做交换的孩子所在的子树满不满足大顶堆,因此是一个 从下往上的遍历处理,每次处理则是一个从上往下的递归过程。

  • 代码如下:

    class Solution {
        public int[] getLeastNumbers(int[] arr, int k) {
            if(arr == null || arr.length == 0 || k == 0) return new int[0];
            int[] result = new int[k];
            int i = 0;
            while(i < k){
                result[i] = arr[i];
                i++;
            }
            buildHeap(result);	//建堆
            while(i < arr.length){
                if(arr[i] < result[0]){
                    result[0] = arr[i];
                    //如果arr[i] < result[0],则说明堆顶元素,即result[0]肯定不是前K小的,将原来的值覆盖掉,相当于移除了堆顶元素,然后从堆顶即下标0开始调整大顶堆。
                    adjust(result, 0);
                }
                i++;
            }
            return result;
        }
        //从最后一个非叶子结点的位置(result.length/2-1)开始
        public void buildHeap(int[] result){
            for(int i = result.length / 2 - 1; i >= 0; i--){
                adjust(result, i);
            }
        }
        public void adjust(int[] result, int i){
            int maxIndex = i;
            int len = result.length;
            //判断其左右孩子结点的值是否比根结点要大,若大,则交换,比如[2,1,3],换为[3,1,2],并继续判断'2'和它的子树满不满足大顶堆。
            if(2*i+1 < len && result[2*i+1] > result[maxIndex]) 
                maxIndex = 2*i+1;
            if(2*i+2 < len && result[2*i+2] > result[maxIndex]) 
                maxIndex = 2*i+2;
            if(maxIndex != i){
                swap(result, maxIndex, i);
                adjust(result, maxIndex);
            }
        }
        public void swap(int[] result, int i, int j){
            int temp = result[i];
            result[i] = result[j];
            result[j] = temp;
        }
    }
    

5、参考资料

  1. 堆排序(大顶堆、小顶堆)----C语
  2. 4种解法秒杀TopK(快排/堆/二叉搜索树/计数排序)❤️
  3. 最小的k个数
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值