Q3-LeetCode215-数组中的第K个最大元素

72 阅读3分钟

实现思路

1 方法1: 双路/三路快排 + 二分剪枝

2 方法2: 优先队列,待完善

3 参考实现:
01 官方参考实现

代码实现

方法1.1: 双路快排 + 二分查找剪枝 时间复杂度: O(n) 空间复杂度: O(log⁡n)

function findKthLargest(nums: number[], k: number): number {
  let len = nums.length;
  // 第k大,对应的下标为 len - k:
  // 比如第1大,对应的下标为 len-1; 第2大,对应的下标为 len-2 ...
  return quickFind(nums, 0, len - 1, len - k);  
};


// 利用 快速排序的partition 思想,确定每个元素的位置
function quickFind(nums, l, r, tdx) {
  if (l >= r) return nums[l];
  let p = partition(nums, l, r);
  // 如果p正好是目标下标,直接返回
  if (p === tdx) return nums[p];
  // 易错点1: 这里二分是必须的
  if (p > tdx) {
    return quickFind(nums, l, p - 1, tdx);
  } else {
    return quickFind(nums, p + 1, r, tdx);
  }
}

function partition(nums, l, r) {
  // S1 获取基准比较值x
  // [0, 1) ==> [0, r-l+1) ==> [l, r+1)
  let rdx = Math.floor(Math.random() * (r - l + 1)) + l;
  swap(nums, l, rdx);
  const x = nums[l];

  // S2 构建区间: [l+1, lt) <=x; (gt, r] >=x
  // 判断 开闭区间的技巧: 初始化时满足是 空区间 + 执行到不满足区间条件时的指针位置是否 符合定义含义
  let lt = l + 1, gt = r;
  // 易错点1: 由于lt和gt都是开区间,所以需要处理指针相等时的最后一个成员
  while (lt <= gt) {
    while (nums[lt] < x) lt++;
    while (nums[gt] > x) gt--;
    // 易错点2: 外部循环里lt < gt成立时,由于内部还会先更新 lt/gt值
    // 所以运行到这里时,不能再保证 lt还<= gt了
    // 如果在这一步 lt === gt了,说明已经处理完所有元素了,不需要再进行后续处理
    // 如果还进行后续交换和更新指针的处理,反而会让 lt/gt 的位置 和定义时的不一致,导致结果出错
    if (lt >= gt) break;
    swap(nums, lt++, gt--);
  }

  // S3 把基准值x放到正确的位置,并最后返回其 位置下标
  // 执行到此处时, [l+1, lt) <=x; (gt, r] >=x; 且lt和gt的关系必然为 lt=gt / lt=gt+1
  // 也就是说 [l+1, lt-1/gt]必然 <=x; [gt+1/lt, r] >=x
  // 所以交换了 l和gt后,就必然会使得 [l, gt-1] <=x; [gt, r] >=x
  swap(nums, l, gt);
  return gt;
}

function swap(nums, i, j) {
  [nums[i], nums[j]] = [nums[j], nums[i]];
}

方法1.2: 三路快排 + 二分查找剪枝 时间复杂度: O(n) 空间复杂度: O(log⁡n)

function findKthLargest(nums: number[], k: number): number {
  let len = nums.length;
  // 第k大,对应的下标为 len - k:
  // 比如第1大,对应的下标为 len-1; 第2大,对应的下标为 len-2 ...
  return quickFind(nums, 0, len - 1, len - k);  
};


// 利用 快速排序的partition 思想,确定每个元素的位置
function quickFind(nums, l, r, tdx) {
  if (l >= r) return nums[l];
  let [lt, gt] = partition(nums, l, r);
  // 表示目标值在 [lt+1, gt-1] 之间
  if (tdx > lt && tdx < gt) return nums[lt+1]
  if (tdx <= lt) {
    return quickFind(nums, l, lt, tdx)
  } else {
    return quickFind(nums, gt, r, tdx)
  }
}

// 三路快排
function partition(nums, l, r) {
  // S1 获取基准比较值 x
  let rdx = Math.floor(Math.random() * (r - l + 1)) + l;
  swap(nums, l, rdx);
  let x = nums[l];
  // S2 定义循环不变量: [l+1, lt] < x; [lt+1, i) ===x; [gt, r] > x
  let lt = l, gt = r + 1, i = l + 1;
  while (i < gt) {
    if (nums[i] < x) {
      swap(nums, ++lt, i++);
    } else if (nums[i] === x) {
      i++;
    } else {
      swap(nums, --gt, i);
    }
  }
  // S3 运行到此处时: [l+1, lt] < x;  [lt+1, i/gt-1] === x, [gt, r] > x
  // 需要把 x放到正确位置,即 把l和lt 交换位置,才能确保 [l, lt] < x成立;
  // 但是交换之后,整个区间的性质其实变成了: [l, lt-1] < x, [lt, gt-1] === x, [gt, r] > x
  swap(nums, l, lt);
  return [lt - 1, gt];
}

function swap(nums, i, j) {
  [nums[i], nums[j]] = [nums[j], nums[i]];
}

方法1.3 不完全的双路快排 时间复杂度: O(n) 空间复杂度: O(logn)

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

function quickFind(arr, l, r, tdx) {
  // 易错点1: 递归中止条件
  if (l >= r) return arr[l];
  let rdx = Math.floor(Math.random() * (r - l + 1)) + l;
  swap(arr, l, rdx);
  let x = arr[l], lt = l - 1, gt = r + 1;
  while (lt < gt) {
    while (arr[++lt] < x);
    while (arr[--gt] > x);
    if (lt < gt) swap(arr, lt, gt);
  }
  // 运行到此时,会保证[l, gt] <= x, [gt + 1, r] >= x
  if (tdx <= gt) return quickFind(arr, l, gt, tdx);
  return quickFind(arr, gt + 1, r, tdx);
}

function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]];
}