我們的實際開發工作中,經常都會使用到數組隨機選取的功能需求,比如彩票隨機一注,隨機五注,機動車號牌隨機選一個等等。
接下來的內容,來給大家分享一下,我們在開發過程中,常用到的4種數組隨機選取實現方案,這些實現方案都是在實戰中應用的,非常值得大家學習和收藏,我們一起來看看都有哪些方法吧!
這種方式是使用Array.sort()和Math.random()結合的方法,Math.random()返回的是一個0-1之間(不包括1)的偽隨機數,注意這不是真正的隨機數。
var letter=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
function shuffle1(arr) {
return arr.sort(()=> 0.5 - Math.random())
}
console.time("shuffle1");
letter=shuffle1(letter);
console.timeEnd("shuffle1");
console.log(letter);
這種方式并不是真正的隨機,來看下面的例子。對這個10個字母數組排序1000次,假設這個排序是隨機的話,字母a在排序后的數組中每個位置出現的位置應該是1000/10=100,或者說接近100次??聪旅娴臏y試代碼:
let n=1000;
let count=(new Array(10)).fill(0);
for (let i=0; i < n; i++) {
let letter=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
letter.sort(()=> Math.random() - 0.5);
count[letter.indexOf('a')]++
}
console.log(count);
結果如下:
可以看出元素a的位置在0到9出現的次數并不是接近100的。
原因有兩點:
這里sort(()=> 0.5 - Math.random())沒有輸入,跟談不上返回相同的結果,所以這個方法返回的結果不是真正的數組中的隨機元素。
既然(a, b)=> Math.random() - 0.5 的問題是不能保證針對同一組 a、b 每次返回的值相同,那么我們不妨將數組元素改造一下,比如將元素'a'改造為{ value: 'a', range: Math.random() },數組變成[{ value: 'a', range: 0.10497314648454847 }, { value: 'b', range: 0.6497386423992171 }, ...],比較的時候用這個range值進行比較,這樣就滿足了Array.sort()的比較條件。
代碼如下:
let letter=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
function shuffle2(arr) {
let new_arr=arr.map(i=> ({value: i, range: Math.random()}));
new_arr.sort((a, b)=> a.r - b.r);
arr.splice(0, arr.length, ...new_arr.map(i=> i.value));
}
console.time("shuffle2");
letter=shuffle2(letter);
console.timeEnd("shuffle2");
console.log(shuffle2);
輸出結果如下:
我們再使用上面的方式測試一下,看看元素a元素是不是隨機分布的。
let n=1000, count=(new Array(10)).fill(0);
for (let i=0; i < n; i++) {
let letter=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
letter=shuffle2(letter)
count[letter.indexOf('a')]++
}
console.log(count);
結果如下:
從這里可以看出,元素a在位置0到9出現的次數是接近100的,也就是說元素a是隨機分布的,其他的元素也是,這時再從這個新數組中截取前幾個元素就是想要的數組了。
上面的sort算法,雖然滿足了隨機性的需求,但是性能上并不是很好,很明顯為了達到隨機目的把簡單數組變成了對象數組,最后又從排序后的數組中獲取這個隨機數組,明顯走了一些彎路。
洗牌算法可以解決隨機性問題,洗牌算法的步驟如下:
洗牌算法是真的隨機的嗎,換言之洗牌算法真的可以隨機得到n個元素中m個嗎?下面拿一個只有5個元素的數組來說明。
數組有5個元素,如下圖。
從5個元素隨機抽出一個元素和最后一個換位,假設抽到3,概率是1/5,如下圖。注意其他任意4個元素未被抽到的概率是4/5。最終3出現在最后一位的概率是1/5。
將抽到的3和最后一位的5互換位置,最后一位3就確定了,如下圖:
再從前面不確定的4個元素隨機抽一個,這里注意要先考慮這4個元素在第一次未被抽到的概率是4/5,再考慮本次抽到的概率是1/4,然后乘一下得到1/5。注意其他任意3個未被抽到的概率是3/4。5出現在倒數第二位的概率是4/5*1/4=1/5如下圖:
現在最后2個元素確定了,從剩下的3個元素中任意抽取一個,概率是1/3,身下任意2個未被抽到的概率是2/3,但是要考慮上一次未被抽到的概率是3/4,以及上上一次未被抽到的概率是4/5,于是最終1出現在倒數第三位的概率是1/3*3/4*4/5=1/5。
現在倒數3個元素已經確定,剩下的2個元素中任意取一個,概率是1/2,但是要考慮上一次未被抽到的概率是2/3,上上一次未被抽到的概率是3/4,上上上一次未被抽到的概率是4/5,最終4出現在倒數第4位的概率是1/2*2/3*3/4*4/5=1/5。
最后還剩下一個2,它出現在倒數第5位的概率肯定也是1/5。不嫌啰嗦的話可以繼續看下去。
現在倒數4個元素已經確定,剩下1個元素中任意取一個,概率是1,但要考慮上一次未被抽中的概率是1/2,上上一次未被抽中的概率是2/3,上上上一次未被抽中的概率是3/4,上上上上一次未被抽中的概率是4/5,于是2出現在倒數5位置的概率是1*1/2*2/3*3/4*4/5=1/5。
有了算法,下面給出洗牌算法的代碼:
let letter=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
function shuffle3(arr) {
let i=arr.length, t, j;
while (i) {
j=Math.floor(Math.random() * (i--)); //
t=arr[i];
arr[i]=arr[j];
arr[j]=t;
}
}
console.time("shuffle3");
shuffle3(letter);
console.timeEnd("shuffle3");
console.log(letter)
運行結果如下:
還有最后一個問題,我們來驗證一下,還是和上面的方法一樣,隨機排序1000次,看看字母a出現在0-9個位置的概率是多少,理論上應該是1000/10=100。
來看下面的代碼:
let n=1000;
let count=(new Array(10)).fill(0);
function shuffle3(arr) {
let i=arr.length, t, j;
while (i) {
j=Math.floor(Math.random() * (i--)); //
t=arr[i];
arr[i]=arr[j];
arr[j]=t;
}
}
for (let i=0; i < n; i++) {
let letter=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
shuffle3(letter);
count[letter.indexOf('a')]++
}
console.log(count);
結果如下:
可以看到基本上都是接近100的,可以說明洗牌算法是隨機的。
最近開發做了一個模擬彩票游戲的功能,彩票有機選,其實就是隨機選取,下面以雙色球為例來看看實現的效果是什么樣的。
雙色球前區從1到33個小球,后區從1到16個小球,一注彩票中前區至少選6個,后區至少選1個。
這里使用洗牌算法實現,如下圖:
天在小破站看到一個前端生成隨機驗證碼的視頻,感覺很有意思,就跟著操作了一下,成功了。
后來自己又想給它加一個提交按鈕,輸入并判斷驗證碼的正確性,也可以正常運行,但是我的代碼好像還是存在一些bug:
1.canvas標簽是繪圖容器,自帶屬性使它是一個默認300*150的容器,縮小canvs容器時,里面的繪圖也會變小,我沒有找到合適的方法去調整它的大小。
2.對于輸入框input,我使用了float浮動,為了使input輸入框和canvas里面的驗證碼并排,但是被vscode警告了。
這次的實戰練習也經歷了很多坎坷,但是也收獲很大。學習到了canvas標簽的用法、JS全局變量和局部變量、以及有關context的一些屬性和方法。
最后,希望路過的大佬,幫我看看bug,幫幫菜鳥。
附上源碼
圖1
圖2
圖3
*請認真填寫需求信息,我們會在24小時內與您取得聯系。