作者是一个大四的菜鸡,最近在准备秋招的过程中遇到字节的一道算法题,觉得非常有意思,决定写篇文章,和大家一起讨论下。
题目
给你一副去掉大王小王的牌,共52张,请你封装一个getCard的函数,接受一个count的参数,会输出count张随机内容的牌,例如getCard(4),会输出 [黑桃1,红桃3,方块4,梅花5]。这个函数可以多次调用,当多次调用时,之前发过的牌不会再次被发出,一直调用这个函数,直到传入的count的值大于手里的牌的数量,直接输出手里剩余的牌。
思路
一开始我的思路是要用一个数据结构来记录手里的牌,发过的牌直接从这个数据结构里面删掉,于是打算用数组,但是数组的splice方法复杂度有点高,那就用map?map删除的方法复杂度可是O(1)啊,然后再利用Math.random()生成一个随机的索引,就是我随机发出的牌呗!说时迟,那时快,思路一出,马上就开始敲了起来。
//初始化一副去掉大小王的牌
let map = new Map();
let type = ['红桃', '梅花', '方块', '黑桃'];
for (let i = 1; i <= 13; i++) {
for (let j = 0; j < type.length; j++) {
map.set((type[j] + i), true)
}
}
function getCard(count) {
let res = [];
function help() {
//记录我现在手里还有的张牌
let tmpArr = [];
for (let item of map.keys()) {
tmpArr.push(item);
}
//生成一张随机的牌
let cardIndex = Math.floor(Math.random() * tmpArr.length);
let card = tmpArr[cardIndex];
//发完我就立马删掉
map.delete(card);
return card;
}
//手里的牌小于count了,就把所有的牌都发掉
if (map.size <= count) {
res = [...map.keys()];
map.clear();
} else {
for (let i = 0; i < count; i++) {
res.push(help())
}
}
return res;
}
console.log(getCard(48));
/**[
'黑桃2', '方块10', '方块11', '红桃3',
'方块4', '黑桃13', '黑桃7', '梅花6',
'黑桃4', '红桃13', '黑桃1', '红桃4',
'红桃5', '梅花4', '红桃8', '黑桃10',
'梅花12', '红桃10', '红桃11', '方块13',
'梅花10', '黑桃3', '梅花3', '梅花5',
'黑桃5', '方块9', '方块12', '方块7',
'方块1', '黑桃9', '黑桃6', '黑桃11',
'梅花2', '方块2', '方块8', '红桃12',
'红桃2', '方块5', '方块6', '方块3',
'红桃7', '梅花11', '红桃6', '红桃1',
'黑桃12', '梅花7', '红桃9', '梅花9'
]*/
console.log(getCard(8)); // [ '梅花1', '梅花8', '黑桃8', '梅花13' ]
console.log(getCard(8)); // []
磕磕绊绊的终于写完了,写完以后自我感觉良好,感觉用例都输出了,想着面试官是不是可以准备出下一道题了。
面试官很温柔的说:“仔细看你的代码,每次发完一张牌,是不是都要重新洗过一次牌,平时打牌的时候会这样吗?,想想能不能优化。”
心里慌的一批,当时还没想到面试官是啥意思...,只想说我平时都不打牌啊,王者峡谷不香吗?后面恍然大悟,原来是这里...
for (let i = 0; i < count; i++) {
res.push(help()) //循环了count次,洗了count次牌
}
面试官此时看不下去了,提醒了我一下:“你代码里面的tmpArr是必须的吗?那里能优化吗?”
噢~,我仿佛被打通了二脉,马上开始改了起来。
let map = new Map();
let type = ['红桃', '梅花', '方块', '黑桃'];
for (let i = 1; i <= 13; i++) {
for (let j = 0; j < type.length; j++) {
map.set((type[j] + i), true)
}
}
function getCard(count) {
let res = [];
function help() {
//改动了这里,根据还有的map长度生成一个值,和一个随机的牌类型
let cardIndex = Math.floor(Math.random() * map.size);
let typeIndex = Math.floor(Math.random() * type.length);
let card = typeIndex + cardIndex;
map.delete(card);
return card;
}
if (map.size <= count) {
res = [...map.keys()];
map.clear();
} else {
for (let i = 0; i < count; i++) {
res.push(help())
}
}
return res;
}
console.log(getCard(48));
console.log(getCard(8));
console.log(getCard(8));
面试官看了一眼便说:“比如我已经发了红桃9,你这样写是不是红桃9还是会被发。这里还是有问题的”
此时我更慌了...,想了一会便放弃了,于是开始了后面的问题。
思考
后面自己进行面试复盘的时候,突然灵机一动,知道如何优化了!
每次调用getCard函数的时候,进行发牌的时候,我只要随机生成一个位置就行了,然后在这个位置的基础上,进行连续的发牌,这样问题不就迎刃而解了吗!
let map = new Map();
let type = ['红桃', '梅花', '方块', '黑桃'];
//初始化一副牌
for (let i = 1; i <= 13; i++) {
for (let j = 0; j < type.length; j++) {
map.set((type[j] + i), true)
}
}
function getCard(count) {
let res = [];
function help(count) {
let tmpArr = [];
let res2 = [];
for (let item of map.keys()) {
tmpArr.push(item);
}
let position = Math.floor(Math.random() * (tmpArr.length - count));
while(count){
res2.push(tmpArr[position])
map.delete(tmpArr[position]);
position++;
count--;
}
return res2;
}
if (map.size <= count) {
res = [...map.keys()];
map.clear();
} else {
res = help(count)
}
return res;
}
console.log(getCard(23));
/*
[
'方块4', '黑桃4', '红桃5',
'梅花5', '方块5', '黑桃5',
'红桃6', '梅花6', '方块6',
'黑桃6', '红桃7', '梅花7',
'方块7', '黑桃7', '红桃8',
'梅花8', '方块8', '黑桃8',
'红桃9', '梅花9', '方块9',
'黑桃9', '红桃10'
]*/
console.log(getCard(24));
/*
[
'红桃2', '梅花2', '方块2',
'黑桃2', '红桃3', '梅花3',
'方块3', '黑桃3', '红桃4',
'梅花4', '梅花10', '方块10',
'黑桃10', '红桃11', '梅花11',
'方块11', '黑桃11', '红桃12',
'梅花12', '方块12', '黑桃12',
'红桃13', '梅花13', '方块13'
]
*/
console.log(getCard(10));
//[ '红桃1', '梅花1', '方块1', '黑桃1', '黑桃13' ]
这样才算解决了这个问题,害,只恨当时太匆匆!没有给面试官讲出来...
那还可以进行优化吗?答案是可以的。
上面的方法还有一个缺点,就是我们在每次调用getCard函数的时候,都要进行一次洗牌,但实际上我们只要在初始化扑克牌的时候,就将其打乱就好了!
//打乱牌的函数
function shuffle(arr) {
for (let i=arr.length-1; i>=0; i--) {
let rIndex = Math.floor(Math.random()*(i+1));
// 打印交换值
// console.log(i, rIndex);
let temp = arr[rIndex];
arr[rIndex] = arr[i];
arr[i] = temp;
}
return arr;
}
let map = new Map();
let allCards = [];
let type = ['红桃', '梅花', '方块', '黑桃'];
//初始化一副牌
for (let i = 1; i <= 13; i++) {
for (let j = 0; j < type.length; j++) {
allCards.push(type[j]+i)
}
}
allCards = shuffle(allCards)
for(let i = 0; i < allCards.length;i++){
map.set(allCards[i],true)
}
function getCard(count) {
let res = [];
function help(count) {
let tmpArr = [];
let res2 = [];
for (let item of map.keys()) {
tmpArr.push(item);
}
let len = tmpArr.length;
while(count){
res2.push(tmpArr[len-count])
map.delete(tmpArr[len-count]);
count--;
}
return res2;
}
if (map.size <= count) {
res = [...map.keys()];
map.clear();
} else {
res = help(count)
}
return res;
}
console.log(getCard(23));
console.log(getCard(25));
console.log(getCard(10));
拓展
关于这种洗牌算法,还有还有很多,例如把一副牌随机打乱,能不能转换成把一个数组打乱呢?
function shuffle(arr) {
for (let i=arr.length-1; i>=0; i--) {
let rIndex = Math.floor(Math.random()*(i+1));//为什么生成这个范围的
// 打印交换值
// console.log(i, rIndex);
let temp = arr[rIndex];
arr[rIndex] = arr[i];
arr[i] = temp;
}
return arr;
}
shuffle([1,2,3,4,5,6]); // [1, 5, 3, 6, 4, 2]
那怎么证明我们是随机打乱的呢?
// 使用 res 存储结果
let res = {};
let times = 100000;
for (let i=0; i<times; i++) {
// 使用 [1, 2, 3] 进行简单测试
let key = JSON.stringify(shuffle([1, 2, 3]));
res[key] ? res[key]++ : res[key] = 1;
}
for (let key in res) {
res[key] = Number.parseFloat(res[key]/times *100 ).toFixed(3) + '%';
}
// 从结果可以看出是实现了真正的乱序的
res:
/*
[1,2,3]: "16.514%"
[1,3,2]: "16.764%"
[2,1,3]: "16.606%"
[2,3,1]: "16.587%"
[3,1,2]: "16.712%"
[3,2,1]: "16.817%"
*/
固定一个值,再进行打乱。
在乱序的同时,固定一个下标的值,使其位置不变,方法有很多,这里只给出一种:
function shuffle(arr, index) {
let res = [];
// 取出固定值
let fix = arr.splice(index, 1)[0];
for (let i=arr.length-1; i>=0; i--) {
let rIndex = Math.floor(Math.random()*(i+1));
res.push(arr[rIndex]);
arr.splice(rIndex, 1);
}
// 将固定值放入指定位置
res.splice(index, 0, fix);
return res;
}
// 多次运行,可以看出数组下标为 1 的值始终是固定的
shuffle([1,2,3,4,5,6], 1);
// [5, 2, 6, 3, 1, 4]
// [5, 2, 6, 3, 4, 1]
// [3, 2, 4, 6, 1, 5]
这里同样测试一下是否实现了真正的乱序:
let res = {};
let times = 100000;
for (let i=0; i<times; i++) {
// 使用 [1, 2, 3] 进行简单测试,固定数组下标 1 的值
let key = JSON.stringify(shuffle([1, 2, 3], 1));
res[key] ? res[key]++ : res[key] = 1;
}
for (let key in res) {
res[key] = Number.parseFloat(res[key]/times *100 ).toFixed(3) + '%';
}
// 固定的同时,依然是乱序的
res;
/*
[1,2,3]: "49.976%"
[3,2,1]: "50.024%"
*/
晚上匆匆写的,肯定还要错误的地方,希望大家可以指正。
祝大家早日拿到满意offer!!!