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

题目

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例
  输入:[3,2,3,1,2,4,5,5,6] 和 k = 4
  输出:4

TopK问题是一道高频面试题!

解法一:排序+查找

由于数组是未排序的,最直接粗暴的方法就是先对数组进行排序,排序后直接通过索引返回满足条件的元素即可。代码简洁到感人。

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

复杂度:总的时间复杂度就是排序的复杂度:(O(NlogN)),没有使用额外的空间,空间复杂度为 (O(1))

解法二:快速排序(单边)

快速排序中,每次partion操作将数组分为两部分,左边小于切分元素,右边大于切分元素。这样我们如果找到这样的一个切分点,partion操作完它右边有 k-1 个元素,那这个切分点就是我们需要的元素。

class Solution {
    Random rand = new Random();
    public int findKthLargest(int[] nums, int k) {
        return findK(nums, 0, nums.length - 1, k);
    }

    private int findK(int[] nums, int lo, int hi, int k){
        int j = partion(nums, lo, hi);
        if(j == k  -1) return nums[j];
        else if(j > k - 1){
                return findK(nums, lo, j - 1, k);
        }
        else{
                return findK(nums, j + 1, hi, k);
        }
    }

    private int partion(int[] nums, int lo, int hi){
        int r = rand.nextInt(hi - lo + 1) + lo;  // 切分点的位置,随机选择很关键
        int tmp = nums[r];   // 哨兵
        nums[r] = nums[lo];
        nums[lo] = tmp;
        while(lo < hi){
            while(lo < hi && nums[hi] <= tmp){
                hi--;
            }
            nums[lo] = nums[hi];
            while(lo < hi && nums[lo] >= tmp){
                lo++;
            }
            nums[hi] =  nums[lo];
        }
        nums[lo] = tmp;
        return lo;
    }
}

复杂度:快速排序的复杂度为 (O(NlogN)),但是上面的代码仅仅是快速排序的部分实现,其期望时间复杂度为 (O(N)),《算法导论》里给出了具体的复杂度分析;空间复杂度为 (O(logN)),即递归栈所使用的额外空间。
注意:选择切分点对于极端情况下程序的计算时间影响很大。如下图,对于leetcode给的测试样例,不使用随机操作程序执行时间会增加5倍以上。

解法三:堆排序

堆排序有两种思路:

  1. 使用最大堆进行原地排序,每次从堆中删去堆顶元素(堆中最大元素)并重新调整堆使其有序,在删去 k-1 个元素并堆有序后,堆顶元素就是所求元素。
  2. 建立大小为 k 的最小堆,接下来每次将一个元素和堆顶元素比较,如果大于堆顶元素,则替换堆顶元素并调整使堆有序。遍历完所有元素后堆顶就是所求元素。

下面是思路一的实现,自己在数组原地实现了最大堆。也可以借助java的PriorityQueue类实现堆。

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int heapSize = nums.length;
        for(int i = heapSize/2; i >= 0; i--){
            adjust(nums, i, heapSize);  // 通过调整使得初始堆有序
        }

        while(--heapSize > nums.length - k){
            swap(nums, 0, heapSize);
            adjust(nums, 0, heapSize);
        }
        return nums[heapSize + 1];
    }

    private void adjust(int[] nums, int i, int size){
        while(2 * i + 1 < size){
            int j = 2 * i + 1;
            if(j + 1 < size && nums[j] < nums[j + 1]){  // 左右孩子中值最大的索引
                j++;
            }
            if(arr[i] > arr[j])   break;
            swap(nums, i, j);
            i = j;
        }
    }

    private void swap(int[] nums, int i, int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

复杂度:使用思路一原地排序,空间复杂度为 (O(1));时间复杂度为 (O(NlogN)),其中构建堆的时间复杂度为 (O(N)),删除k个元素的时间代价为 (O(klogN)),渐进时间复杂度为 (O(NlogN))。使用思路二的最小堆,时间复杂度为 (O(Nlog(k))),如果不使用递归不考虑递归栈的空间,空间复杂度为 (O(k))

!!!解法二快速排序和解法三堆排序是TopK问题推荐的解法,一定要掌握!

原文地址:https://www.cnblogs.com/rezero/p/13222211.html