一天一大 leet(数组中的第 K 个最大元素)难度:中等 DAY-29

时间:2022-07-25
本文章向大家介绍一天一大 leet(数组中的第 K 个最大元素)难度:中等 DAY-29,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目(难度:中等):

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例

  1. 示例 1
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
  1. 示例 2
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度

抛砖引玉

先排序(倒序)后直接取值 实现 sort 排序功能,循环原数组把元素逐个放到排序数组中:

  • 大于第一个元素,放到首位
  • 小于最后一个元素,放到末尾
  • 在两个元素中间:循环判断位置在排序数组哪个位置,插入进元素
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
  // nums.sort((a, b) => b - a).slice(0, k)
  // return nums[k - 1]
  let sortArr = []
  for (let i = 0; i < nums.length; i++) {
    if (sortArr.length === 0) {
      sortArr[0] = nums[i]
    } else if (sortArr[sortArr.length - 1] >= nums[i]) {
      sortArr[sortArr.length] = nums[i]
    } else if (sortArr[0] <= nums[i]) {
      sortArr.unshift(nums[i])
    } else {
      let len = sortArr.length
      for (let j = 0; j < len; j++) {
        if (sortArr[j] <= nums[i]) {
          sortArr.splice(j, 0, nums[i])
          break
        }
      }
    }
  }
  return sortArr[k - 1]
}

官方答案

基于快速排序的选择方法

  • 声明两个索引来圈定第 k 大的元素的位置,left,right
  • 默认 left = 0,right = nums.length - 1;
  • 循环 left 到 right 中间的元素(任取一个或者 right 位置上的数作为基准), 确保基准左边的元素都小于等于它,右边的元素都大于等于它:
    • 如果 指针遇到小于 right 的数,则与 right 替换当前指针指向的数字,最终 right 上是基准数
    • 记录小于基准数的数字个数,就基准数是第几大数
  • 当基准的索引小于第 k 大位置索引缩小范围:left 到 q - 1
  • 当基准的索引大于第 k 大位置索引缩小范围:q+1 到 right
  • 等于基准数的索引,则当前位置就第 k 大的值,返回
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
// 替换指定位置两个数
  function swap(a, i, j) {
var tmp = a[i]
    a[i] = a[j]
    a[j] = tmp
  }
  function quickSelect(a, left, right, index) {
let q = randomPartition(a, left, right)
// 如果index即假设第k大位置数的索引
//  大于基准数的索引,则缩小范围到:q+1 到 right
//  小于基准数的索引,则缩小范围到:left 到 q - 1
//  等于基准数的索引,则当前位置就第k大的值,返回
if (q == index) {
return a[q]
    } else {
return q < index
        ? quickSelect(a, q + 1, right, index)
        : quickSelect(a, left, q - 1, index)
    }
  }
// 任意取left到right中间一个数放到right位置做基准数据
// 此部分逻辑可不用,直接去left或者right上的值做基准(默认用right做基准)
  function randomPartition(a, left, right) {
let i = Math.floor(Math.random() * (right - left + 1)) + left
swap(a, i, right)
return partition(a, left, right)
  }
// 循环left到right中数据是right的左侧都小于等于它,并返回并返回基准数在数组有多少数小于它(即第几大)
  function partition(a, left, right) {
let x = a[right],
      i = left - 1
// 如果指针上在left位置上,数据小于right上的数据:swap(a, left, j)
// 如果指针上在j位置上,数据小于right上的数据:swap(a, left++, j)
for (let j = left; j < right; ++j) {
if (a[j] <= x) {
swap(a, ++i, j)
      }
    }
// 上面的循环如果i等于j其j等于right时,递归时则无法终止
// 则单独替换第i+1上和right上的数
swap(a, i + 1, right)
return i + 1
  }
return quickSelect(nums, 0, nums.length - 1, nums.length - k)
}

基于堆排序的选择方法

构造前 k 个最大元素小顶堆,取堆顶,即把最大的 k 个元素依次提到数组顶部

  • 每次取最大推送到顶部
  • 第 k 次时则第 k 大的数在顶部
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
// 替换指定位置两个数
function swap(a, i, j) {
let tmp = a[i]
    a[i] = a[j]
    a[j] = tmp
  }
// 遍历数组分别对每个元素判断其是否大于左右的值
// 因为比较范围为 x-1到x+1则循环一半就可以遍历数组
function buildMaxHeap(a, len) {
for (let i = parseInt(len / 2, 10); i >= 0; --i) {
      maxHeapify(a, i, len)
    }
  }
// 指针指向i
// 如果两边的数字大于它则用较大的数字替换它
// 递归比较知道查到最大值结束
function maxHeapify(a, i, len) {
let left = i * 2 + 1,
      right = i * 2 + 2,
      largest = i
if (left < len && a[left] > a[largest]) {
      largest = left
    }
if (right < len && a[right] > a[largest]) {
      largest = right
    }
if (largest != i) {
      swap(a, i, largest)
      maxHeapify(a, largest, len)
    }
  }
let len = nums.length
  buildMaxHeap(nums, len)
for (let i = nums.length - 1; i >= nums.length - k + 1; --i) {
    swap(nums, 0, i)
    --len
    maxHeapify(nums, 0, len)
  }
return nums[0]
}

菜鸟的自白

  • 快速排序
  • 堆排序 本题逻辑不怎么复杂,但是因为对上面两个算法不了解,官方答案看了好几次才看明白。要补下快速排序和堆排序了。