215.数组中的第k个最大元素(JS)

数组中的第K个最大元素

Category Difficulty Likes Dislikes
algorithms Medium (57.96%) 225 -
Tags

divide-and-conquer | heapjavascript

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

示例 1:java

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
复制代码

示例 2:web

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
复制代码

说明:数组

你能够假设 k 老是有效的,且 1 ≤ k ≤ 数组的长度。bash

/* * @lc app=leetcode.cn id=215 lang=javascript * * [215] 数组中的第K个最大元素 */
/** * @param {number[]} nums * @param {number} k * @return {number} */
var findKthLargest = function(nums, k) {
    
};
复制代码

1 全排序

这应该是最容易想到的方式了app

var findKthLargest = function (nums, k) {
  nums = nums.sort((a, b) => b - a)
  return nums[k - 1]
};
复制代码

时间复杂度:nlognless

空间复杂度:O(1)ide

可是很明显,这种作法很很差,好比数组长度为1w,咱们只要取第3个,这就很尴尬了函数

2部分排序(冒泡)

因此,咱们就思考着,直接给前k个数排序就能够了post

var findKthLargest = function (nums, k) {
  for (let i = 0; i < k; i++) {
    for (let j = 0; j < nums.length - 1 - i; j++) {
      if (nums[j] > nums[j + 1])
        [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]]
    }
  }
  return nums[nums.length - k]
};
复制代码

那时间复杂度就是O(n*k)

3部分排序(堆)

这个部分排序还能够再精简一点,好比咱们要的是第k个元素,那前k个元素也能够不用排序的嘛

这个时候,不少人就会思考呀?若是不知道把前k个数都排出来,咱们怎么知道知道第k个是谁呢?

那就要引入堆的概念了,若是咱们建立一个只有k 个元素的最小堆,那堆订必定是比堆里的任意元素是小的

var findKthLargest = function (nums, k) {
  let minHeap = new MinHeap()
  for (let i = 0; i < nums.length; i++) {
    if (minHeap.size() < k) minHeap.push(nums[i])
    else if (minHeap.top() < nums[i]) {
      minHeap.pop()
      minHeap.push(nums[i])
    }
  }
  return minHeap.top()
};
复制代码

这样时间复杂度就能够降到O(nlogk),可是使用堆结构就会额外的时候必定的空间,空间复杂度就是O(k)

固然这里的堆结构要本身实现

若是不知道怎么写的能够看我写的堆及堆排序(JS)

class MinHeap {
  constructor() {
    this.heap = []
    this.len = 0
  }
  size() {
    return this.len
  }
  push(val) {
    this.heap[++this.len] = val
    this.swin(this.len)
  }

  pop() {
    const ret = this.heap[1]
    this.swap(1, this.len--)
    this.heap[this.len + 1] = null
    this.sink(1)
    return ret
  }
  swin(ind) {
    while (ind > 1 && this.less(ind, parseInt(ind / 2))) {
      this.swap(ind, parseInt(ind / 2))
      ind = parseInt(ind / 2)
    }
  }
  sink(ind) {
    while (ind * 2 <= this.len) {
      let j = ind * 2
      if (j < this.len && this.less(j + 1, j)) j++
      if (this.less(ind, j)) break
      this.swap(ind, j)
      ind = j
    }
  }
  top() {
    return this.heap[1]
  }
  isEmpty() {
    return this.len === 0
  }

  swap(i, j) {
    [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]
  }
  less(i, j) {
    return this.heap[i] < this.heap[j]
  }
}
复制代码

4 快速选择

其实在面对一个找出某一符合元素的题,即搜索题时

咱们常常会立马想到二分查找

function binarySearch(arr, val) {
  let low = 0
  let high = arr.length - 1
  while (low <= high) {
    // mid 表示中间值
    let mid = Math.floor(low + (high - low) / 2)
    if (arr[mid] === val) return mid
    val < arr[mid] ? high = mid - 1 : low = mid + 1
  }
  return null
}

let arr = [0, 1, 2, 3, 4, 5]
console.log(binarySearch(arr, 0));
复制代码

二分查找有一个前提,就是arr[mid]要是一个中间值,这样才能够排除另外一半

提到中间值,不得不让咱们想到快速排序的pivot

那快排的partition函数就恰好与二分查找完美的结合

function quickSort(arr) {
  return quick(arr, 0, arr.length - 1)
}
function quick(arr, left, right) {
  if (arr.length > 1) {
    let index = partition(arr, left, right)
    if (left < index - 1) quick(arr, left, index - 1)
    if (index < right) quick(arr, index, right)
  }
  return arr
}
function partition(arr, left, right) {
  const pivot = arr[Math.floor(left + (right - left) / 2)]
  let i = left
  let j = right
  while (i <= j) {
    while (arr[i] < pivot) i++
    while (pivot < arr[j]) j--
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]]
      i++; j--;
    }
  }
  return i
}
let arr = [11, 2, 33, 4, 5]
console.log(quickSort(arr));
复制代码

不懂快速排序的能够看我写的 排序演化(三):快速

可是上面这种写法pivot会在left数组中,而不在rightleft之间

因此咱们须要对上面这种写法的partition中修改一下,使得pivot的值脱离出来

function partition(arr, left, right) {
  let mid = Math.floor(left + (right - left) / 2)
  const pivot = arr[mid]
  // 把pivot放在arr的最后面
  [arr[mid], arr[right]] = [arr[right], arr[mid]]
  let i = left
  // 把pivot排除在外,不对pivot进行排序
  let j = right - 1
  while (i <= j) {
    while (arr[i] < pivot) i++
    while (pivot < arr[j]) j--
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]]
      i++; j--;
    }
  }
  // 由于arr[i]是属于left的,pivot也是属于left的
  // 故咱们能够把本来保护起来的pivot和如今数组的中间值交换
  [arr[right], arr[i]] = [arr[i], arr[right]]
  return i
}
复制代码

那如今咱们只要在二分搜索里面添加上述的partition就能够实现快速查找

可是还须要主要一点的是,咱们是要升序仍是降序呢?

咱们是要求第k个最大元素

那这个数组的顺序应该是

arr = [5,4,3,2,1]这样的逆序的

好比上面的数组中,第2大的数就是4,即arr[1]

那咱们的partition应该按逆序划分,因此上面的代码还要改为

while (i <= j) {
+ while (arr[i] > pivot) i++
- while (arr[i] < pivot) i++
+ while (pivot < arr[j]) j--
- while (pivot < arr[j]) j--
  if (i <= j) {
    [arr[i], arr[j]] = [arr[j], arr[i]]
    i++; j--;
  }
}
复制代码

partition返回的下标i,就表示该arr[i]是该数组的第k-1位,由于下标是从0开始的,而语义上的第1位,便是i === 0

while (low <= high) {
  const mid = partition(arr.low, high)
  if (mid === k - 1) return arr[mid]

}
复制代码

那当mid < k -1表示 第k位 在arr[mid]的右边,那咱们只要查找arr[mid+1]~arr[high]便可

mid > k - 1表示 第k位在arr[mid]的左边,那那么咱们只要查找arr[low] ~ arr[mid - 1]

因此代码就是

while (low <= high) {
  const mid = partition(nums, low, high)
  if (mid === k - 1) return nums[mid]
  mid < k - 1 ? low = mid + 1 : high = mid - 1
}
复制代码

最终的代码就是

/* * @lc app=leetcode.cn id=215 lang=javascript * * [215] 数组中的第K个最大元素 */
/** * @param {number[]} nums * @param {number} k * @return {number} */
var findKthLargest = function (nums, k) {
  let low = 0
  let high = nums.length - 1
  while (low <= high) {
    const mid = partition(nums, low, high)
    if (mid === k - 1) return nums[mid]
    mid < k - 1 ? low = mid + 1 : high = mid - 1
  }

}

function partition(arr, low, high) {
  let mid = Math.floor(low + (high - low) / 2)
  const pivot = arr[mid]; // 这里记得添加分号 
  // 把pivot放在arr的最后面
  [arr[mid], arr[high]] = [arr[high], arr[mid]]
  let i = low
  // 把pivot排除在外,不对pivot进行排序
  let j = high - 1
  while (i <= j) {
    while (arr[i] > pivot) i++
    while (arr[j] < pivot) j--
    if (i <= j) {
      [arr[i], arr[j]] = [arr[j], arr[i]]
      i++; j--;
    }
  }
  // 由于arr[i]是属于left的,pivot也是属于left的
  // 故咱们能够把本来保护起来的pivot和如今数组的中间值交换
  [arr[high], arr[i]] = [arr[i], arr[high]]
  return i
}
复制代码

复杂度分析

  • 时间复杂度 : 平均状况 O(N),最坏状况 O(N^2)
  • 空间复杂度 : O(1)
相关文章
相关标签/搜索