红包算法分析与实现

1. 算法原理分析

在知乎和一些其他博客中,很多人都提出了自己的观点。我选取其中的一个算法进行分析。

1.1 基本思路

有人认为,抢红包的额度是从0.01到剩余平均值×N(N是一个系数,决定最大的红包值)之间。例如:一共发了10块钱,发了10个红包,第一个人可以拿到(0.01~1×N)之间的一个红包值。

为了确保所有人至少有1分钱拿,不能前几个人就把钱拿光了,因此需要有一个判断算法。

1.2 示例代码

public class test{
    public static void main(String[] args)
    {   
        float num=10, N=1.9f; // 开始将系数设为1.9
        int people=10;
        for(int i=0; i<10; i++)
        {
            System.out.println("the number" + people + " can get  " + num/people*N);
            num = num - num/people*N;
            people--;
        }
        System.out.println("there remain" + num);
    }   
}
the number10 can get  1.9
the number9 can get  1.71
the number8 can get  1.5176251
the number7 can get  1.3225019
the number6 can get  1.1241267
the number5 can get  0.9217838
the number4 can get  0.71438247
the number3 can get  0.5000677
the number2 can get  0.2750373
the number1 can get  0.027503723
there remain-0.01302808

问题分析:

  • 当10个人结束后剩余的钱为负数,说明最后一个人达不到他的最大值

  • 如果将系数改为2.3,会出现number1为负数的情况

    the number10 can get  2.3
    the number9 can get  1.9677777
    the number8 can get  1.6480138
    the number7 can get  1.3419542
    the number6 can get  1.0511974
    the number5 can get  0.7778861
    the number4 can get  0.5250732
    the number3 can get  0.29754147
    the number2 can get  0.10413953
    the number1 can get  -0.031241853
    there remain0.017658439
    
  • 因此,在使用系数的算法时,添加判断金额是否合理是很重要的

2. 算法实现

2.1 设置金额的上下限

private static final float MINMONEY = 0.01f;  
private static final float MAXMONEY = 200f;

我们的红包金额至少有0.01,最多有200(单笔红包金额不会超过200)。

2.2 判断金额是否合法

private boolean isRight(float money, int count) {
    double avg = money/count;
    if(avg < MINMONEY) {
        return false;
    }
    else if(avg > MAXMONEY) {
        return false;
    }
    return true;
}

2.3 随机产生红包

private float randomRedPacket(float money, float mins, float maxs, int count) {
    if(count == 1) {
        return (float)(Math.round(money*100))/100;
    }
    if(mins == maxs) {
        return mins; // 如果最大值和最小值一样,就返回mins
    }
    
    float max = maxs > money ? money : maxs;
    float one = ((float)Math.random()*(max-mins)+mins);
    one = (float)(Math.round(one*100))/100;
    
    float moneyOther = money - one;
    if(isRight(moneyOther, count-1)) {
        return one;
    } else {
        // 重新分配
        float avg = moneyOther / (count-1);
        if(avg < MINMONEY) {
            return randomRedPacket(money, mins, one, count);
        } else if(avg > MAXMONEY) {
            return randomRedPacket(money, one, maxs, count);
        }
    }
    return one;
}

2.4 实现红包分配

private static final float TIMES = 2.1f;

public List<Float> splitRedPackets(float money, int count) {
    if(!isRight(money, count)) {
        return null;
    }
    
    List<Float> list = new ArrayList<Float>();
    float max = (float)(money*TIMES/count);
    max = max > MAXMONEY ? MAXMONEY : max;
    
    for(int i=0; i<count; i++) {
        float one = randomRedPacket(money, MINMONEY, max, count-i);
        list.add(one);
        money -= one;
    }
    return list;
}

2.5 编写主函数

public static void main(String[] args) {  
    RedPacketUtil util = new RedPacketUtil();  
    System.out.println(util.splitRedPackets(200, 100));  
}  

运行方式:

javac RedPacketUtil.java
java RedPacketUtil
RedPacketUtil
[1.75, 1.52, 1.27, 2.81, 3.5, 1.37, 2.03, 0.77, 1.1, 1.89, 1.87, 3.46, 2.34, 0.99, 1.67, 3.63, 1.03, 4.13, 3.56, 0.57, 2.79, 0.96, 1.63, 3.36, 3.92, 3.12, 2.44, 1.2, 1.44, 0.24, 0.43, 3.16, 3.32, 3.24, 1.53, 3.53, 2.49, 1.3, 2.22, 4.05, 2.34, 0.4, 2.02, 3.71, 2.08, 2.73, 2.02, 0.06, 2.07, 1.72, 2.42, 0.95, 0.32, 2.85, 1.96, 3.67, 0.36, 2.33, 1.89, 0.25, 2.34, 1.02, 1.19, 0.69, 0.21, 3.03, 1.25, 0.41, 3.79, 1.54, 3.43, 3.09, 3.09, 2.9, 0.19, 2.08, 3.88, 0.67, 3.66, 0.41, 1.31, 2.37, 2.82, 0.65, 1.67, 1.39, 0.22, 1.23, 1.01, 3.28, 2.98, 0.89, 3.89, 0.84, 4.06, 1.06, 0.06, 1.71, 1.32, 4.59]

3. 数据分析

3.1 增加模拟次数

这个红包分配算法是网友们猜测的,暂时不去考虑微信是不是这么做的。

3.1.1 模拟实现

在上节实验中的完整代码中导入工具类库:

import java.util.Scanner;

修改main函数:

public static void main(String[] args) {     
    System.out.println("please input a number");  
    Scanner sc = new Scanner(System.in);
    int num = sc.nextInt(), count = num, NUM = 20;
    float[] index = new float[NUM];
    
    while(num-- > 0) {
        RedPacketUtil util = new RedPacketUtil();  
        List<Float> temp = util.splitRedPackets(20, NUM); 
        for(int i=0; i<temp.size(); i++) {
            index[i] += temp.get(i);
        }
    }
    
    for(int i=0; i<NUM; i++) {
        System.out.println("the number " + i + " can get " + (index[i]/count));
    }
}

模拟结果:

  • 模拟20块红包分给20个人,运行10次

    please input a number
    10
    the number 0 can get 1.234
    the number 1 can get 1.014
    the number 2 can get 1.186
    the number 3 can get 1.02
    the number 4 can get 1.129
    the number 5 can get 1.12
    the number 6 can get 1.0849999
    the number 7 can get 0.844
    the number 8 can get 0.759
    the number 9 can get 1.08
    the number 10 can get 1.242
    the number 11 can get 0.73800004
    the number 12 can get 1.0509999
    the number 13 can get 0.82699996
    the number 14 can get 0.80700004
    the number 15 can get 0.995
    the number 16 can get 0.96599996
    the number 17 can get 0.99599993
    the number 18 can get 0.56700003
    the number 19 can get 1.34
    
  • 当运行次数达到1000000次时,结果呈现正态分布

    please input a number
    1000000
    the number 0 can get 1.0548186
    the number 1 can get 1.05548
    the number 2 can get 1.0544133
    the number 3 can get 1.0541049
    the number 4 can get 1.0543569
    the number 5 can get 1.0551047
    the number 6 can get 1.0537063
    the number 7 can get 1.0544281
    the number 8 can get 1.0545907
    the number 9 can get 1.0557877
    the number 10 can get 1.0539982
    the number 11 can get 1.0553659
    the number 12 can get 1.0531938
    the number 13 can get 1.0456945
    the number 14 can get 1.0171772
    the number 15 can get 0.95570326
    the number 16 can get 0.8545562
    the number 17 can get 0.7179943
    the number 18 can get 0.56496185
    the number 19 can get 1.1307287
    

3.2 人数变化对结果的影响

修改main函数中的代码,将人数增加到200人,模拟100000次:

please input a number
1000000
the number 0 can get 10.497068
the number 1 can get 10.507459
the number 2 can get 10.505903
the number 3 can get 10.488457
the number 4 can get 10.510313
the number 5 can get 10.50352
the number 6 can get 10.502333
the number 7 can get 10.51191
the number 8 can get 10.499479
the number 9 can get 10.498564
the number 10 can get 10.502607
the number 11 can get 10.50349
the number 12 can get 10.494
the number 13 can get 10.404507
the number 14 can get 10.170025
the number 15 can get 9.569489
the number 16 can get 8.599382
the number 17 can get 7.242853
the number 18 can get 5.732309
the number 19 can get 11.738466

分析结果:

  • 偏后的人拿到大金额红包的概率会小很多
  • 红包分配呈现一定的规律性,符合概率分布特征

4. 算法优化建议

4.1 性能优化

  • 可以考虑使用更高效的随机数生成算法
  • 减少重复计算,如平均值的计算

4.2 精度优化

  • 当前使用四舍五入到分的精度,可以考虑使用BigDecimal提高精度
  • 避免浮点数计算带来的精度问题

4.3 稳定性优化

  • 增加更多的边界条件检查
  • 提供更详细的错误处理机制

5. 总结

该红包分配算法通过合理的随机数生成和金额限制,实现了相对公平的红包分配机制。通过大量模拟实验验证了其在不同场景下的稳定性,为实际应用提供了理论基础和实现参考。

完整代码:

import java.util.ArrayList;
import java.util.List;

public class RedPacketUtil {
    private static final float MINMONEY = 0.01f;

    private static final float MAXMONEY = 200f;

    private static final float TIMES = 2.1f;

    private boolean isRight(float money, int count) {
        double avg = money / count;
        if (avg < MINMONEY) {
            return false;
        } else if (avg > MAXMONEY) {
            return false;
        }
        return true;
    }

    private float randomRedPacket(float money, float mins, float maxs, int count) {
        if (count == 1) {
            return (float) (Math.round(money * 100)) / 100;
        }
        if (mins == maxs) {
            return mins; // 如果最大值和最小值一样,就返回mins
        }

        float max = maxs > money ? money : maxs;
        float one = ((float) Math.random() * (max - mins) + mins);
        one = (float) (Math.round(one * 100)) / 100;

        float moneyOther = money - one;
        if (isRight(moneyOther, count - 1)) {
            return one;
        } else {
            // 重新分配
            float avg = moneyOther / (count - 1);
            if (avg < MINMONEY) {
                return randomRedPacket(money, mins, one, count);
            } else if (avg > MAXMONEY) {
                return randomRedPacket(money, one, maxs, count);
            }
        }
        return one;
    }

    public List<Float> splitRedPackets(float money, int count) {
        if (!isRight(money, count)) {
            return null;
        }

        List<Float> list = new ArrayList<Float>();
        float max = (float) (money * TIMES / count);
        max = max > MAXMONEY ? MAXMONEY : max;

        for (int i = 0; i < count; i++) {
            float one = randomRedPacket(money, MINMONEY, max, count - i);
            list.add(one);
            money -= one;
        }
        return list;
    }

    public static void main(String[] args) {
        RedPacketUtil util = new RedPacketUtil();
        System.out.println(util.splitRedPackets(200, 100));
    }
}

如果觉得本文对你有帮助,可以点击下方打赏扫码支持一下。你的鼓励是我持续写下去的最大动力,感谢!

更多推荐