leetcode 215.数组中的第K个最大元素 | 刷题打卡

391 阅读5分钟

题目链接

站在巨人的肩膀上

思路

直接用原生方法倒叙排序后取值,取巧的办法,哈哈。

代码

function findKthLargest(nums: number[], k: number): number {
    return nums.sort((a, b) => b - a)?.[k - 1];
};

快排

利用快排的思想找出第k大的值

思路

快排基本思路:

  1. 随便找数组[i, j]里的一个基准值,假设q是该基准值的索引;
  2. 然后将比q大的值移动到p的右边,将比q小的值移动到p的左边,这样数组[i, j]就会被q分成左右两个区间[i, q - 1][i, q + 1]
  3. 然后递归两个区间做同样的操作,最后能完成排序。

这里的q每次调整完就是最终排好序的位置,因为目标就是找到第k大的值,只要刚好是第k大的数,直接返回就行了,后面就不用判断,不需要将整个数组都排好序。

这里有个优化点,判断一下qk的位置,如果qk左边,只需要递归左区间就行,反正,递归右区间

代码

function findKthLargest(nums: number[], k: number): number {
    return quickSelect(nums, 0, nums.length - 1, nums.length - k);
};

function quickSelect(nums: number[], l: number, r: number, index: number): number {
    const q = partition(nums, l, r);
    if (q === index) {
        // 找到目标值,返回
        return nums[q];
    } else {
        return q < index ?
            quickSelect(nums, q + 1, r, index) : // 递归右区间
            quickSelect(nums, l, q - 1, index); // 递归左区间
    }
}

function partition(nums: number[], l: number, r: number): number {
    let x = nums[r]; // 以最后一项为基准点,大于它放它右边,小于它的放它左边
    let i = l - 1; // 基准点最后的位置
    for (let j = l; j < r; j++) {
        // 当前值小于基准点,则交换
        if (nums[j] <= x) {
            swap(nums, ++i, j); // i加1,保证i是最后一个小于基准值的值的位置
        }
    }
    swap(nums, ++i, r); // i加1变成第一个大于基准值的位置,再交换第一个大于基准值和基准值的位置
    return i; // 最后的i就是基准值的位置
}

/**
 * 交换两数
 */
function swap(nums: number[], i: number, j: number): void {
    [nums[i], nums[j]] = [nums[j], nums[i]];
}

代码优化

问题:

如果每次都取同一个位置的值作为基准值(这里是区间的最后一个值),有时候会出现一种极端情况,就是一直在左区间或一直在右区间进行递归,这种情况是最坏的,时间复杂度是 O(n^2)

解决办法:

我们可以引入随机化来加速这个过程,基准值每次都是随机取的,它的时间复杂度会降为 O(n)

function findKthLargest(nums: number[], k: number): number {
    return quickSelect(nums, 0, nums.length - 1, nums.length - k);
};

function quickSelect(nums: number[], l: number, r: number, index: number): number {
-    const q = partition(nums, l, r);
+    const q = randomPartition(nums, l, r);
    if (q === index) {
        // 找到目标值,返回
        return nums[q];
    } else {
        return q < index ?
            quickSelect(nums, q + 1, r, index) : // 递归右区间
            quickSelect(nums, l, q - 1, index); // 递归左区间
    }
}

function partition(nums: number[], l: number, r: number): number {
    let x = nums[r]; // 以最后一项为基准点,大于它放它右边,小于它的放它左边
    let i = l - 1; // 基准点最后的位置
    for (let j = l; j < r; j++) {
        // 当前值小于基准点,则交换
        if (nums[j] <= x) {
            swap(nums, ++i, j); // i加1,保证i是最后一个小于基准值的值的位置
        }
    }
    swap(nums, ++i, r); // i加1变成第一个大于基准值的位置,再交换第一个大于基准值和基准值的位置
    return i; // 最后的i就是基准值的位置
}

/**
 * 交换两数
 */
function swap(nums: number[], i: number, j: number): void {
    [nums[i], nums[j]] = [nums[j], nums[i]];
}

+ function randomPartition(nums: number[], l: number, r: number): number {
+    const i = getRandom(l, r); // 获取范围内的随机值
+    swap(nums, i, r); // 和最后一项交换
+    return partition(nums, l, r);
+ }

+ /**
+  * 获取指定范围内的随机数
+  */
+ function getRandom(n: number, m: number): number {
+    return Math.floor(Math.random() * (m - n + 1) + n)
+ }

堆排序

思路

我们需要的是第k大的元素,只需要对部分元素进行排序,无需对整个数组进行排序。

堆是一个完全二叉树,所以可以用数组进行映射,堆的特点:

  • n 个元素的左子节点为2 * n + 1
  • n 个元素的右子节点为2 * n + 2
  • n 个元素的父节点为Math.floor((n - 1) / 2)
  • 最后一个非叶子节点为Math.floor(arr.length / 2) - 1

image-20210902144245364

堆排序是基于堆这种数据结构而设计的一种算法。

堆的种类有两种:

  • 大顶堆:每个节点的值都 大于或等于 其左右孩子节点的值
  • 小顶堆:每个节点的值都 小于或等于 其左右孩子节点的值

结点的两个孩子节点大小没有要求

这里查找第k大的元素,显然用大顶堆比较合适。

堆排序就是对部分元素进行排序,利用堆排序可以将无序数组遍历具有一定规律的大顶堆数组。大顶堆的根元素,也就是数组的第一项一定是最大的,删除最大的重新排序,将第二大的移动到堆顶,重复删除k-1次,即可获取第k大的元素。

如何将无序数组变成大顶堆参考文章:www.cnblogs.com/chengxiao/p…

代码

function findKthLargest(nums: number[], k: number): number {
    let { length } = nums;

    buildMaxHeap(nums, length); // 将数组构建为一个大顶堆

    // 找第k大的元素
    for (let i = nums.length - 1; i >= nums.length - k + 1; i--) {
        swap(nums, 0, i); // 将堆顶元素(最大的那个)放到数组最后面
        length--; // 堆顶元素不再参数后续的上浮
        // 将后续的最大值浮上来
        bubble(nums, 0, length);
    }

    return nums[0];
};

function buildMaxHeap(nums: number[], length: number) {
    for (let i = Math.floor(length / 2); i >= 0; i--) {
        bubble(nums, i, length); // 进行上浮操作
    }
}

/**
 * 上浮
 */
function bubble(nums: number[], i: number, length: number) {
    const l = i * 2 + 1; // 左子节点
    const r = i * 2 + 2; // 右子节点
    let largest = i; // 最大值下标

    // 比较当前节点和其两个子节点的大小,报错最大值的下标
    if (l < length && nums[l] > nums[largest]) {
        largest = l;
    }
    if (r < length && nums[r] > nums[largest]) {
        largest = r;
    }
    
    // 如果当前值不是最大值
    if (largest !== i) {
        // 上浮
        swap(nums, i, largest);
        // 继续调整下面的子节点
        bubble(nums, largest, length);
    }
}

/**
 * 交换两数
 */
function swap(nums: number[], i: number, j: number) {
    [nums[i], nums[j]] = [nums[j], nums[i]];
}