215. Kth Largest Element in an Array

Leetcode 问题: 215. Kth Largest Element in an Array

题目

Given an integer array nums and an integer k, return the kth largest element in the array.

Note that it is the kth largest element in the sorted order, not the kth distinct element.

传入数字阵列 nums 及排名 k,找到阵列中第 k 大的数字

Example 1

Input: nums = [3,2,1,5,6,4], k = 2
Output: 5

Example 2

Input: nums = [3,2,3,1,2,4,5,5,6], k = 4
Output: 4

答案

JavaScript

1. 数字阵列先排序

/**
 * @param {number[]} numsList
 * @param {number} kth
 * @return {number}
 */
var findKthLargest = function(numsList, kth) {
    // 排序数字列(小到大排序)
    numsList.sort(function(a, b) {
        return a - b;
    });

    // 越后面数字越大,从后面找第 k 排名的数字
    let kthNumber = numsList[numsList.length - kth];

    return kthNumber;
};

2. 使用 QuickSelect 的 Partition 切割,将数字阵列切割排序

一般的排序需要将所有的数字都排序过,所以必须比对所有的数字,所以时间複杂度至少需要 nlog(n),当排序的输字阵列越多,时间複杂度就会越多

所以如果要能够在数字越多的状况下,也能够有平均速度以上的排序速度,可以选择 QuickSelect 排序选择方式,时间複杂度最快可以是 O(n),但最慢可能会到 O(n^2)

在数字阵列比较小的状况,直接排序 是比较快的,但在数字阵列可大可小的状况下,QuickSelect 平均速度是都能够在水准以上的

QuickSelect 流程
Input: nums = [3,2,1,5,6,4], k = 2
Output: 5
A. 选择一用来比较的 pivotCheckNumber 当作检查数字
  • 确保「检查数字」左边的数字,都是比「检查数字」小
  • 确保「检查数字」右边的数字,都是比「检查数字」大

一开始随机选择 最右边 的数字当作检查数字

索引 0 1 2 3 4 5
数字 3 2 1 5 6 4
检查指标 左指标 右指标
其他指标 最小数字指标 检查数字指标

目标要将比「检查数字」 小的数字放在左边,比较大的就放在「检查数字」右边

所以就拿「最小数字指标」当作储存较小数字的指标位置,预设是最左边

B. 从左指标检查到右指标

「左指标」数字「检查数字」 小,就将「左指标」数字 放到 「最小数字指标」,而 「最小数字指标」 就往右移动,等待放入下一个比 「检查数字」 小的数字,然后持续检查到右指标

Step 1

  • 左指标数字 3检查数字指标数字 4 小,将 左指标数字 3最小数字指标 数值交换
  • 「最小数字指标」 往右移动
  • 左指标 往右移动
索引 0 1 2 3 4 5
数字 3 2 1 5 6 4
检查指标 左指标 右指标
其他指标 最小数字指标 检查数字指标

Step 2

  • 左指标数字 2检查数字指标数字 4 小,将 左指标数字 2最小数字指标 数值交换
  • 「最小数字指标」 往右移动
  • 左指标 往右移动
索引 0 1 2 3 4 5
数字 3 2 1 5 6 4
检查指标 左指标 右指标
其他指标 最小数字指标 检查数字指标

Step 3

  • 左指标数字 1检查数字指标数字 4 小,将 左指标数字 1最小数字指标 数值交换
  • 「最小数字指标」 往右移动
  • 左指标 往右移动
索引 0 1 2 3 4 5
数字 3 2 1 5 6 4
检查指标 左指标 右指标
其他指标 最小数字指标 检查数字指标

Step 4

  • 左指标数字 5检查数字指标数字 4 大,不做任何动作
  • 左指标 往右移动
索引 0 1 2 3 4 5
数字 3 2 1 5 6 4
检查指标 左指标 右指标
其他指标 最小数字指标 检查数字指标

Step 5

  • 左指标数字 6检查数字指标数字 4 大,不做任何动作
  • 左指标 往右移动
索引 0 1 2 3 4 5
数字 3 2 1 5 6 4
检查指标 左指标 & 右指标
其他指标 最小数字指标 检查数字指标

Step 6

  • 左指标 已经检查到 右指标 位置了,不再继续检查
  • 检查数字指标数字 4最小数字指标数字 5 交换
  • 所以目前切割的位置在 最小数字指标索引 3 的位置,数值是 4
    • 最小数字指标索引 3 的位置右边都是比 4 大的数字
    • 最小数字指标索引 3 的位置左边都是比 4 小的数字
  • 所以目前 最小数字指标索引 的切割位置及数字都是确定的
索引 0 1 2 3 4 5
数字 3 2 1 4 6 5
检查指标 左指标 & 右指标
其他指标 最小数字指标 检查数字指标

Step 7

因为要选择第 k 大的数字,所以在排序的数字阵列中,选择 (length - k) 阵列索引 的数字

  • 如果要选第 1 大的数字,等于是要选择索引阵列中 (6 - 1) = 5 第 5 个索引 位置的数字
  • 如果要选第 2 大的数字,等于是要选择索引阵列中 (6 - 2) = 4 第 4 个索引 位置的数字
  • 如果要选第 3 大的数字,等于是要选择索引阵列中 (6 - 3) = 3 第 3 个索引 位置的数字
  • 如果要选第 4 大的数字,等于是要选择索引阵列中 (6 - 4) = 2 第 2 个索引 位置的数字
  • …以此类推

状况 1: 要找的数字是刚好是切割位置

确定的 切割位置最小数字指标,所以索引位置确定会是 3,数字确定会是 4

若刚好要找 第 3 大的数字,等于是要选择索引阵列中 (6 - 3) = 3 第 3 个索引 位置的数字

等于 目前的切割点 就是要找的 第 k 大数字,可以直接回传 4

索引 0 1 2 3 4 5
数字 3 2 1 4 6 5
检查指标 左指标 & 右指标
其他指标 最小数字指标 检查数字指标

状况 2: 要找的数字是大小是大于切割位置的数字

假如要找 第 2 大 的数字,等于是要选择索引阵列中 (6 - 2) = 4 第 4 个索引 位置的数字

但目前的切割位置是在 最小数字指标,第 3 个索引,而刚刚切割流程中,已经确定

  • 最小数字指标索引 3 的位置右边都是比 4 大的数字
  • 最小数字指标索引 3 的位置左边都是比 4 小的数字

表示现在要往 切割位置右方 去找,才能找到 第 2 大 的数字,所以就重複刚刚的步骤,重新对右半边的数字阵列做切割比较即可

  • 可以将 左指标 设定在 切割位置 右边 4
  • 右边区块的最小数字指标设定与 左指标 相同
  • 然后重複刚刚的步骤,直到找到 切割位置索引位置第 k 大数字 相同,符合 状况 1: 要找的数字是刚好是切割位置 即可
索引 0 1 2 3 4 5
数字 3 2 1 4 6 5
检查指标 左指标 右指标
其他指标 X X X X 最小数字指标 检查数字指标

状况 3: 要找的数字是大小是小于切割位置的数字

假如要找 第 4 大 的数字,等于是要选择索引阵列中 (6 - 4) = 2 第 2 个索引 位置的数字

状况 2 雷同,只是要反过来往 切割位置左方 去找,才能找到 第 4 大 的数字,所以就重複刚刚的步骤,重新对左半边的数字阵列做切割比较即可

  • 可以将 右指标 设定在 切割位置 左边 2
  • 左边区块的最小数字指标设定与 左指标 相同
  • 然后重複刚刚的步骤,直到找到 切割位置索引位置第 k 大数字 相同,符合 状况 1: 要找的数字是刚好是切割位置 即可
索引 0 1 2 3 4 5
数字 3 2 1 4 6 5
检查指标 左指标 右指标
其他指标 最小数字指标 检查数字指标 X X X
/**
 * @param {number[]} numsList
 * @param {number} kth
 * @return {number}
 */
var findKthLargest = function(numsList, kth) {
    // 选择第 k 大的数字 : 在排序的数字阵列中,选择 (length - k) 阵列索引的数字
    let kIndexNumber= numsList.length - kth;
    // 左指标:从阵列头开始
    let leftPointer = 0;
    // 右指标:从阵列尾开始
    let rightPointer = numsList.length - 1;
    while (leftPointer < rightPointer) {
        // 当左右指标没有交叉,继续寻找
        let partitionPointer = partitionNumberList(numsList, leftPointer, rightPointer);

        if (partitionPointer == kIndexNumber) {
            // 目前切割的点,就是要寻找的第 k 个索引的数字
            break;
        } else if (partitionPointer < kIndexNumber) {
            // 目前切割的点,比要找的 k 个索引数字还小,切割点左边确认数字更小不需要找,往切割点右边部分继续找
            // 将「左指标」移动到「切割点」右方
            leftPointer = partitionPointer + 1;
        } else {
            // 目前切割的点,比要找的 k 个索引数字还大,切割点右边确认数字更大不需要找,往切割点左边部分继续找
            // 将「右指标」移动到「切割点」左方
            rightPointer = partitionPointer - 1;
        }
    }

    return numsList[kIndexNumber];
};

// 切割数字阵列
var partitionNumberList = function(numsList, leftPointer, rightPointer) {
    // 选择右指标当作「检查数字」
    let pivotCheckNumber = numsList[rightPointer];
    // 较小数字纪录指标设定为左指标
    let smallerNumberPointer = leftPointer;

    for(let i = leftPointer; i < rightPointer; i++) {
        // 从「左指标」检查到「右指标」
        if (numsList[i] <= pivotCheckNumber) {
            // 若「目前检查的数字」小于等于「检查的数字」
            // 将目前数字放到「较小数字记录指标」位置
            [numsList[smallerNumberPointer], numsList[i]] = [numsList[i], numsList[smallerNumberPointer]];
            // 「较小数字记录指标」位置往后移动
            smallerNumberPointer++;
        }
    }
    // 检查完成,确认「较小数字记录指标」数字都比「检查数字」小
    // 「较小数字记录指标」与「检查数字」交换
    //  - 「较小数字记录指标」左方都是比「检查数字」小的数字
    //  - 「较小数字记录指标」右方都是比「检查数字」大的数字
    [numsList[smallerNumberPointer], numsList[rightPointer]] = [numsList[rightPointer], numsList[smallerNumberPointer]];

    return smallerNumberPointer;
};

参考资料