【leetcode】215.数组中第k个最大元素js

题目

代码-sort

最开始最简单粗暴的解法,当然也是顺利通过了。

javascript 复制代码
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    nums.sort((a, b) => b - a)
    return nums[k - 1]
};

代码-快速选择

但是呢这题如果真就这样写的话那肯定不能是中等难度了,于是想到会不会是要自己手搓排序。而我想到的快排时间复杂度是O(nlogn),跟题目要求的O(n)比起来还是太高了,经过题解和d老师的指导,了解到可以对快速排序做一个优化,也就是------快速选择。

我们知道快速排序是选一个基准,然后小于基准的放左边,大于基准的放右边,最后再找到基准应该在的位置放进去。之后左右分别递归以上过程,递归的终止条件是需要排序的数组长度为1。而这题要找的是第k大的元素,所以只需要找这个元素在的那半边就行了。

那我们怎么知道这元素在哪半边呢?用下标判断。第k大的元素是排序好了之后的数组的numsnums.length - k,而我们选择的基准在放到它该在的位置的时候也能获取到下标,因此就可以去做判断:如果相等,就找到了;如果不相等,那就去处理目标元素在的那半边,另一边就不用再排序处理了。

javascript 复制代码
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const target = nums.length - k
    
    const swap = (arr, i, j) => {
        [arr[i], arr[j]] = [arr[j], arr[i]]
    }
    // 分区
    const partition = (left, right) => {
        // 随机选基准
        const pivotId = Math.floor(Math.random() * (right - left + 1)) + left
        const pivot = nums[pivotId]
        // 交换基准与最右侧数的位置,便于比较
        swap(nums, pivotId, right)
        // 记录当前交换位置
        let storeId = left
        // 比较
        for (let i = left; i < right; i++) {
            if (nums[i] <= pivot) {
                swap(nums, i, storeId)
                storeId++
            }
        }
        // 把基准放到正确的位置
        swap(nums, storeId, right)
        return storeId
    }

    let left = 0, right = nums.length - 1
    while (left <= right) {
        const pivotIndex = partition(left, right)
        if (pivotIndex === target) {
            return nums[pivotIndex]
        } else if (pivotIndex < target) {
            left = pivotIndex + 1
        } else {
            right = pivotIndex - 1
        }
    }
    return -1
};

tip:这里随机选基准是为了避免最坏的情况,也就是完全降序的数组。

这个代码看起来万事大吉对吧!时间复杂度符合要求理论上也应该没问题。但是!遇到下面这样的样例就超时了:

也就是有很多很多重复元素的场景下,快速选择算法的时间复杂度会退化成O(n²),导致超时。d老师有话说:

为什么重复元素会导致 O(n²)?

因为 partition 使用 <= pivot,把所有等于 pivot 的元素都放到了左边

当数组里绝大多数元素都等于 pivot(比如随机选到了 1),左边会包含几乎整个数组(因为 1 <= 1 成立),而右边只有很少的元素。这样每次递归只能排除掉一个或几个元素,总操作数变成 n + (n-1) + (n-2) + ...,就是 O(n²)。

虽然用了随机基准,但碰到这种大量重复的数字,随机选到 1 的概率极高(因为 1 占了绝大部分),所以基本每次都会退化。

那这时候咋办呢?一看问题其实是出在重复的元素上,那能不能单独处理这些重复的元素呢?可以!三路划分(荷兰国旗)快速选择可以!

代码-三路划分

把数组分成三块:小于 pivot等于 pivot大于 pivot

这样当 pivot 是重复值时,所有等于 pivot 的元素一次性归位,不再参与后续递归,避免了重复元素的干扰。

javascript 复制代码
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const target = nums.length - k
    
    const swap = (i, j) => {
        [nums[i], nums[j]] = [nums[j], nums[i]]
    }
    // 三路划分,分为小于pivot、等于pivot、大于pivot的三个区间
    const threePathPartition = (left, right) => {
        const pivotId = Math.floor(Math.random() * (right - left + 1)) + left
        const pivot = nums[pivotId]
        let lt = left, gt = right
        let i = left
        while (i <= gt) {
            if (nums[i] < pivot) {
                swap(lt, i)
                i++
                lt++
            } else if (nums[i] > pivot) {
                swap(gt, i)
                gt--
            } else {
                i++
            }
        }
        return [lt, gt]
    }

    let left = 0, right = nums.length - 1
    while (left <= right) {
        const [lt, gt] = threePathPartition(left, right)
        if (target < lt) {
            // 目标在小于区间
            right = lt - 1
        } else if (target > gt) {
            // 目标在小于区间
            left = gt + 1
        } else {
            // 目标在等于区间
            return nums[target]
        }
    }
    return -1
};

过了!!!!