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

40 阅读1分钟

leetcode.cn/problems/kt…

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

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

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

 

示例 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 <= 105 -104 <= nums[i] <= 104

方法一:基于快速排序的选择方法

思路和算法

image.png

代码:

class Solution {
    public int findKthLargest(int[] nums, int k) {
        return qSort(nums, 0, nums.length - 1, nums.length - k);
    }

    //对数组中下标为 low - high 的子表进行快速排序
    private int qSort(int[] nums, int low, int high, int k) {
        int pivot;
        //将子表一分为二,算出枢轴 pivot
        pivot = partition(nums, low, high);

        if (pivot == k) {
            return nums[pivot];
        } else {
            if (pivot < k) {
                //对高子表进行递归排序
                return qSort(nums, pivot + 1, high, k);
            } else {
                //对低子表进行递归排序
                return qSort(nums, low, pivot - 1, k);
            }
        }
    }

    private int partition(int[] nums, int low, int high) {
        int pivotKey;
        //用子表的第一个记录作为枢轴记录
        pivotKey = nums[low];
        //从表的两端交替向中间扫描
        while (low < high) {
            while (low < high && nums[high] >= pivotKey) {
                high--;
            }
            //比枢轴小的记录交换到低端
            swap(nums, low, high);

            while (low < high && nums[low] <= pivotKey) {
                low++;
            }
            //比枢轴大的记录交换到高端
            swap(nums, low, high);
        }
        //返回枢轴所在的位置
        return low;
    }

    //交换数组中的两个数
    private void swap(int[] nums, int low, int high) {
        int temp = nums[low];
        nums[low] = nums[high];
        nums[high] = temp;
    }
}

复杂度分析

时间复杂度:O(n),如上文所述,证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。

空间复杂度:O(logn),递归使用栈空间的空间代价的期望为 O(logn)。

方法二:基于堆排序的选择方法

思路和算法

image.png

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();

        for (int i = 0; i < nums.length; i++) {
            if (priorityQueue.size() < k) {
                priorityQueue.offer(nums[i]);
            } else {
                if (priorityQueue.peek() < nums[i]) {
                    priorityQueue.poll();
                    priorityQueue.offer(nums[i]);
                }
            }
        }
        return priorityQueue.peek();
    }
}
class Solution {
    public int findKthLargest(int[] nums, int k) {

        // 将带排序序列构建成一个小顶堆
        for (int i = nums.length / 2; i >= 0; i--) {
            headAdjust(nums, i, nums.length - 1);
        }

        //逐步将每个最小值的根节点与末尾元素交换,并且将 nums[1] 至 nums[i-1] 调整为小顶堆
        for (int i = nums.length - 1; i > 0; i--) {
            swap(nums, 0, i);
            headAdjust(nums, 0, i - 1);
        }

        return nums[nums.length - k - 1];
    }

    /**
     * 已知 nums[low] 至 nums[high] 中除了 nums[low] 之外均满足堆的定义
     * 本方法调整 nums[low] ,使 nums[low] 至 nums[high] 成为一个小顶堆
     *
     * @param nums
     * @param s
     * @param m
     */
    private void headAdjust(int[] nums, int s, int m) {
        int temp = nums[s];

        //沿关键字较大的孩子结点向下筛选
        for (int j = 2 * s; j <= m; j *= 2) {
            //j 为关键字中较大的记录的下标
            if (j < m && nums[j] < nums[j + 1]) {
                j++;
            }

            if (temp >= nums[j]) {
                break;
            }

            //较大元素插入到它的根节点
            nums[s] = nums[j];

            //s 变为较大元素的下标
            s = j;
        }
        //插入
        nums[s] = temp;
    }

    //交换数组中的两个数
    private void swap(int[] nums, int low, int high) {

        int temp = nums[low];

        nums[low] = nums[high];

        nums[high] = temp;
    }
}

复杂度分析

时间复杂度:O(nlogn),建堆的时间代价是 O(n),删除的总代价是 O(klogn),因为 k<n,故渐进时间复杂为 O(n+klogn)=O(nlogn)。

空间复杂度:O(logn),即递归使用栈空间的空间代价。