从笔试题讲起:给出一个数组,将其随机打乱成n个数组并返回

191 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

1.从一道简单的笔试题讲起

昨天参加某公司前端实习生笔试,最后一题是给出一个数组,将其随机打乱成n个数组并返回,题目本身不难,但是我却卡了很久bug,主要是太久没有写代码了,今天记录一下这一题的两种解法。

首先对题目进行分析:

1.肯定要使用Math.random(),这是很好想到的。

2.需要不重复的打乱,也就是说父数组的元素在子数组中总共只能出现一次。这题的难点就在这里了。

说是难点其实也非常简单,主要昨天玩了太久根本没有认真笔试了...

下面给出两种实现的方法:

1.用一个数组记录是否使用过某个元素,如果没有使用过就分配给子数组,如果使用过了就重新随机选取。

2.将父数组的某个元素分配给子数组之后就删除这个元素,以防止重复取。

方案一

const divideArr = function(arr,num) {
    let sonArrList = [];
    // 先全部设置为false未使用
    let used = Array(arr.length).fill(false);
    // 每个子数组的长度
    let len = arr.length / num;
    
    for(let i = 0; i < num; i++) {
        let sonArr = [];
        for(let i = 0; i < len; i++) {
            setItem(arr,sonArr);
        }
        sonArrList.push(sonArr);
    }
    return sonArrList;
    
    function setItem(arr,sonArr) {
        let itemIndex = Math.floor(Math.random() * arr.length);
        // 未使用直接使用,并标记使用了
        if(used[itemIndex] == false) {
            sonArr.push(arr[itemIndex]);
            used[itemIndex] = true;
        }else {
        // 使用过重新寻找
            setItem(arr,sonArr)
        }
    }
} 

方案二

const divideArr = function(arr,num) {
    let sonArrList = [];
    // 每个子数组的长度
    let len = arr.length / num;
    // 先克隆一份arr避免修改,保持纯函数
    let arrCopy = [...arr];
    for(let i = 0; i < num; i++) {
        let sonArr = [];
        for(let i = 0; i < len; i++) {
            setItem(arrCopy,sonArr);
        }
        sonArrList.push(sonArr);
    }
    return sonArrList;
    
    function setItem(arr,sonArr) {
        let itemIndex = Math.floor(Math.random() * arr.length)
        sonArr.push(arr[itemIndex]);
        // 删除使用过的元素
        arr.splice(itemIndex,1)
    }
} 

2.无法等分的提醒

通过以上的两个方案,我们已经实现了将数组等分成arr.length / num的长度,但是实际工作中,不一定能够等分成num个数组,比如假设我们有13个元素,需要分成3组,这三组必然是不同长度。此时如果我们用上面的方法就会出现末尾数组分配到undefined。当然我们实际上也只能接受这样的结果,但是我们应该提醒一下使用函数的人:你想清楚了,现在可能会出现undefined

严格的话我们可以直接抛出错误,不严格的话我们应该提醒。

function alertCanNotTotallyDivide(arr,num) {
    let len = arr.length;
    if(len % num !== 0) {
        // 抛出错误
        // throw new Error(`传入的arr长度为${len},需要等分成${num}份,无法实现。`)
        // 进行提醒
        console.log(`传入的arr长度为${len},需要等分成${num}份,无法实现。`)
    }
}

3.效率对比

可能有同学会想,既然有两种解决方式,那么应该使用哪一种呢?这时候我们就需要进行运行效率的比较了。

我们可以使用一个很大的数组,比如一个包含2的16次方的数字的数组,然后考虑将其等分的所需的时间。

说做就做,我们先生成一个包含[1-2**16]的数组,然后用时间戳比较两种方法运行的时长,这里我们统一等分成4份试试看。

let arr = [];
for(let i = 0; i < Math.pow(2,20); i++) {
    arr.push(i)
}
// divideArr1也做如此处理,因为它比较长,所以用方法2做说明
const divideArr2 = function(arr,num) {
    let prev = Date.now();
    let sonArrList = [];
    // 每个子数组的长度
    let len = arr.length / num;
    // 先克隆一份arr避免修改,保持纯函数
    let arrCopy = [...arr];
    for(let i = 0; i < num; i++) {
        let sonArr = [];
        for(let i = 0; i < len; i++) {
            setItem(arrCopy,sonArr);
        }
        sonArrList.push(sonArr);
    }
    let cur = Date.now();
    console.log(`该方法运行了${cur - prev}毫秒`)
    return sonArrList;
    
    function setItem(arr,sonArr) {
        let itemIndex = Math.floor(Math.random() * arr.length)
        sonArr.push(arr[itemIndex]);
        // 删除使用过的元素
        arr.splice(itemIndex,1)
    }
} 

结果发现方法1直接爆了...

image.png

想想应该是重复取random的时候重复了太多次导致出问题了。看来还是方法二好。

不过话说回来,方法一有没有什么修正的办法呢?可以让取random的时候不要取重,感觉还可以再思考思考。

需要说明的是,我并不是很了解js底层的运行逻辑,此处的比较也没有做的非常的完善(比如如果等分成很多小份,是不是结果会有所不同),大家粗略的了解一下即可。如果有大佬能在评论区分析一下这个问题,感谢您的帮助。