[路飞]_最小的k个数

129 阅读3分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

剑指 Offer 40. 最小的 k 个数
b站视频

题目介绍

输入整数数组 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

解题思路

这道题的解法可以很简单,一行代码就可以搞定,但我们不只是为了做题而做题,因此可以发散思维多思考几种方法,本文将讲解 3 种解题思路

思路一

思路一是利用 js 数组中自带的排序方法,将传入的数组按从小到大进行排序,然后取排序后数组的前 k 位,就是最小的 k 个数

解题代码

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

思路二:快速排序

快速排序同样是将传入的数组按从小到大排列,然后取排序后数组的前 k 位,但是快速排序的效率会比 js 中自带的排序效率高一点

快速排序的步骤

  1. 取数组的第一个位置作为比较的基准值 base,然后定义两个游标 lr 分别指向数组的第一位和最后一位
  2. 如果 l 的下标比 r 小,则往前移动 r 的下标,直到找到小于基准值 base 的值,或者 r 的下标小于等于 l 的下标为止
  3. 如果 l 的下标比 r 小,则往后移动 l 的下标,直到找到大于基准值 base 的值,或者 r 的下标小于等于 l 的下标为止
  4. 交换 lr 位置的值
  5. 重复 2-4 的步骤,直到 l 的下标大于等于 r 的下标
  6. basel 位置的值进行交换,此时 base 将数组分成两边,左边全部为小于 base 的值,右边全部为大于 base 的值
  7. base 左右两边的数组重复快速排序 1-6 的过程,直到排序结束
  8. 返回排序后的前 k 个数即为最小 k 个数

最小的k个数-快速排序.gif

解题代码

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

思路三:利用大顶堆

另一种思路是可以利用一个固定大小的大顶堆,如果数据量没有达到大顶堆的大小,那么直接往大顶堆插入值,并且调整大顶堆的结构,如果数据量大于大顶堆的大小,则看要插入的数值是否大于堆顶元素的值,如果大于等于,则不插入,否则将堆顶元素替换为插入的值,然后调整大顶堆的结构

大顶堆的构造方法

  1. 定义 Heap 类,定义 push 方法,如果没有达到大顶堆的大小,直接插入元素,并向上调整;如果超过大顶堆的值并且符合插入条件,则将堆顶元素替换为插入元素,并向下调整
  2. 定义向上调整方法 sortBack,从插入元素的位置,依次与其父节点进行比较,如果大于父节点,则交换两者位置,然后继续往上比较,比较到堆顶为止
  3. 定义向下调整方法 sortFront,从堆顶元素,依次与其孩子节点进行比较,最大的值与父节点交换位置,然后继续向下比较,直到最后一个元素
  4. 最后返回大顶堆中的元素,则为最小的 k 个数

最小的k个数-大顶堆.gif

解题代码

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