一起养成写作习惯!这是我参与「掘金日新计划 · 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直接爆了...
想想应该是重复取random的时候重复了太多次导致出问题了。看来还是方法二好。
不过话说回来,方法一有没有什么修正的办法呢?可以让取random的时候不要取重,感觉还可以再思考思考。
需要说明的是,我并不是很了解js底层的运行逻辑,此处的比较也没有做的非常的完善(比如如果等分成很多小份,是不是结果会有所不同),大家粗略的了解一下即可。如果有大佬能在评论区分析一下这个问题,感谢您的帮助。