「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
题目介绍
输入整数数组 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 <= 100000 <= arr[i] <= 10000
解题思路
这道题的解法可以很简单,一行代码就可以搞定,但我们不只是为了做题而做题,因此可以发散思维多思考几种方法,本文将讲解 3 种解题思路
思路一
思路一是利用 js 数组中自带的排序方法,将传入的数组按从小到大进行排序,然后取排序后数组的前 k 位,就是最小的 k 个数
解题代码
var getLeastNumbers = function(arr, k) {
return arr.sort((a, b) => a - b).slice(0, k)
};
思路二:快速排序
快速排序同样是将传入的数组按从小到大排列,然后取排序后数组的前 k 位,但是快速排序的效率会比 js 中自带的排序效率高一点
快速排序的步骤
- 取数组的第一个位置作为比较的基准值
base,然后定义两个游标l和r分别指向数组的第一位和最后一位 - 如果
l的下标比r小,则往前移动r的下标,直到找到小于基准值base的值,或者r的下标小于等于l的下标为止 - 如果
l的下标比r小,则往后移动l的下标,直到找到大于基准值base的值,或者r的下标小于等于l的下标为止 - 交换
l和r位置的值 - 重复
2-4的步骤,直到l的下标大于等于r的下标 - 将
base与l位置的值进行交换,此时base将数组分成两边,左边全部为小于base的值,右边全部为大于base的值 - 对
base左右两边的数组重复快速排序1-6的过程,直到排序结束 - 返回排序后的前
k个数即为最小k个数
解题代码
var getLeastNumbers = function(arr, k) {
quickSort(arr, 0, arr.length - 1)
return arr.slice(0, k)
};
var quickSort = function(arr, low, high) {
if (low >= high) return
const base = arr[low]
let l = low
let r = high
while (l < r) {
while (l < r && arr[r] >= base) r--
while (l < r && arr[l] <= base) l++
[arr[l], arr[r]] = [arr[r], arr[l]]
}
[arr[low], arr[l]] = [arr[l], arr[low]]
quickSort(arr, low, l - 1)
quickSort(arr, l + 1, high)
}
思路三:利用大顶堆
另一种思路是可以利用一个固定大小的大顶堆,如果数据量没有达到大顶堆的大小,那么直接往大顶堆插入值,并且调整大顶堆的结构,如果数据量大于大顶堆的大小,则看要插入的数值是否大于堆顶元素的值,如果大于等于,则不插入,否则将堆顶元素替换为插入的值,然后调整大顶堆的结构
大顶堆的构造方法
- 定义
Heap类,定义push方法,如果没有达到大顶堆的大小,直接插入元素,并向上调整;如果超过大顶堆的值并且符合插入条件,则将堆顶元素替换为插入元素,并向下调整 - 定义向上调整方法
sortBack,从插入元素的位置,依次与其父节点进行比较,如果大于父节点,则交换两者位置,然后继续往上比较,比较到堆顶为止 - 定义向下调整方法
sortFront,从堆顶元素,依次与其孩子节点进行比较,最大的值与父节点交换位置,然后继续向下比较,直到最后一个元素 - 最后返回大顶堆中的元素,则为最小的
k个数
解题代码
var getLeastNumbers = function(arr, k) {
// 构建大顶堆
const heap = new Heap(k)
// 依次向大顶堆插入元素
while (arr.length) {
heap.push(arr.pop())
}
// 返回大顶堆的元素
return heap.arr
};
class Heap {
constructor(k) {
this.arr = []
// 维护大顶堆的大小
this.size = k
}
push (val) {
if (this.arr.length < this.size) {
// 如果没达到大顶堆的大小,直接插入,向上调整
this.arr.push(val)
this.sortBack()
} else if (this.arr[0] > val) {
// 如果大于大顶堆的大小并且符合插入条件,替换掉堆顶元素,向下调整
this.arr[0] = val
this.sortFront()
}
}
sortBack () {
// 从最后一位开始向上比较
let ind = this.arr.length - 1
if (ind === 0) return
// 如果没到达堆顶,并且父节点的值小于子节点,则替换两者位置
while (ind > 0 && this.arr[ind] > this.arr[Math.floor((ind - 1) / 2)]) {
[this.arr[ind], this.arr[Math.floor((ind - 1) / 2)]] = [this.arr[Math.floor((ind - 1) / 2)], this.arr[ind]]
// 替换位置之后,从父节点的位置继续往上比较
ind = Math.floor((ind - 1) / 2)
}
}
sortFront () {
// 从堆顶元素开始比较
let ind = 0
// 判断是否有孩子
while (ind * 2 + 1 < this.arr.length) {
// temp 用于保存父节点与孩子节点之间较大值的位置
let temp = ind
if (this.arr[ind * 2 + 1] > this.arr[ind]) temp = ind * 2 + 1
// 有左节点不一定有右节点,所以需要先判https://www.bilibili.com/video/BV1pT4y127BJ/断右节点是否存在
if (this.arr[ind * 2 + 2] !== undefined && this.arr[ind * 2 + 2] > this.arr[temp]) temp = ind * 2 + 2
// 如果当前节点就是最大的节点,跳出当前循环
if (temp === ind) break
// 否则将当前节点与最大的节点进行交换
[this.arr[ind], this.arr[temp]] = [this.arr[temp], this.arr[ind]]
// 交换位置之后,从最大值的位置继续向下进行比较
ind = temp
}
}
}