等概率产生 [1-7] 的随机数

本文详细解析了在Java中如何生成等概率的1-7随机数,通过扩展可以应用于生成任意范围的等概率随机数。文章讨论了使用Random类、Math.random()方法以及时间戳的局限性,并提出了两种解决方案,通过组合随机数扩大范围并进行取模运算,确保每个结果的概率相等。最后,文章还介绍了使用m进制转换的方法扩展到生成[1-n]的随机数,展示了代码实现和测试结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. [准备阶段] 产生1-5的随机数

Java中产生随机数的方法主要有三种:

  1. new Random()
  2. Math.random()
  3. currentTimeMillis()

Ranom类

边界为rand.nextInt(MAX - MIN + 1) + MIN;

public static void randFive(int[] arr) {
    Random random = new Random();
    for (int i = 0; i < arr.length; i++) {
        arr[i] = random.nextInt(5) + 1;
    }
}

Math.random()

【缺陷】:很难凑成既包括0又包括5的范围,只能是[0, 4]或者[0+1,4+1]

public static void randFiveII(int[] arr) {
    int max = 5, min = 1;
    for (int i = 0; i < arr.length; i++) {
        arr[i] = (int) (Math.random() * 5 + 1);
    }
}

时间戳

【缺陷】:放入循环中产生,由于CPU执行速度快,产生连续的随机数是相同的,适合产生单个

public void randFiveIII() {
    int max = 100, min = 1;
    long randomNum = System.currentTimeMillis();
    int ran = (int) (randomNum % (max - min) + min);
    //循环同一时间会产生相同的数
    System.out.print(ran);
}

2. 解决一

误区

randFive产生的每一个rand % 3再相加,但是并不是等概率的!

public static void main(String[] args) {
    int[] arr_seven = new int[10];
	for (int i = 0; i < arr_seven.length; i++) {
    	arr_seven[i] = getRandomNumSeven(randFive());
	}
}

//不是等概率
public static int getRandomNumSeven(int randNum) {
    return randNum + (randNum % 3);
}

【原因】:

randFive() 能够等概率生成 1-5 之间的整数,1,2,3,4,5 生产的概率均为 0.2、

rand()%3 产生0的概率是1/5,而产生1和2的概率都是2/5,所以这个方法产生6和7的概率大于产生5的概率。

如果randFive是产生1-10,是等概率吗?

并不是。比如对于 6来讲(4+2, 2+4, 3+3),它被生成的生成的概率比1 (1+0,0+1)要大。因为63种组合,而1只有2种组合

所以,对原来randFive产生的1-5的随机数,不能用加减乘除来得到getRandomNumSeven

解决

randFive,来构造一个更大的范围。使得范围里每一个值被生成的概率是一样的,而且这个范围是7的倍数。

  1. 先产生一个均匀分布的 0, 5, 10, 15, 20的数
  2. 再产生一个均匀分布的 0, 1, 2, 3, 4 的数。相加以后,会产生一个 0到24的数,而且每个数(除0外)生成的概率是一样的
  3. 只取 1 - 21 这一段,和7 取余以后+1就能得到完全均匀分布的1-7的随机数了

获得每个数的次数(概率)相等。即每个数只能由一种组合得到

/**
 * 调用randFive()来 等概率 产生 1-7
 * @return
 */

public static void main(String[] args) {
    // 测试统计
    
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < 1000000; i++) {
        
        int temp = getRandomNumSeven();
        
        if (map.containsKey(temp)) {
            map.put(temp, map.get(temp) + 1);
        } else {
            map.put(temp, 1);
        }
    }
    
    map.forEach((key, value) -> {
        System.out.println(key + " -- 出现次数:" + value);
    });
}

public static int getRandomNumSeven() {
    while (true) {
        int randLowSeven = (randFive() - 1) * 5 + randFive();
        if(randLowSeven <= 21) {
            return randLowSeven % 7 + 1;
        }
    }

测试 100W次 出现的结果:

在这里插入图片描述

在这里插入图片描述

Python测试:

import numpy

def randFive():
    return numpy.random.randint(1, 6)  # 随机1到5,不包括右端点6

def getRandomNumSeven():
    while True:
        randLowSeven = randFive() + 5 * (randFive() - 1)  # 产生均匀的1-25
        if randLowSeven <= 21:  # 只取1-21
            return randLowSeven % 7 + 1


res = [0] * 8
for i in range(1000000):
    rnd = getRandomNumSeven()
    res[rnd] += 1
print(res[1:])  # 随机生成多次,验证1到7的出现次数是否均匀

# res[1:]为[142980, 142709, 142939, 142875, 142398, 143277, 142822],是均匀的

在这里插入图片描述

思考探究

为什么是 (randFive() - 1) * 5 + randFive() ,为什么是弃掉大于21的数再模7

因为要先通过两次调用,将已有的随机数函数扩大范围(randFive() - 1) * 5 + randFive(),本身[1-7]的范围用randFive()的函数等概率产生,5个数凑7个数一定是不公平的。所以要通过randFive()来放大取值范围;

但是,放大了取值范围,5等概率扩大的数的范围在[1, 25](假设此时已对[0, 24] + 1),多了22\23\24\25 三个数,如果去去除,则

22 % 7 = 1
23 % 7 = 2
24 % 7 = 3
25 % 7 = 4

那么1、2、3、4就不是等概率了,所以只取[1, 21]。由于产生扩大的每个数时是等概率的,那么在去除25 % 7 = 4后,三个子集中每个元素依然是等概率的。


3. 解决二

这种实现前6个是等概率的,最后一个与前6个出现的次数差距很大,存在问题。

算法思路是:

  1. 通过(randFive() * 5 + randFive()) <= 25产生 6 7 8 9 10 11 …… 26,27 28 29 30 这25个数,每个数的出现机率相等
  2. 只需要前面21个数,所以舍弃后面的4个数
  3. 将 [6 7 8 ]转化为 1,[9 10 11] 转化为 2,……,[24 25 26] 转化为 7。公式是 (randLowSeven-3) / 3
	public static int getRandomNumSevenII() {
        int randLowSeven = 0;
        while ((randLowSeven = randFive() * 5 + randFive()) > 26);

        return (randLowSeven - 3) / 3;
    }

在这里插入图片描述

最后一次不是等概率

但是,这样写却是不正确的。导致最后一次不公平。

因为出现 > 26 的情况,说明 randFive() * 5 有很大概率等于25,从而又导致while之后的randLowSeven有很大机率很大,破坏了平衡性。

public static int getRandomNumSeven() {
    int randLowSeven = 0;
    while (true) {
        if((randLowSeven = randFive() * 5 + randFive()) <= 25) {
            return (randLowSeven - 3) / 3;
        }
    }
}

在这里插入图片描述

在这里插入图片描述

4. 拓展

就是对【方法一】的拓展,由之前的(randFive - 1)产生每一位5进制数,然后将``randLowSeven%7+1`就得到了均匀分布于1到7的算法.

那么现在将其转为m进制的数,等概率表示**[1 - n]** 之间的范围

public class Main {
    public static int rand1ToM(int m) {
        return (int) (Math.random() * m) + 1;
    }

    //产生1-n的随机函数
    public static int rand1ToN(int n, int m) {
        int[] nMSys = getMSysNum(n - 1, m);
        int[] randNum = getRanMSysNumLessN(nMSys, m);
        return getNumFromMSysNum(randNum, m) + 1;
    }

    // 把value转成m进制的数
    public static int[] getMSysNum(int value, int m) {
        int[] res = new int[32];
        int index = res.length - 1;
        while (value != 0) {
            res[index--] = value % m;
            value = value / m;
        }
        return res;
    }

    // 等概率随机产生一个0~nMsys范围上的数,只不过是m进制表达的
    public static int[] getRanMSysNumLessN(int[] nMSys, int m) {
        int[] res = new int[nMSys.length];
        int start = 0;
        while (nMSys[start] == 0) {
            start++;
        }
        int index = start;
        boolean lastEqual = true;
        while (index != nMSys.length) {
            res[index] = rand1ToM(m) - 1;
            if (lastEqual) {
                if (res[index] > nMSys[index]) {
                    index = start;
                    lastEqual = true;
                    continue;
                } else {
                    lastEqual = res[index] == nMSys[index];
                }
            }
            index++;
        }
        return res;
    }

    // 把m进制的数转成10进制
    public static int getNumFromMSysNum(int[] mSysNum, int m) {
        int res = 0;
        for (int i = 0; i != mSysNum.length; i++) {
            res = res * m + mSysNum[i];
        }
        return res;
    }


    public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<>();
        System.out.println("randM 等概率产生 [1 - n]的随机数: ");
        for (int i = 0; i < 1000000; i++) {

            // 测试方法二
            int temp = rand1ToN(4, 5);

            if(map.containsKey(temp)) {
                map.put(temp, map.get(temp) + 1);
            } else {
                map.put(temp, 1);
            }
        }

        map.forEach((key, value) -> {
            System.out.println(key + " -- 出现次数:" + value);
        });
    }
}

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值