java数据结构与算法刷题-----LeetCode215. 数组中的第K个最大元素

java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846

在这里插入图片描述

1. 堆排序

解题思路:时间复杂度O( n ∗ l o g 2 n n*log_2n nlog2n),使用小根堆会达到近乎O(n),空间复杂度O( l o g 2 n log_2{n} log2n)
  1. 使用小根堆,建堆时间复杂度O(k),调整堆(删除堆顶并插入新元素)O( n ∗ l o g 2 k n*log_2k nlog2k),其中k是题目要求的返回第k最大元素。因此小根堆大小为k,故建堆为O(k). 共计O( k + n ∗ l o g 2 k k+n*log_2k k+nlog2k) = O(n)
  2. 不断地将元素插入到小根堆(根最小,其它元素都比根大)中,当堆中有k个元素,此时还需要往堆中插入元素时,需要进行判断。
  3. 因为此时堆顶元素正好是堆中倒数第k大元素。如果新插入元素比堆顶大。证明当前堆顶不是倒数第k大
  4. 则堆顶删除,并将新元素插入。此时调整堆,新的堆顶元素为第k大。以此类推。直到所有元素入堆后。
  5. 最终返回堆顶即可。
堆排序https://blog.csdn.net/grd_java/article/details/136937525
代码:当前官方增加了很多测试用例,已经无法超越100%的用户了,目前最快的算法,只能达到17ms,进行优化后,也只到了15ms。我查看2021年提交时的记录,是3ms超越100%。目前已经无法达到了。
  1. 使用Java提供的优先级队列实现小根堆(面试时候肯定不让你用。因此这个代码帮你理解整体的思路。然后第二个实现方法,我们需要自己实现小根堆)
    在这里插入图片描述
class Solution {
    //[6,5]
    //
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            //返回值>0,o1放在o2后面。反之o1放o2前面
            //小根堆,小的在前面。利用o1-o2实现。如果o1小,o1-o2<0,o1会放在o2前面
            //如果o1大o2小,o1-o2>0,o1会放在o2后面。而小的o2放在o1前面
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });
        for(int num:nums){
            if(queue.size() == k){
                if(queue.peek() < num) {
                    queue.poll();
                    queue.offer(num);
                }
            }else{
                queue.offer(num);
            }
        }
        return queue.poll();
    }
}
  1. 自己实现小根堆,因为Java自带容器加了很多健壮性和线程安全的逻辑,所以效率较慢,我们自己实现小根堆就会快很多。
    在这里插入图片描述
class Solution {
        public int findKthLargest(int[] nums, int k) {
        int[] minHeap = new int[k];//小根堆
        for (int i = 0; i < k; i++) {//大小为k
            minHeap[i] = nums[i];
        }
        //k/2-1是二叉树的知识,代表以k个结点构成的二叉树的第一个非叶子结点。k/2-2是第二个非叶子,以此类推。i == 0是整个二叉树的根结点
        for (int i = k / 2 - 1; i >= 0; i--) {//调整小根堆,从下到上,依次让每一颗子树满足小根堆
            adjustHeap(minHeap, i);//i是二叉树的每个非叶子结点,小根堆的要求是:每个子树,根结点都是整棵树最小
        }
        //小根堆构建完成后,minHeap[0]就是当前第k大的数。接下来需要不断进行判断和入堆操作
        for (int i = k; i < nums.length; i++) {
            if (nums[i] > minHeap[0]) {//如果当前i,是比小根堆堆顶更大的元素,那么堆顶不是第k大,
                minHeap[0] = nums[i];//将堆顶出堆,并将i放在堆顶位置
                adjustHeap(minHeap, 0);//此时很有可能小根堆逻辑被破坏,也就是i太大,不满足小根堆,因此需要让i进行下降调整,让其重新满足小根堆定义
            }
        }
        return minHeap[0];
    }

    /**
     * 以root为根结点构建/调整堆
     * @param array 堆
     * @param root 当前根结点
     */
    private void adjustHeap(int[] array, int root) {
        //让root结点下降到合适位置,以满足小根堆效果(任何一颗子树,根结点都是最小的)
        while (true) {//当堆调整完成后,结束
            int left = 2 * root + 1;//获得root的左子结点下标
            int right = left + 1;//获得root的右子结点下标
            int min = root;//最小值,最终需要放到root结点位置
            //如果左子结点存在,并且左子结点更小,让min指向这个结点
            if (left < array.length && array[left] < array[min]) min = left;
            //如果右子结点存在,并且右子结点更小,让min指向这个结点
            if (right < array.length && array[right] < array[min]) min = right;
            //如果min == root说明小根堆调整结束
            if (min == root) break;
            //让min当前指向位置和root交换,也就是下降操作,说明root当前指向的结点不是最小值,不满足小根堆
            //因为小根堆,越上面层次的结点,越小,所以如果当前root太大,需要让其下降
            swap(array, root, min);
            //root本次下降完成后,min的位置是root新的位置。因为root下降到min的位置
            //让root指向min,然后继续循环,判断是否root需要继续下降。直到它下降到合适位置
            root = min;
        }
    }

    private void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

2. 快速选择

堆排序方便将K个数全找出来,例如数组中最大的k个数。如果只是找第k大数,有点大材小用。因为堆排序需要操作很多与第k大数无关的k-1,k-2…这些数。
而快速选择是快速排序的创始人针对解决单独只想找到某一个数字的问题而创作的算法,它不需要类似快速排序每一趟确定一个数字的最终位置。这个算法只关心第K个元素的位置。因此它的时间复杂度来到了O(n)

解题思路
  1. 我们每次都找到区间中间位置的数x
  2. 以x为基枢,保证当前区域,所有小于x的放在x左边,大于x的放在x的右边
  3. 如果x左边够k个数,就继续去左边处理,如果左边不够k个数,就去右边处理

如果左边部分没有第k大数,那就去右边找,此时左边部分的 leftNums个数,都已经确定都比第k大数小,所以k - leftNums为下轮右边部分该找的值。这一点很重要,会节省很多操作。这也是为什么我这个算法在官方测试用例增加后依然可以跑进3ms。而官方的写法只能在11ms。

代码:因为时间复杂度比堆更低,因此更快

在这里插入图片描述

class Solution {
    //程序入口
    public int findKthLargest(int[] _nums, int k) {
        int n = _nums.length;
        return quickselect(_nums, 0, n - 1, n-k+1);
    }
    //交换值
    void swap(int[] a,int i,int j){
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    //快速选择
    int quickselect(int[] a, int left, int right, int k) {
        if(left >= right) return a[left];
        int x = a[left + right >> 1];//获取当前区间中间的数x
        //借助两个下标,让比x小的去x左边,比x大的去右边
        int leftIndex = left - 1, rightIndex = right + 1;//分别代表区间中应该在x左边(<=x)的下标和x右边的下标
        while(leftIndex < rightIndex){
            do leftIndex++;while(a[leftIndex] < x);//当leftIndex指向的值,>=x时,终止循环
            do rightIndex--;while(a[rightIndex] > x);//当rightIndex指向的值,<=x时,终止循环
            if(leftIndex < rightIndex){//如果leftIndex目前在x左边,而且rightIndex目前在x右边,说明他俩的指向的值应该调换位置
                swap(a,leftIndex,rightIndex);//让小的去x左边leftIndex位置,大的去x右边rightIndex位置
            }
        }//直到目前区间实现x是中位数,左边都比x小,右边都比x大为止
        //此时rightIndex必然指向第一个比x小的值(也就是x比它小的左边部分的边界)
        if(k <= (rightIndex - left + 1)) //如果left到rightIndex区间(比x小的左边部分),正好够我们找到第k大数
            return quickselect(a,left,rightIndex,k);//进入左边区间继续寻找
        else //如果左边部分没有第k大数,那就去右边找,此时左边部分的 rightIndex - left + 1已经确定都比第k大数小,所以k - 左边部分为下轮右边部分该找的值
            return quickselect(a,rightIndex+1,right,k-(rightIndex-left+1));//右边部分找第(k - 左边部分个数)大的数。
    }
}
  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值