基于一道字节面试算法题的思考

889 阅读3分钟

f2c15e8c2fc7e0a00f268920cfe7030d.jpeg

作者是一个大四的菜鸡,最近在准备秋招的过程中遇到字节的一道算法题,觉得非常有意思,决定写篇文章,和大家一起讨论下。

题目

给你一副去掉大王小王的牌,共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!!!