【LeetCode】215.数组中的第K个最大元素(三种方法,九个思路的代码实现,java格式)

题目

题目链接

image-20200621204933917

解析

这道题据说是面试的高频考题,同时也是基础算法的应用。

image.png

方法一:暴力解法

题目要求我们找到“数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素” ,

语义是从右边往左边数第 k 个元素(从 11 开始),那么从左向右数是第几个呢,我们列出几个找找规律就好了。

  • 一共 6 个元素,找第 2 大,索引是 4;
  • 一共 6 个元素,找第 4 大,索引是 2。

因此,升序排序以后,目标元素的索引是 len - k。这是最简单的思路,如果只答这个方法,面试官可能并不会满意,但是在我们平时的开发工作中,还是不能忽视这种思路简单的方法,理由如下:

  • 最简单同时也一定是最容易编码的,编码成功的几率最高,可以用这个最简单思路编码的结果和其它思路编码的结果进行比对,验证高级算法的正确性;
  • 在数据规模小、对时间复杂度、空间复杂度要求不高的时候,简单问题简单做;
  • 思路简单的算法考虑清楚了,有些时候能为实现高级算法铺路,这道题也是如此;
  • 低级算法往往容错性最好,即在输入不满足题目条件的时候,往往还能得到正确的答案,而高级算法对输入数据的要求就非常苛刻,这一点可以参考 「力扣」第 4 题:“寻找两个有序数组的中位数”

参考代码 1

import java.util.Arrays;

public class Solution {
   

    public int findKthLargest(int[] nums, int k) {
   
        int len = nums.length;
        Arrays.sort(nums);
        return nums[len - k];
    }
}

复杂度分析

  • 时间复杂度:O*(NlogN),这里 N 是数组的长度,算法的性能消耗主要在排序,JDK 默认使用快速排序,因此时间复杂度为 O*(NlogN)。
  • 空间复杂度:O(1),这里是原地排序,没有借助额外的辅助空间。

到这里,我们已经分析出了:

1、我们应该返回最终排定以后位于 len - k 的那个元素;
2、性能消耗主要在排序,JDK 默认使用快速排序。

学习过 “快速排序” 的朋友,一定知道一个操作叫 partition,它是 “分而治之” 思想当中 “分” 的那一步。经过 partition 操作以后,每一次都能排定一个元素,并且这个元素左边的数都不大于它,这个元素右边的数都不小于它,并且我们还能知道排定以后的元素的索引。于是可以应用 “减而治之”(分治思想的特例)的思想,把问题规模转化到一个更小的范围里。

于是得到方法二。

方法二:借助 partition 操作定位到最终排定以后索引为 len - k 的那个元素(特别注意:随机化切分元素)

以下是注意事项,因为很重要,所以放在前面说:

快速排序虽然快,但是如果实现得不好,在遇到特殊测试用例的时候,时间复杂度会变得很高。如果你使用 partition 的方法完成这道题,时间排名不太理想,可以考虑一下是什么问题,这个问题很常见。

以下的描述基于 “快速排序” 算法知识的学习,如果忘记的朋友们可以翻一翻自己的《数据结构与算法》教材,复习一下,partition 过程、分治思想和 “快速排序” 算法的优化。

分析:我们在学习 “快速排序” 的时候,接触的第 1 个操作就是 partition(切分),简单介绍如下:

partition(切分)操作,使得:

  • 对于某个索引 jnums[j] 已经排定,即 nums[j] 经过 partition(切分)操作以后会放置在它 “最终应该放置的地方”;
  • nums[left]nums[j - 1] 中的所有元素都不大于 nums[j]
  • nums[j + 1]nums[right] 中的所有元素都不小于 nums[j]

image.png

partition(切分)操作总能排定一个元素,还能够知道这个元素它最终所在的位置,这样每经过一次 partition(切分)操作就能缩小搜索的范围,这样的思想叫做 “减而治之”(是 “分而治之” 思想的特例)。

切分过程可以不借助额外的数组空间,仅通过交换数组元素实现。下面是参考代码:

参考代码 2

public class Solution {
   

    public int findKthLargest(int[] nums, int k) {
   
        int len = nums.length;
        int left = 0;
        int right = len - 1;

        // 转换一下,第 k 大元素的索引是 len - k
        int target = len - k;

        while (true) {
   
            int index = partition(nums, left, right);
            if (index == target) {
   
                return nums[index];
            } else if (index < target) {
   
                left = index + 1;
            } else {
   
                right = index - 1;
            }
        }
    }

    /**
     * 在数组 nums 的子区间 [left, right] 执行 partition 操作,返回 nums[left] 排序以后应该在的位置
     * 在遍历过程中保持循环不变量的语义
     * 1、[left + 1, j] < nums[left]
     * 2、(j, i] >= nums[left]
     *
     * @param nums
     * @param left
     * @param right
     * @return
     */
    public int partition(int[] nums, int left, int right) {
   
        int pivot = nums[left];
        int j = left;
        for (int i = left + 1; i <= right; i++) {
   
            if (nums[i] < pivot) {
   
                // 小于 pivot 的元素都被交换到前面
                j++;
                swap(nums, j, i);
            }
        }
        // 在之前遍历的过程中,满足 [left + 1, j] < pivot,并且 (j, i] >= pivot
        swap(nums, j, left);
        // 交换以后 [left, j - 1] < pivot, nums[j] = pivot, [j + 1, right] >= pivot
        return j;
    }

    private void swap(int[] nums, int index1, int index2) {
   
        int temp = num
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值