当前位置:   article > 正文

[排序 手写堆 快排模板] 215. 数组中的第K个最大元素(堆排序、快速排序)_数组第 k 大的数堆排序

数组第 k 大的数堆排序

215. 数组中的第K个最大元素

题目链接:https://leetcode-cn.com/problems/kth-largest-element-in-an-array/

相同题目:剑指offer 40.


分类

  • 堆排序(PriorityQueue、手写堆
  • 快速排序(利用快排的partition分治)

在这里插入图片描述

题目分析

这题考的就是排序算法的应用,有多种实现方法,面试的话可以有很多考点。

思路1:Arrays.sort

Arrays.sort是升序排列,要的是第k大的元素,即为nums[n-k]

class Solution {
    public int findKthLargest(int[] nums, int k) {
        Arrays.sort(nums);
        return nums[nums.length-k];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 时间复杂度:O(NlogN)
  • 空间复杂度:O(logN)

思路2:堆排序(适合大数据量)

堆排序可以分为两种方法:

  • 方法1:小顶堆存放数组的所有元素,然后弹出n - k 次顶部节点,第n - k次弹出的顶部节点就是整个数组的第k大元素;
  • 方法2:小顶堆存放数组的前 k个元素,然后后续元素只有 > 顶部节点才加入堆中,当所有节点都处理完毕时,顶部节点就是第k大元素。

实现上分为:使用java提供的PriorityQueue作为顶堆;自己手写顶堆。

因为方法2可以避免堆过大,维持堆的空间复杂度为O(K),所以我们分别用PriorityQueue和用自己写的顶堆实现方法2。

为了加深对手写堆的理解,我们用手写堆分别实现方法1和方法2。

使用PriorityQueue(小顶堆(大小=k) + 大于顶部的入堆)

使用一个优先级队列PriorityQueue,但设置一个计数器以确保队列只保存k个元素。这样能降低空间复杂度。

优先级队列默认是小顶堆,我们先将前k个元素直接入队,此时队列里保存的nums的前k个元素,顶部保存的是这k个元素里的最小值。

然后,再取剩余的元素入队,剩余的元素每入队一个,就和顶部比较,如果新元素 > 顶部,则将顶部弹出,然后新元素入队。

剩余的元素都按这个操作处理。

最后,所有元素都处理完毕,此时堆中保存的就是nums数组最大的前K个元素,堆的顶部就是第k大的元素。

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        //将nums数组前k个元素入队
        for(int i = 0; i < k; i++){
            queue.offer(nums[i]);
        }
        //将剩余元素入队
        for(int i = k; i < nums.length; i++){
            //如果当前元素 > 顶部,就将顶部弹出,当前元素入队
            int top = queue.peek();
            if(nums[i] > top){
                queue.poll();
                queue.offer(nums[i]);
            }
        }
        //此时堆顶部就是第k大的元素
        return queue.peek();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 时间复杂度:O(NlogN),共N个元素入队,最差情况下每个元素入队都要做一次堆化,一次堆化是O(logN),所以整体时间复杂度为O(NlogN)
  • 空间复杂度:因为规定了队列最多只能保存k个元素,所以O(K)
手写堆(小顶堆(大小=n) + 全部元素入堆 + 弹出n-k次顶部)

该方法对整个数组建小顶堆,每次弹出的堆顶是当前堆内的最小元素,而第k大元素就第n-k小元素,所以第n-k次弹出的堆顶就是第k大元素。

具体分析见:

class Solution {
	//堆排序主函数
    public int findKthLargest(int[] nums, int k) {
        int heapSize = nums.length;//初始堆大小为数组大小
        //在数组上建堆
        buildMinHeap(nums, heapSize);
        //将顶部删除+最后一个叶节点覆盖根部 + 重新对根节点做堆化(寻找第n-k个弹出的元素)
        for(int i = 0; i < nums.length - k; i++){
            nums[0] = nums[heapSize - 1];
            heapSize--;
            minHeapify(nums, 0, heapSize);
        }
        //最后根部元素就是第k大元素
        return nums[0];
    }
    //建堆(小顶堆)
    public void buildMinHeap(int[] nums, int heapSize){
        int n = nums.length;
        //从倒数第1个非叶节点开始直到根节点
        for(int i = (n - 1) / 2; i >= 0; i--){
            //每个非叶节点都执行一次调整函数
            minHeapify(nums, i, heapSize);//传递数组、非叶节点的下标、堆的大小(为什么要传递堆的大小?见分析)
        }
    }
    //小顶堆的调整函数(递归实现)
    public void minHeapify(int[] nums, int parent, int heapSize){
        int lchild = parent * 2 + 1, rchild = parent * 2 + 2;
        int min = parent;//存放左右孩子中的较小节点下标
        //先拿父节点和左孩子比较,取较小值的下标(孩子下标>=heapSize表示父节点没有该孩子节点)
        if(lchild < heapSize && nums[lchild] < nums[min]){
            min = lchild;
        }
        //再拿前面得到的较小点和右孩子比较,取较小值的下标
        if(rchild < heapSize && nums[rchild] < nums[min]){
            min = rchild;
        }
        //经过前面的if分支,如果当前节点的孩子都大于它,或当前节点为叶子节点,则min = parent,不会进入下面的分支而是退出该函数,表示完成这次调整
        if(min != parent){
            //交换min下标和parent下标
            swap(nums, parent, min);
            //这只是一次调整,还需要拿当前min位置的元素继续和它的左右孩子比较(进入下一层递归)
            minHeapify(nums, min, heapSize);
        }
    }
    //数组两元素交换
    public void swap(int[] nums, int a, int b){
        int temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
手写堆(小顶堆(大小=k) + 大于顶部的入堆)
class HeapSortK {
    //获取top-k元素主函数
    public int getTopK(int[] nums, int k){
        int heapSize = k;//指定堆的大小
        //先构建k个数的小顶堆:只对[0~k-1]的元素建堆,建立在nums上
        buildMinHeap(nums, heapSize);
        //再依次处理k及往后的元素
        for(int i = k; i < nums.length; i++){
            //元素>顶部才入堆 + 堆化
            if(nums[i] > nums[0]){
                nums[0] = nums[i];
                //对根部做堆化
                minHeapify(nums, 0, heapSize);
            }
        }

        return nums[0];//顶部保存的就是第k大元素
    }
	...建堆、调整的代码和上面的实现相同。
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

思路3:利用快排partition实现分治(速度最快)

这个思路是基于快速排序的特点:选择一个枢轴pivot,经过一趟快排后,这个枢轴会位于它在有序数组里的最终位置,如果是降序排列,那么在它之前的元素都大于等于它,在它之后的元素都小于它。

利用这个特点我们可以用来解top-k问题,按数组下标来表示的话,数组的第k大元素就是降序数组里下标 = k-1的元素。

算法流程:降序快排 + 分治
一轮降序快排(实现partition函数)

1、设置两个指针left, right,初始时分别指向待排序范围的左右边界。选择数组中的一个元素作为枢轴pivot,通常随机选择一个数,或选择nums[left]。

2、双指针开始工作:

  • right先移动,从后往前寻找第一个 >= pivot的元素,找到了之后将 nums[left] 和 nums[right] 交换,如果交换发生,则将left + 1;
  • left后移动,从前往后寻找第一个 < pivot的元素,找到之后将nums[left] 和 nums[right]交换,如果交换发生,则将right - 1;

不断循环上面两个步骤,直到left == right时,退出循环,此时下标left就是pivot最终所在的位置,令:

nums[left] = pivot;//(易遗漏)
  • 1

3、将pivot所在下标返回。

根据partition的返回值进行分治

前面选择了一个枢轴pivot后,经过一趟降序快排partition,返回它所在的下标 i :

  • 如果i == k - 1,说明当前元素就是第k大元素,返回nums[i]即可;
  • 如果i > k - 1,说明第k大元素在[0,i-1]之中,继续在[0~i-1]范围内做快排,寻找第k-1个元素;
  • 如果i < k - 1,说明第k大元素在[i + 1, n - 1]之中,继续在[i+1,n-1]范围内做快排,寻找第k-1个元素。
实现代码
class Solution {
    public int findKthLargest(int[] nums, int k) {
        quickSort(nums, k, 0, nums.length - 1);
        return nums[k - 1];
    }
    //快排主函数,当pivot是第k大元素时算法结束
    public void quickSort(int[] nums, int k, int left, int right){
        int index = partition(nums, left, right);
        if(index == k - 1) return;
        else if(index > k - 1) quickSort(nums, k, left, index - 1);
        else if(index < k - 1) quickSort(nums, k, index + 1, right);
    }
    //一轮降序快排函数:选择nums[left]作为枢轴,在[left,right]内做一次partition,返回所在的下标
    //发生填充的指针向内移动,另一个指针保持不动。直到两个指针相遇时,算法结束.
    public int partition(int[] nums, int left, int right){
        //取nums[left]作为枢轴pivot
        int pivot = nums[left];
        //直到left 
        while(left < right){
            //从末尾开始寻找大于pivot的元素
            while(right > left && nums[right] <= pivot) right--;
            if(left == right) break;
            nums[left++] = nums[right];
            while(left < right && nums[left] > pivot) left++;
            if(left == right) break;
            nums[right--] = nums[left];
        }
        nums[left] = pivot;//最后将枢轴插入合适的位置
        return left;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号