三年前微信红包爆火的时候,脑补了下背后的分配原理,并用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个一块儿来行不行?
行... 挑选位置公平的话,分配结果就公平,可是最大数额与交换数量正相关,但越高的数额,能获得的几率会急剧减少。打住打住,再细想下去,个人坑怕是要填不完了 _(:з」∠)_
我用它写过一个没有后端数据的进度条,一抖一抖增长长短不一还仿佛真的在帮你加载同样...
其余...望君发挥想象力
最后 ( ˙-˙ )
初入掘金,不足之处望海涵,转载烦请注明出处