js隨機數生成器的擴充套件

lhyt發表於2019-02-25

0.前言

給你一個能生成隨機整數1-7的函式,就叫他生成器get7吧,用它來生成一個1-11的隨機整數,不能使用random,而且要等概率。

getx就是指一個能生成1到x的隨機數的函式

主角:get7(你們所有人都沒有random這個技能,全都disable了)

function  get7() {
	return ~~(Math.random()*7)+1 //規則:整篇文章,唯一能用random的地方
}
複製程式碼

1.擴充套件+分割槽

既然是擴充套件,那麼我給小範圍隨機數生成器擴充套件個幾倍,再擷取目標隨機數範圍不就得了。 先問一下,怎麼用get7能實現一個合格的get14?這樣子?

function get14(){
	return get7() +get7() 
}
複製程式碼

我們來測試一下:

    	var obj = Object.create(null)
    	for(var i = 0;i<1000000;i++){
    		var n = get14();
    		if(!obj[n]){
    			obj[n] = 1
    		}else{
    			obj[n] ++
    		}
    	}
    	console.log(obj)
複製程式碼

先不說最小值的問題,首先,他不是等概率的序列 那麼我們探討另一個問題,究竟get14能不能直接用get7加減乘除就表示出來?

喂,說get7() 乘以11/7的那個,你確定沒問題?

1.1 擴充套件

既然是小範圍隨機擴充套件到大範圍,那麼肯定離不開小範圍隨機數生成器get7的多次呼叫。當然我們最終目標很明確,目標隨機數生成器get11,它的每一個隨機數都會等概率對映到get7的擴充套件序列裡面:

js隨機數生成器的擴充套件

然後我們很快就可以想到一個公式:

a*(getx - 1) + getx
複製程式碼

a是個整數,整個公式含義是,把getx擴充套件為a倍,並且實現等概率分佈。比如x是3,我們想擴充套件成2倍,a=2,就是2*(get3 - 1) + get3,範圍是1-7,但是,我們看看概率:

 x\y   1  2  3
  0    1  2  3 
  2    3  4  5
  4    5  6  7   =》1-7的概率是1:1:2:1:2:1:1
複製程式碼

明顯這個公式還有前提

1.2 a取值範圍

我們再看a = 1:

x\y 1  2  3
 0  1  2  3 
 1  2  3  4
 2  3  4  5   =》1-5的概率是1:2:3:2:1
複製程式碼

好像矩陣每一行都是有交集

//如果a是3,ran3 - 1生成0-6 ,ran3 生成 1-3
x\y  1  2  3
0  1  2  3 
3  4  5  6
6  7  8  9   =》1-9等概率

//如果a是4,ran3 - 1生成0-8 ,ran3 生成 1-3
x\y 1  2  3
0  1  2  3 
4  5  6  7
8  9  10 11   =》數字都是等概率出現的,只是中間缺失了一些數,但不影響大局
複製程式碼

所以,只要保證所有的數等概率出現,先滿足對映表最大值大於等於自身的平方(3*3 = 9,即a至少要是3) 為什麼呢?因為不足本身,必然有交集,就像上面1和2兩個矩陣每一行都有交集,只要大於本身的大小,矩陣每一行就不會有交集,沒有交集,那它們就可以等概率 所以,對於7想擴充套件一個等概率序列,get14(get小於49都是沒用,不是等概率)顯然是沒用的,起碼要get49,此後所有的序列長度都是49。所以一個get14得通過get49得到,我們也可以從get49到get11了

1.3 從get49到get11

function get49(){
    var n = 7*(get7()-1) + get7() //a*(getx - 1) + getx,a取7,不信自己列印看一下
    return  n
}
var obj = Object.create(null)
for(var i = 0;i<1000000;i++){
	var n = get49();
	if(!obj[n]){
		obj[n] = 1
	}else{
		obj[n] ++
	}
}
console.log(obj)
複製程式碼

既然我們看見全部元素等概率出現了,那我們只要把多餘的排除掉就行,即是遇到不是我們想要的範圍那些,重新生成一次。

function get11(){
    var n = 7*(get7()-1) + get7() //a*(getx - 1) + getx,a取7,不信自己列印看一下
    return  n > 11?get11():n
}
複製程式碼

改改get49就行,對了,好像擴充套件陣列太多沒用的了,我們應該提高擴充套件陣列的利用率,並且減小遞迴次數

function get11(){
	var n = 7*(get7()-1) + get7() 
	return  n>44?get11():~~((n-1) / 4)+1
}
複製程式碼

2.二進位制法

對小隨機數函式進行二進位制劃分,一半表示1一半表示0,然後用二進位制表示大隨機數,再去除多餘的 get7到get11,8<11<16,我們取4位二進位制,也就是取4次get7 因為7是奇數,我們就去掉一個吧,那我們去掉1,當遇到1重新生成一次,剩下的劃分二等分

//獲取二進位制序列
function getBinary(){
	var n = get7()
	if(n>4){
		return 1
	}else if(n>1){
		return 0
	}else{
		return getBinary()
	}
}
//二進位制序列轉化回去
function get11_by_binary(){
	var res = ''
	for(var i = 0;i<4;i++){
		res += getBinary()
	}
	res = parseInt(res,2)
	return res>11?get11_by_binary():res
}
複製程式碼

當然,效能會差很多,因為太多遍歷了。通用版本也不難,可以自己封裝一個。

3. 總結

其實第一種方法叫做拒絕取樣。我們知道等概率生成某個範圍的隨機數,想通過這個函式生成一個更小範圍的隨機數,就應該這樣子:超過預期範圍,重新抽取,所以叫做拒絕取樣。 基本的操作:

//我們還是用get7獲取1到小於7的隨機數
function getn(n){//n是小於7的正整數
    var num = get7()
    return num > n?getn(n):num
}

//while形式
function getn(n){
	var t
	do{
		t = get7()
	}while(t>n)
	return t
}
複製程式碼

那我們get14就可以很靈活獲得了,7*2得到14是吧,那就來:

function get14(){
	var t
	do{
		t = get7()
	}while(t>2)//我們就叫他get2吧
	return get7() + 7 * (t -1) //上面的a*(getx - 1) + getx公式的變種,這個a等於7
}
複製程式碼

都get14了,那get11還會遠嗎,大於11就拒絕取樣咯。前面說先要get49?這只是一個循序漸進的過程,這樣子你可以深刻理解到這個過程要怎麼來,是不是感覺拒絕取樣很靈活?

公式推廣: 已知生成器getn能生成1-n的隨機數,那麼由getn拒絕取樣得到的新生成器geta和getb(a,b都不大於n),可以生成get(a*b):

get(a*b) = geta + a*(getb-1)//公式是對稱的,可以交換a和b


//上面的例子用公式解釋
get14() = get7() + 7 * (get2() -1) = get2() + 2*(get7() -1)
//其實get10也可以
get10() = get5() + 5 * (get2() -1) = get2() + 2*(get5() -1)
複製程式碼

get7能獲得get2,那get14就可以得到了還可以獲得get5,那get10也是可以做到。剛剛好就是最完美的,如果目標生成器是質數,就讓拒絕取樣次數儘量少,也就是儘量靠近目標。這種隨機數擴充套件, 套路就是超過的拒絕取樣,不足的利用加法和乘法使得剛剛好到目標範圍或者超過目標

相關文章