【路飞】最小的 k 个数

149 阅读2分钟

记录 1 道算法题

最小的 k 个数

leetcode-cn.com/problems/zu…

最简单的做法就是先将数组排序,然后截取前面 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,  剩下 241
    left 21       right 4
    
    递归处理 214
    2, 1  基准 2, 剩下 1
    left 1          right []
    返回 12
    返回 4
    返回 1234

取前 k 个做的改造就是只需要递归处理长度为 k 的数组。即 left + pivot(基准) + right 长度为k。

  • 如果 left.length 为 k, 那么就只要 return left的递归处理就好了, right可以直接丢弃。
  • 如果 left.lengthk - 1,那么说明要返回 leftpivot
  • 如果 left.length + 1 是 k ,那么说明 还要从 rightk - 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 没有大根堆所以需要自己实现一个。

大根堆的特点是

  1. 堆顶的数值是堆内最大的。
  2. 堆内有固定数目的数值。

那么我们可以一个个存入堆中,推入的时候需要升序排列,保证最大的数保持在数组的结尾。

然后再实现一个对比的方法。如果给的数 比数组的结尾的数 小,那么弹出数组的数,然后用上面的插入方法,放在数组合适的位置。使数组保持升序。但是说实话,这种方法,是把堆内的数遍历了多次,可能这就是慢的地方吧。

    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)
          }
        }
    }

以上是性能并没有多好的三种实现方法的代码。只是一个思路和练习,之后会再去找找性能更好的代码怎么写。随缘更新这篇文章。