0
点赞
收藏
分享

微信扫一扫

LeetCode215 数组中的第K个最大元素

一点读书 2022-04-04 阅读 178
数据结构

目录

题目

分析

解答

(1)堆排序

(2)快排变形 

自测

补充


题目

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

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: 
[3,2,1,5,6,4] 和k = 2
输出: 5

示例 2:

输入: 
[3,2,3,1,2,4,5,5,6] 和k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 104
  • -104 <= nums[i] <= 104

分析

        这题看似考察的查找,实际上考察的是排序,寻找topK,那么必然需要排序,可以将整个数组排序后取出倒数第K个数。但是这样的话复杂度就比较高了,因此我们采取不完全的排序来实现该功能,有2种主流的实现方法,堆排序和快排变形。

解答

(1)堆排序


        通过建立小根堆的方法实现排序。利用小根堆最小元素在堆顶的特性,遍历数组,并只保留最大的k个元素,最后,留在堆顶的元素即为第K大元素。

        但需要注意,在与面试官沟通的时候,需要与其确认,是否允许JDK内置的优先队列,PriorityQueue,如果不能,则需要手动实现小根堆。

        (1.1)PriorityQueue

    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
        for(int num:nums){
            queue.add(num);
            if(queue.size()>k){
                queue.poll();
            }
        }
        return queue.poll();
    }

        (1.2)手动实现堆

    public int findKthLargest(int[] nums, int k) {
        int[] res = new int[k];
        for(int i=0;i<k;i++){
            res[i] = nums[i];
        }
        // 针对前k个元素建立小根堆,最小元素在顶端
        buildHeap(res);
        // 遍历后续元素
        for(int i=k;i<nums.length;i++){
            if(nums[i]>res[0]){
                // 每次将新进入的元素 与堆顶元素比较,若更大,则需要入堆,继而递归调整小根堆
                res[0] = nums[i];
                adjust(res,0);
            }
        }
        return res[0]; 
    }
    
    public void buildHeap(int[] arr){
        // 针对堆的属性,从数组长度的中间位置开始建立堆
        for(int i=arr.length/2-1;i>=0;i--){
            adjust(arr,i);
        }
    }

    public void adjust(int[] arr,int index){
        int maxIndex = index;
        int len = arr.length;
        if(index*2+1<len && arr[index*2+1]<arr[maxIndex]){ 
            maxIndex = index*2+1;
        }
        if(index*2+2<len && arr[index*2+2]<arr[maxIndex]){ 
            maxIndex = index*2+2;
        }
        if(maxIndex!=index){
            swap(arr,index,maxIndex);
            adjust(arr,maxIndex);
        }
    }
    
    public void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

注意优化的细节:    
        1、k如果超过一半时,其实可反过来找len-k+1个最小元素
        2、如果当前数小于堆顶的元素,就不用添加    
以上两个点都可以用来减小堆的维护所消耗的时间和空间。
空间复杂度:O(k) ,堆内只要保持有K个元素即可,因此为O(k)。   
时间复杂度:O(n*logk) ,小/大根堆的维护时间为O(logk),因为每个元素都是先插入堆尾,然后再通过逐层向上寻找父节点的方式维护堆的属性(即父节点小于左右孩子节点),至多递归logk次(删除同理)。然后需要遍历全部n个元素,因此为O(nlogk)。

(2)快排变形 

        除了建堆,我们还可以使用快排变形来优化。我们知道快排的核心思路是通过分选定一个基准值,将比基准值小的放左边,大的放右边,通过这种方式将让整个数组达到大致有序的状态。在本题中,我们就可以使用这个特性,当选定的基准值在将两侧划分完毕后刚好在k位置时,左侧都是小于nums[k]的值,刚好就是我们要求的答案。

    private static Random random = new Random(System.currentTimeMillis());

    public int findKthLargest(int[] nums, int k) {
        int len = nums.length;
        // 注意题目,这里求的是第K大
        int target = len - k;
        int left = 0;
        int right = len - 1;
		return partition(nums,left,right,target);
    }

    // 在区间 nums[left..right] 区间执行 partition 操作
    private int partition(int[] nums, int left, int right,int k) {
        // 在区间随机选择一个元素作为标定点
        if (right > left) {
            int randomIndex = left + 1 + random.nextInt(right - left);
            swap(nums, left, randomIndex);
        }

        int pivot = nums[left];
		int i = left, j = right;
        // 交换两侧逆序元素
        while (i < j) {
            while (i < j && nums[j] >=pivot) j--;
            while (i < j && nums[i] <=pivot) i++;
            swap(nums, i, j);
        }
        // 将基准元素放到其最终位置上
        swap(nums, left, j);
        // 判断k与当前位置的大小关系
		if(j>k){
			return partition(nums,left,j-1,k);
		}
		if(j<k){
			return partition(nums,j+1,right,k);
		}
        return nums[j];
    }

    public void swap(int[] arr,int i,int j){
        int curr = arr[i];
        arr[i] = arr[j];
        arr[j] = curr;
    }

时间复杂度:O(n) ,每次递归都会遍历数组的一部分,第一次递归是完整的数组,即n,由于基准值pivot具有随机性,后续递归子数组的长度的平均长度为上一层的1/2,于是总的遍历长度即可得出n+1/2*n+1/4*n+...+1/n*n,由等比数列求和公式得出结果为2n-1,即O(n) 。
空间复杂度:O(logN),平均递归深度O(logN)。

自测

        题目规定了K与数组的长度大于等于1,那么我们不用考虑K为0或数组为空的情况。当然我们也可以在代码中加上兼容,以提高程序的可扩展性。其次,题目表明数组元素正负都有可能,因此在设计样例的时候需要考虑到负数的存在。同时观察题目中给出的样例存在重复元素的情况,这也是我们需要考虑的点。综上,我们设计出以下样例。

[1] 和 k = 1
[3,3,2,1] 和 k = 2
[-3,3,2,1] 和 k = 2

补充

相关题目:剑指 Offer 40. 最小的k个数

除此之外,还有类似无序数组找中位数这样的功能,都属于topK类型题目的变种,需要牢记以上这两种解法与复杂度的分析方法。

举报

相关推荐

0 条评论