Partition 搜寻第 k 个数字

Partition 搜寻第 k 个数字

找出第 k 小的数字,使用 partition 将阵列切割

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

搜寻第 K 大的数字

索引是从 0 开始计算,第 k 大等于是索引第 length - k 个索引数字

TypeScript

/**
 * @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;
};

搜寻第 K 小的数字

索引是从 0 开始计算,第 k 小等于是索引第 k 个索引数字

TypeScript

function findKthSmallestNumber(numsList: number[], kth: number): any {
    // 设定指标
    let leftPointer = 0;
    let rightPointer = numsList.length - 1;

    while (leftPointer < rightPointer) {
        // 切割点
        let partitionPointer = partitionNumberList(numsList, leftPointer, rightPointer);

        if (partitionPointer == kth) {
            // 如果「切割点」刚好是第 k 个数字,不需要再找了,直接回传切割点指标数字
            break;
        }

        if (partitionPointer < kth) {
            // 如果「切割点」比第 k 个数字还要小,表示目前切割点是偏左,在 k 的左边
            // 所以左边的数字一定是比较小不用找,可能的答案在右边
            // 往右方部分找,找下一个切割点
            leftPointer = partitionPointer + 1;
        } else {
            // 如果「切割点」比第 k 个数字还要大,表示目前切割点是偏右,在 k 的右边
            // 所以右边的数字一定是比较大不用找,可能的答案在左边
            // 往左方部分找,找下一个切割点
            rightPointer = partitionPointer - 1;
        }
    }

    return numsList[kth];
}


// 切割数字阵列
var partitionNumberList = function(numsList: number[], leftPointer: number, rightPointer: number): number {
    // 选择右指标当作「检查数字」
    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;
};