记录 1 道算法题
最小的 k 个数
最简单的做法就是先将数组排序,然后截取前面 k 个
function getLeastNumbers(arr, k) {
return arr.sort((a, b) => a - b).slice(0,k)
}
稍微复杂一点的做法有大根堆和快排,但是我写出来执行时间挺长的,如果还有其他优化的地方请指教。
首先是快排
快排是 将数组第一个数作为基准,然后遍历剩余的数,大于基准就放入right,小于则放入left,最后返回 [...left, pivot, ...right]。递归处理left 和 right。最后的终止条件是 数组的长度 <= 1 则直接返回数组。
3, 2, 4, 1 基准 3, 剩下 2, 4, 1
left 2, 1 right 4
递归处理 2,1 和 4
2, 1 基准 2, 剩下 1
left 1 right []
返回 1,2
返回 4
返回 1, 2, 3, 4
取前 k 个做的改造就是只需要递归处理长度为 k 的数组。即 left + pivot(基准) + right 长度为k。
- 如果
left.length为 k, 那么就只要return left的递归处理就好了, right可以直接丢弃。 - 如果
left.length是k - 1,那么说明要返回left加pivot。 - 如果
left.length + 1是 k ,那么说明 还要从right拿k - left.length - 1。
我们发现无论是哪种情况,都要递归计算 left。
然后为了方便处理 pivot , 我们可以推入 left。反正如果长度够了 会截取 left.slice(0, k)。如果放在 right 要处理多出 pivot 一个长度的情况,很麻烦。
function getLeastNumbers(arr, k) { // 取前 k 个数
// 递归的终止条件
if (arr.length <= 1) return arr
// 基准
const pivot = arr[0]
let left = []
let right = []
// 从第二个开始摆放
for (let i = 1; i < arr.length; i++) {
const a = arr[i]
if (a > pivot) {
right.push(a)
} else {
left.push(a)
}
}
// 先处理 left,已经排序好的新left
left = getLeastNumbers(left, left.length)
// 排序好的left 推入 基准,这时已经是升序了
left.push(pivot)
if (left.length >= k) {
// 如果长度满足,直接返回
return left.slice(0, k)
} else {
// 长度还差一点,排序 right, 只取 k - left.length 个
// 因为pivot 已经处理了,所以是 k - left.length
right = getLeastNumbers(right, k - left.length)
// 返回已经排序好的数组
return [...left, ...right]
}
}
另一种是大根堆。从开头到这里,三个方法,执行时间是一个比一个长= =
因为js 没有大根堆所以需要自己实现一个。
大根堆的特点是
- 堆顶的数值是堆内最大的。
- 堆内有固定数目的数值。
那么我们可以一个个存入堆中,推入的时候需要升序排列,保证最大的数保持在数组的结尾。
然后再实现一个对比的方法。如果给的数 比数组的结尾的数 小,那么弹出数组的数,然后用上面的插入方法,放在数组合适的位置。使数组保持升序。但是说实话,这种方法,是把堆内的数遍历了多次,可能这就是慢的地方吧。
function getLeastNumbers(arr, k) {
// 生成实例
const heap = new Heap()
// 把 前面 k 个数 放入堆中
for (let i = 0; i < k; i++) {
heap.add(arr[i])
}
// 遍历剩下的数,和堆顶的数进行比较
for (let i = k; i < arr.length; i++) {
heap.compare(arr[i])
}
// 返回堆内维护的数组
return heap.data
}
class Heap {
constructor() {
this.data = []
}
add(val) {
const { data } = this
// 因为是升序,所以遍历时从后面开始遍历堆内的数。
// 触发插入的数小得离谱。
let i = data.length - 1
// 这里用一个 do while,是无论如何都先执行一遍,
// 是为了 第一次插入的时候 堆还是空的,i = 0, while就不能写 > 0
do {
// 这里是考虑到了 undefined > 任何数都是 false
// 是为了可以在任何地方插入,包括 堆是空的时候。
if (!(data[i] > val)) {
// i + 1 是因为想要插入到目标的后面
return data.splice(i + 1, 0, val)
}
} while (i-- >= 0)
}
compare(val) {
// 如果堆顶的数大于 就会被替换
if (this.data[this.data.length - 1] > val) {
this.data.pop()
this.add(val)
}
}
}
以上是性能并没有多好的三种实现方法的代码。只是一个思路和练习,之后会再去找找性能更好的代码怎么写。随缘更新这篇文章。