高频5. 数组中的第K个最大元素 及其变形题的各种解法 Java codetop

题目描述

数组中的第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

各种解法

快排,快速选择

class Solution {
    Random random = new Random();
    public int findKthLargest(int[] nums, int k){
        return quickSelect(nums, 0, nums.length-1, nums.length-k); // 范围,想找的索引位置
    }
    public int quickSelect(int[] a, int l, int r, int index){
        int q = randomPartition(a, l, r);
        if(q == index){
            return a[q];
        }else{
            return q < index?quickSelect(a,q+1, r, index) : quickSelect(a, l, q-1, index);
        }
    }
    public int randomPartition(int[] a, int l, int r){
        // 从l,r之间随机选择一个,换到最后的r位置,作为分界
        int i = random.nextInt(r-l+1)+l;
        swap(a, i, r);
        return partition(a,l,r);
    }
    public int partition(int[] a, int l, int r){
        // 以最右边的元素作为分界线,获取他应该在的位置
        int x = a[r], i = l-1;
        // 从左到右遍历元素,如果小于分界元素,让他与左边的元素交换
        for(int j=l; j<r; ++j){
            if(a[j] <= x){
                swap(a, ++i, j);
            }
        }
        // 到i位置,前面的都是比分界小的,确定分界的位置就是i+1
        swap(a, i+1, r);
        return i+1;
    }
    public void swap(int[] a, int i, int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

解题思路
  • 快排的核心思想,就是要找到作为分界的q,在整个数组中的正确位置。把比他小的数,都交换到左边,最后的空出来的位置就是分界值q应该放置的位置。
  • 因为要找到第k大的最大元素,所以应该找的是索引在nums.length-K的数。
  • 如果当前得到的分界q的正确索引小于nums.length-K,那么只需要继续对分界右边的内容找另一个分界继续向下做;大于的,对分界左边的内容继续进行。
  • 直到分界q的索引正好是nums.length-K,输出该值,就是第K大元素
  • 理想情况下,每次划分都是绝对均匀划分,那么每次递归后,数组规模减小为一半,总共递归lgN次,每次对 n / 2 k n/2^k n/2k个元素比较,总的时间复杂度为 O ( ∑ k = 0 l g N N / 2 k ) = O ( N ) O(\sum_{k=0}^{lgN}N/2^k) = O(N) O(k=0lgNN/2k)=O(N)
  • 除此之外,为了保证快排算法的稳定性,每次的分界值的选取采用随机的方式,近似于均匀,时间复杂度O(N)。如果从小到大排,防止每次取到的数刚好是最大的,这样就要每次都执行很多交换,退化为O(N^2)。也就是最坏情况下每次规模降低1。

堆排序

自己实现堆
class Solution {
       public int findKthLargest(int[] nums, int k){
        int heapSize = nums.length;
        buildMaxHeap(nums, heapSize);
        // 开始删除根
        for(int i = nums.length-1; i>=nums.length-k+1; --i){
            swap(nums,0,i);
            --heapSize;
            maxHeapify(nums,0,heapSize);
        }
        return nums[0];
    }
    public void buildMaxHeap(int[] a, int heapSize){
        for(int i=heapSize/2; i>=0; --i){ // 考察父节点是否都比子节点大
            maxHeapify(a, i, heapSize);
        }
    }
    // 带着heap大小调整
    public void maxHeapify(int[] a, int i, int heapSize){
        int l = i*2+1, r = i*2+2, largest = i;
        if(l<heapSize && a[l]>a[largest]){
            largest = l;
        }
        if(r < heapSize && a[r]>a[largest]){
            largest = r;
        }
        if(largest != i){
            swap(a, i, largest);
            maxHeapify(a, largest, heapSize);// 当前节点是否需要继续往下调整
        }
    }
    public void swap(int[] a, int i, int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}
解题思路
  • 自己实现一个大根堆,使用数组实现。
  • 首先需要基于当前的数组来构建一个原始的大根堆,对数组中的元素进行调整。因为是树结构,所以组织形式是 索引 i 为父节点,那么 索引 2*i+1为左子节点,索引 2*i+2 为右子节点。需要保证父节点的值大于两个子节点的值。
  • maxHeapify函数:每个父节点和相应左右子节点的调整方式是,找到父节点和他的两个左右子节点的值进行比较,找到最大的,换到父节点的位置,父节点交换到该子节点的位置。然后作为父节点继续向下和下面的左右子节点比较。只要是父节点需要交换而且还在堆的大小范围内,都要一直向下调整下去。
  • 构建原始堆时,需要对其中的一半的节点执行maxHeapify即可,因为只要父节点调整完毕,就可以了。而且是从后往前的,索引0位置的是最大的根,最后调整。
  • 现在要拿到第k大的元素,需要每次把根换到最后,然后调整的范围-1,执行k-1次,把第1到第k-1都调到后面,最后剩下的nums[0]就是第k大的元素
  • 每次把跟换到最后,把最后的换到当前位置,所以需要对换上来的元素的大小进行向下调整。
  • 时间复杂度 O(N+klogN)
用优先队列PriorityQueue实现
class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> pq = new PriorityQueue<Integer>(k);
        for(int i=0; i<nums.length; i++){
            if(pq.size()<k){
                pq.add(nums[i]);
            }else{
                if(nums[i]>=pq.peek()){
	                pq.poll();
	                pq.add(nums[i]);
                }
            }
        }
        return pq.peek();
    }
}
解题思路
  • PriorityQueue默认是使用小根堆,把他的大小设置为k
  • 因为要求的是第k大元素,所以保证最后优先队列中存的是最大的k个值,因为小根堆,所以堆顶就是第k大元素
  • 首先按顺序把k个值存入。如果后面的数比堆顶大,那么就把堆顶删除,改为存入该元素,处理完所有数,堆顶就是所求的结果。
  • 时间复杂度为 O(K+(N-K)logK)
BFPRT算法
  • BFPRT取自5个作者的姓名首字母。
  • 上面的快速选择算法,只能在理想情况下才能达到O(N),堆排序,只有当K远小于N时,才能近似于O(N)。
  • 因为快速选择算法中,如果每次刚好拿到的分界值就是中位数是最理想的情况。BFPRT就是额外加了一步计算近似中位数的步骤。计算的是近似的,不是准确的,准确的时间复杂度太高。
  • BFPRT做了两步:
    • 数组划分,划分为N/5个子数组,每个数组中5个元素,找到每一组的中位数,O(N),递归找到中位数的中位数
    • 按照上面找到的中位数的中位数,近似中位数的范围在30%-70%,按照递归函数的时间复杂度推导出为O(N)。具体推导参考 具体推导
  • 快速选择的分界值,到底使用 随机选择 还是 BFPRT方案中用中位数的中位数。BFPRT中可以保证时间复杂度为O(N),而随机只能做到期望时间复杂度为O(N)

变形题

1. 无序数组找中位数

其实就是topK变成了top(N/2)了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值