• leetcode 40最小K个数—大顶堆&快排(全网最通俗易懂java版)


    一、堆:
    总的来说如果求最小值就要构建大顶堆,反之小。
    大小顶堆原则:每个节点都比其左右节点大或者小(也就是说他的约束只在三个节点,但原则对全局节点限制),不能保证高层数据一定大于底层数据(也有可能出现等于),同层的数据左右没有一定的大小顺序,以下两种均为正确的大顶堆。正是由于只能保证高层数据大于等于底层数据,所以才需要对顶堆进行初始化。
    eg1:
        3
      3   2
    1  1   0   2
    eg2:
        3
      2  1
    2  1  0  1  
    虽然叶子节点2大于高层的1但是仍然只能在底层,这是根据数据的先来先进,后来后进,一旦满足顶堆原则则不再更新。
    堆的时空复杂度:
    • 时间复杂度为O(k+(n-k)*logk)= O(logK);  
    • k个元素的最大堆的空间复杂度为O(k)
    排版可能混乱所以文后代码截图
    思路:
    • 初始化K个数的大顶堆
    • 对K+1等之后的数字要进行更新大顶堆的操作,由于大顶堆表示顶部的元素是最大的,所以如果后面的数小于大顶堆最大值才能有资格更新大顶堆(即删除大顶堆中最大的元素),从而使得大顶堆的顶部元素变小;如果大于大顶堆中的最大值也就是顶部元素,说明该元素没有资格成为数组中最小值的候选人。
    • 整个数组遍历完之后将大顶堆中的每个元素都存储到最终的输出结果。

    需要注意的点:

    • if语句中判断范围的放在前面,后面再进行数组索引 eg: ✅if(right<lengthh&&heap[left]<heap[right]) ❌ if(heap[left]<heap[right]&&right<lengthh)
    • 索引的时候一定要看准再写,不然后面调试真的一时半会儿找不出来问题 eg:易错点if(arr[i]<heap[0]){ heap[0]=arr[i]; ...别写成if(arr[k]<heap[0]){ heap[0]=arr[k]; ...
    • 注意{}的含义是代码块。
      在代码块内侧定义的变量,作用域范围在代码块。这点与C不同,所以java里面可以在循环体内不断定义同名变量,但为了养成良好的习惯,尽量在循环体外定义,在循环体内初始化数值。
    class Solution {
        public int[] getLeastNumbers(int[] arr, int k) {
            //创建初始化K个元素的大顶推
            int[] arr1=new int[k];
            if(arr==null||arr.length==0||k==0||k>arr.length) return arr1;
            int[] heap=new int[k];
            for(int i=0;i<k;i++){
                createh(heap,arr,i);
            }
            //后面出现的数进行更新堆的判断
            for (int i=k;i<arr.length;i++){
                if(arr[i]<heap[0]){
                    heap[0]=arr[i];
                    adjh(heap);
                }   
            }
            //读取大顶堆中的K个元素
            for(int i=0;i<k;i++){
                arr1[i]=heap[i];
            }
            return arr1;
        }
        public void createh(int[] heap,int[] arr,int k){
            heap[k]=arr[k];
            //只要没到根元素就不断向上更新,直到满足大顶堆原则
            while(k!=0){
                int par=(k-1)/2;
                if(heap[par]<heap[k]) {
                    swap(heap,par,k);
                    k=par;
                }
                else break;//已满足大顶堆原则
            }
        }
        public void adjh(int[] heap){
            int lengthh=heap.length;
            int par=0;
            int left=2*par+1;
            int right=left+1;
            int maxv=0;
            while(left<lengthh){//因为我们放数据是从左往右放,所以左边不越界是底线
                maxv=left;
                //如果右节点也没有越界,则选取左右最大的值对应的索引用于后面更新比较
                if(right<lengthh&&heap[left]<heap[right]) maxv=right;
                if(heap[par]<heap[maxv]){
                    swap(heap,par,maxv);               
                }else{
                    break;
                }
                par=maxv;
                left=2*par+1;
                right=left+1;
            }
        }
        public void swap(int[] arr,int a,int b){//注意这里的引用参数传递,类似C
            int tmp=arr[a];
            arr[a]=arr[b];
            arr[b]=tmp;
        }
    }
     
    二、单方向快排
     
    首先回顾一下快排思想:
    1. 1、选择第一个元素作为比对数
    2. 2、i指针从前往后遍历,j指针从后往前遍历
    3. 3、如果i没有到数组的末尾而且索引i处的数值大于比对数,则i不动去遍历j
    4. 4、如果j没有到数组的开头而且索引j处的数值小于比对数,则将索引i与j两处所指向的数值进行交换,接下来继续回到步骤3不懂移动i和j
    5. 5、当i>=j则终止循环,将此时j处的数值与比对数进行位置变换,注意此时j处的数值一定小于等于比对数,因为i=j时j处的数值与比对数相同,交换没有关系,i>j时,此时两个指针一定是相互交错的,遍历的一定是已经处理完的数值(也就是前部分都小于等于比对数,后部分都大于等于比对数)。
    6. 6、然后分别对前部分和后部分递归调用快排算法。
    那么这道题其实就相当于简化了快排,因为只要最小的K个,也就是不需要对全局进行排序,只需要将最小的K个取出来就行了!由此思路如下:
    partition 操作是原地进行的,需要 O(n) 的时间,因为要将N长度的数组与对比数进行比较。
    1. 核心思想是如果左侧刚好为K个,则一次part就是结果
    2. 左侧不够K个则对右边排序选m-k个补充到结果
    3. 左侧大于K个则对左侧进一步排序直到左侧为K个。
    代码如下:

    class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
    if (k == 0 || arr.length == 0) {
    return new int[0];
    }
    // 最后一个参数表示我们要找的是下标为k-1的数
    return quickSearch(arr, 0, arr.length - 1, k - 1);
    }

    private int[] quickSearch(int[] nums, int lo, int hi, int k) {
    // 每快排切分1次,找到排序后下标为j的元素,如果j恰好等于k就返回j以及j左边所有的数;
    int j = partition(nums, lo, hi);
    if (j == k) {
    return Arrays.copyOf(nums, j + 1);
    }
    // 否则根据下标j与k的大小关系来决定继续切分左段还是右段。
    return j > k? quickSearch(nums, lo, j - 1, k): quickSearch(nums, j + 1, hi, k);
    }

    // 快排切分,返回下标j,使得比nums[j]小的数都在j的左边,比nums[j]大的数都在j的右边。
    private int partition(int[] nums, int lo, int hi) {
    int v = nums[lo];
    int i = lo, j = hi + 1;
    while (true) {
    while (++i <= hi && nums[i] < v);
    while (--j >= lo && nums[j] > v);
    if (i >= j) {
    break;
    }
    int t = nums[j];
    nums[j] = nums[i];
    nums[i] = t;
    }
    nums[lo] = nums[j];
    nums[j] = v;
    return j;
    }
    }

    代码截图:

  • 相关阅读:
    2013-06-28,“万能数据库查询分析器”在中关村本月数据库类下载排行中重返前10位
    Oracle 存储过程
    强化学习精要:核心算法与TensorFlow实现
    深入理解TensorFlow:架构设计与实现原理
    Vue.js实战
    TensorFlow机器学习实战指南
    深入浅出React和Redux
    Flutter技术入门与实战
    TensorFlow:实战Google深度学习框架
    深度学习:一起玩转TensorLayer
  • 原文地址:https://www.cnblogs.com/sjh-dora/p/12830836.html
Copyright © 2020-2023  润新知