最小的k个数

321 阅读2分钟

题目

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]

限制:

0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000

解法一: 数组排序

var getLeastNumbers = function(arr, k) {
     return arr.sort((a, b) => a - b).slice(0, k)
}

拓展:sort()方法的"黑历史"

在 V8 引擎 7.0 版本之前,数组长度小于10时, Array.prototype.sort() 使用的是插入排序,否则用快速排序。

在 V8 引擎 7.0 版本之后就舍弃了快速排序,因为它不是稳定的排序算法,在最坏情况下,时间复杂度会降级到 O(n2)。

而是采用了一种混合排序的算法:TimSort(以人名命名) 。

在数据量小的子数组中使用插入排序,然后再使用归并排序将有序的子数组进行合并排序 。

解法二: 构建大顶堆求 Top k问题

可以通过构造一个大顶堆来解决,大顶堆上的任意节点值都必须大于等于其左右子节点值,即堆顶是最大值。

所以我们可以从数组中取出 k 个元素构造一个大顶堆,然后将其余元素与大顶堆对比,如果小于堆顶则替换堆顶,然后堆化,所有元素遍历完成后,堆中的元素即为前 k 个最小值

思路:

  • 从数组中取前 k 个数( 0 到 k-1 位),构造一个大顶堆
  • 从 k 位开始遍历数组,每一个数据都和大顶堆的堆顶元素进行比较,如果大于堆顶元素,则不做任何处理,继续遍历下一元素;如果小于堆顶元素,则将这个元素替换掉堆顶元素,然后再堆化成一个大顶堆。
  • 遍历完成后,堆中的数据就是前 K 小的数据
var getLeastNumbers = function(arr, k) {
  // 从 arr 中取出前 k 个数,构建一个大顶堆
  let heap = arr.slice(0, k)
  // 堆化 数组下标从1开始,null 起到占位作用
  heap.unshift(null)
  buildHeap(heap, k)
  // 从 k 位开始遍历数组
  for(let i = k; i < arr.length; i++) {
      if(heap[1] > arr[i]) {
          // 替换并堆化
          heap[1] = arr[i]
          heapify(heap, k, 1)
      }
  }
  // 删除第一个占位元素
  heap.shift()
  return heap
};

// 原地建堆,从后往前,自上而下式建大顶堆
let buildHeap = (arr, k) => {
  if(k === 1) return
  // 从最后一个非叶子节点开始,自上而下式堆化
  for(let i = k >> 1; i > 0 ; i--) {
      heapify(arr, k, i)
  }
}

// 堆化
let heapify = (arr, k, i) => {
  // 自上而下式堆化
  while(true) {
      let maxIndex = i
      if(2*i <= k && arr[2*i] > arr[i]) {
          maxIndex = 2 * i
      }
      if(2*i+1 <= k && arr[2*i+1] > arr[maxIndex]) {
          maxIndex = 2 * i + 1
      }
      if(maxIndex !== i) {
          swap(arr, i, maxIndex)
          i = maxIndex
      } else {
          break
      }
  }
}

// 交换
let swap = (arr, i , j) => {
  let temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}