1. 程式人生 > >從N個數裡面,隨機抽取M個數(可以用作抽獎隨機使用者)

從N個數裡面,隨機抽取M個數(可以用作抽獎隨機使用者)

直接抽取

const cards = Array(62).fill().map((_,i)=>i+1); //初始化一個 1~62 的陣列

function draw(n = 1){ // 一次抽取 n 個,預設一次 1 個
    var ret = [];
    for(var i = 0; i < n; i++){
        let idx = Math.floor(cards.length * Math.random());
        ret.push(...cards.splice(idx, 1));
    }
    return ret;
}
console.log(draw(10)); //抽取一次,10箇中獎者

上面這個方法非常直觀,首先生成一個順序的 1 ~ 62 號的陣列,然後從其中隨機抽取 10 次,為了不重複,將抽取的數字通過 cards.splice(idx, 1) 從原陣列中取出來。

上面這種方式可行,但它不是最好的,因為每次 splice 一個數字,取 10 個數字需要 splice 10 次,這看起來不是特別好。可以想到另一種方法,先對陣列進行“洗牌”,然後一次把 10 個數字取出來:

先洗牌

function draw(amount, n = 1){
    const cards = Array(amount).fill().map((_,i)=>i+1); 

    for(let i = amount - 1; i >= 0; i--){
        let rand = Math.floor((i + 1) * Math.random());
        [cards[rand], cards[i]] =  [cards[i], cards[rand]];
    }
    return cards.slice(0, n);
}
console.log(draw(62, 10));

上面這個版本是月影實際現場寫出的(略有修改),它是不錯的,但是它也有明顯缺點。首先它先把所有的牌都排序了,但實際上只需要排序 10 張牌就好,多餘的排序沒有必要。其次,它不方便連續抽獎,比如第一次抽取 10 個號,然後再想多抽取 5 個號,它就做不到了。

我們先解決第一個問題:

不需要洗所有的牌

function draw(amount, n = 1){
    const cards = Array(amount).fill().map((_,i)=>i+1); 

    for(let i = amount - 1, stop = amount - n - 1; i > stop; i--){
        let rand = Math.floor((i + 1) * Math.random());
        [cards[rand], cards[i]] =  [cards[i], cards[rand]];
    }
    return cards.slice(-n);
}
console.log(draw(62, 10));

上面這個版本是優化過的版本,顯然如果取 10 個數,只需要迴圈 10 次即可,不需要把 64 張牌都洗了。

要解決可以連續抽獎的問題,就需要把 cards 提取出來(就像方案 1 的隨機抽取一樣),但是那樣的話就使得函式有副作用,雖說是臨時寫一個抽獎,也不喜歡設計得太糙。或者,那就加一個構造器執行初始化?

構造器負責初始化

function Box(amount){
    this.cards = Array(amount).fill().map((_,i)=>i+1); 
}
Box.prototype.draw = function(n = 1){
    let amount = this.cards.length, cards = this.cards;

    for(let i = amount - 1, stop = amount - n - 1; i > stop; i--){
        let rand = Math.floor((i + 1) * Math.random());
        [cards[rand], cards[i]] =  [cards[i], cards[rand]];
    }

    let ret = cards.slice(-n);    
    cards.length = amount - n;

    return ret;
}

var box = new Box(62);
console.log(box.draw(5), box.draw(5)); //一次取 5 個,取 2 次

更優雅的解決方式?

實際上,對於一次可能抽取任意多個獲獎人的場景,用 ES6 的 generators 非常合適,我們可以直接拿洗牌的版本略做修改:

function * draw(amount){
    const cards = Array(amount).fill().map((_,i)=>i+1); 

    for(let i = amount - 1; i >= 0; i--){
        let rand = Math.floor((i + 1) * Math.random());
        [cards[rand], cards[i]] =  [cards[i], cards[rand]];
        yield cards[i];
    }
}
var drawer = draw(62);

console.log(Array(10).fill().map(()=>drawer.next().value)); //一次取出10個結果

最後補充一個小技巧,利用 Array(n).fill().map(...) 可以方便快速地構造陣列: