[前端]_一起刷leetcode 剑指 Offer 40. 最小的k个数

128 阅读3分钟

大家好,我是挨打的阿木木,爱好算法的前端摸鱼老。最近会频繁给大家分享我刷算法题过程中的思路和心得。如果你也是想提高逼格的摸鱼老,欢迎关注我,一起学习。

题目

剑指 Offer 40. 最小的k个数

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

暴力解法(节省键盘寿命)

思路

  1. 对整个数组进行从小到大的排序;
  2. 用数组的slice方法复制前k个元素。

实现

/**
 * @param {number[]} arr
 * @param {number} k
 * @return {number[]}
 */
var getLeastNumbers = function(arr, k) {
    return arr.sort((a, b) => a - b).slice(0, k);
};

结果

image.png

一行代码搞定,简直完美~

一看执行用时,好家伙这也太慢了。那我们正儿八经的想一想有没有什么解法是更快的。其实每种排序我们加个限制,排到第k位就结束,起码能打败50%的选手,但是我们如果想追求尽可能找到最优解的话,首选快速排序。为什么呢?首先明确快速排序的特性,就是快嘛。然后我们每一次找基准分成左右两个部分,左边存放比基准值小的,右边存放大的。那么当左边部分的长度小于k时,我们都不用进行排序就找到了前面部分。所以这种方式我们只需要找数量够了即可,效率是最高的。

快速排序Bug版

/**
 * @param {number[]} arr
 * @param {number} k
 * @return {number[]}
 */
var getLeastNumbers = function(arr, k) {
    let result = [];

    // 长度不够k就进行快排拿
    while (result.length < k) {
        let [ minArr, maxArr ] = findMinArray(arr);

        if ((result.length + minArr.length) <= k) {
            result = result.concat(minArr);
            arr = maxArr;
        } else {
            arr = minArr;
        }

    }

    return result;
};

// 找到数值比较小的那部分
function findMinArray(arr) {
    // 左边比基准值小, 右边比基准值大
    let minArr = [], maxArr = [];

    // 佛系取个基准
    let base = Math.random() * arr.length | 0;
    const n = arr.length;

    // 切割成左右两个模块
    for (let i = 0; i < n; i++) {
        if (arr[i] <= arr[base]) {
            minArr.push(arr[i]);
        } else {
            maxArr.push(arr[i]);
        }
    }

    return [ minArr, maxArr ];
}

本来想着一套乱披风锤法带走,但是很不幸,发现没办法通过所有的测试用例。因为这里想快速把小于某个基准值的放在左边,大于的放在右边。结果我们要找的数组,刚好有重复的值在基准的左右两边,把人整傻眼了。

image.png

找到了问题,那么我们来写个if语句对症下药。每轮统计最小值数组的时候,顺带记录一下他们的值是不是全部相同,避免陷入无限循环中去。

最终代码

/**
 * @param {number[]} arr
 * @param {number} k
 * @return {number[]}
 */
var getLeastNumbers = function(arr, k) {
    let result = [];

    // 长度不够k就进行快排拿
    while (result.length < k) {
        let [ minArr, maxArr, repeat ] = findMinArray(arr);

        if ((result.length + minArr.length) <= k) {
            result = result.concat(minArr);
            arr = maxArr;
        } else if (repeat) {
            // 如果全是重复元素的话,切一段补上即可
            result = result.concat(minArr.slice(0, k - result.length));
        } else {
            arr = minArr;
        }

    }

    return result;
};

function findMinArray(arr) {
    // 左边比基准值小, 右边比基准值大
    let minArr = [], maxArr = [];

    // 佛系取个基准
    let base = Math.random() * arr.length | 0;
    const n = arr.length;

    // 数组从0开始,我们取个它永远得不到的值
    let prevCount = -1;
    // 是否全都一样
    let repeat = true;

    // 切割成左右两个模块
    for (let i = 0; i < n; i++) {
        if (arr[i] <= arr[base]) {
            minArr.push(arr[i]);

            // 记录重复元素
            if (prevCount === -1) {
                prevCount = arr[i];
            } else if (repeat && prevCount !== arr[i]) {
                repeat = false;
            }
        } else {
            maxArr.push(arr[i]);
        }
    }

    return [ minArr, maxArr, repeat ];
}

结果

image.png

看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。