三年前微信紅包爆火的時候,腦補了下背後的分配原理,並用C寫了個demo,現在回想以爲當時的解法有必定的趣味性,遂豐富完整了下,用js重寫了一遍。git
紅包算法需知足的規則以下:github
我腦補的第一畫面就是:排排坐,分果果
。算法
因而分配原理以下:後端
衆人們先按搶紅包的順序依次入座,圍成圓環,將金額均分到每一個人,而後每人同時將本身手中的金額隨機抽出部分給左右臨近的2我的,但保證手頭至少剩餘1單位的金額
,完成分配。數組
- 獲取分配總額
因爲弱類型語言可變換莫測的入參,在拿到總金額數字的時候必須抖個機靈作下過濾,這裏使用了jonschlinkert大神寫的is-number函數,用於判斷入參是不是數字,不然置它爲0;另外,爲了規避js中小數運算的精度問題,該算法中只使用整數進行加減,即將小數放到位整數(乘倍數),運算後再縮小回原來倍數(除倍數)。微信
class RandomSplit{
constructor(num){
// 實際總數
this.num = this.getNum(num);
// 放大倍數
try{
this.multiple = this.num.toString().split('.')[1].length;
}catch(e){
this.multiple = 0;
}
// 用於整數運算的總數
this.calcNum = this.num * Math.pow(10, this.multiple);
}
// 判斷是否爲number(取用至「is-number」)
isNumber(num){
let number = +num;
if((number - number) !== 0){
return false;
}
if(number === num){
return true;
}
if(typeof num === 'string'){
if(number === 0 && num.trim() === ''){
return false;
}
return true;
}
return false;
}
// 獲取數字
getNum(num, defaultNum = 0){
return this.isNumber(num) ? (+num) : defaultNum;
}
}
複製代碼
- 環形入座,將總數按份數均分
看「環形」二字,彷彿須要使用雙向循環鏈表,爲節省代碼,這裏只用一維數組模擬其效果,在數組首尾作數據銜接便可。在該算法中,全部用於分配交換的數字的原子單位都是整數1,因此均分也須要均分爲整數,例如總數15
均分爲6
份,先每份分到2
(Math.floor(15/6)===2),還餘3
(15%6===3),爲了使後面用於計算的機率儘量平均,咱們須要把這餘下的3
個單位均勻灑落到那6份裏面,相似過程以下圖:echarts
同理,若想要均分地更加精確,可提供精度的位數,而後將總數按該位數放大,整數均分後每份再按該精度位數縮小。dom
因而均分函數以下:函數
// 均分份數, 均分精度, 是否直接返回放大後的整數
average(n, precision, isInt){
precision = Math.floor(this.getNum(precision, 0));
n = Math.floor(this.getNum(n));
let calcNum = this.calcNum * Math.pow(10, precision<0 ? 0 : precision);
// 份數超過放大後的計算總數,即不夠分的狀況
if(n > calcNum){
return [];
}else{
let index = 0;
// 平均數
let avg = Math.floor(calcNum / n);
// 剩餘數
let rest = calcNum % n;
// 剩餘數填充間隔
let gap = Math.round((n-rest) / rest) + 1;
// 原始平均數組
let result = Array(n).fill(avg);
//
while (rest > 0) {
index = (--rest) * gap;
result[index>=n ?(n-1) : index]++;
}
// 返回放大後的結果數組
if(isInt){
return result;
}
// 返回處理完符合精度要求的結果數組
return result.map((item) => {
return (item / Math.pow(10, this.multiple + precision));
});
}
}
複製代碼
測試效果以下:測試
- 相鄰隨機交換
獲得均分數額後,每一個位置先隨機出將要給出的數額,該數額大於等於0且小於本身的初始數額,再將該數額隨機劃分爲兩份,分別給到相鄰的左右位置。
// 隨機劃分的份數, 劃分精度
split(n, precision){
n = Math.floor(this.getNum(n));
precision = Math.floor(this.getNum(precision, 0));
// 均分
let arr = this.average(n, precision, true);
let arrResult = arr.concat();
for (let i = 0; i < arr.length; i++) {
//給出的總額
let num = Math.floor(Math.random() * arr[i]);
// 給左鄰的數額
let numLeft = Math.floor(Math.random() * num);
// 給右鄰的數額
let numRight = num - numLeft;
// 首尾index處理
let iLeft = i===0 ? (arr.length-1) : (i-1);
let iRight = i===(arr.length-1) ? 0 : (i+1);
arrResult[i] -= num;
arrResult[iLeft] += numLeft;
arrResult[iRight] += numRight;
}
// 縮小至原尺度
return arrResult.map((item) => {
return (item / Math.pow(10, this.multiple + precision));
});
}
複製代碼
測試效果以下:
使用Echarts繪製隨機分配結果,將100數額劃分爲10份,精度爲1,橫座標爲順序位置,縱座標爲分配到的數額:
那每一個位置得到數額的機率是否相等呢?下圖是隨機分配100次的結果,並將每一個位置的在這100次分配中所得的平均數用紅色標出:
那分配1000次呢?
因而可知,隨機分配次數越多,每一個順序位置獲得的平均數額會穩定在平均分配的數額左右,公平性獲得了印證;同時,由於每一個位置只能獲得相鄰兩個位置的數額交換,因此分配結果中任意位置的數額不會超過平均數額的3倍(即本身愛財如命,同時又獲得相鄰者的傾力相助),這樣即可以控制隨機分配結果中的最高金額不至於太高。
和除本身外的隨機位置的兩位進行隨機數額交換?
從機率上講,和以前等價...只和本身左右或者右邊的位置進行隨機數額交換?
分配結果依然公平,但最高數額不會超過平均數額的2倍每一個位置隨機左右一邊而後進行隨機數額交換?
又雙叒隨機,仍是公平的,最高數額仍是少於平均數額的3倍(感受貌似能夠替代以前的方案,還能順便降一倍的線性複雜度,我文章要重寫了?! (°ー°〃))誰說只能挑2個進行交換?3個4個5個一塊兒來行不行?
行... 挑選位置公平的話,分配結果就公平,可是最大數額與交換數量正相關,但越高的數額,能獲得的機率會急劇減少。打住打住,再細想下去,個人坑怕是要填不完了 _(:з」∠)_
我用它寫過一個沒有後端數據的進度條,一抖一抖增長長短不一還彷彿真的在幫你加載同樣...
其餘...望君發揮想象力
最後 ( ˙-˙ )
初入掘金,不足之處望海涵,轉載煩請註明出處