算法 | 如何通过[1,5]范围计算出[1,7]范围等概率随机数?

266 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第30天,点击查看活动详情

前言

本文主要介绍如何通过[1,5]范围计算出[1,7]范围等概率随机数。

给出一个方法random,该方法会等概率返回[1,5]范围的一个随机整数,现在要根据这个random方法实现一个返回[1,7]范围等概率随机整数的方法。

不可对random方法进行任何修改。

解析

已知random方法会等概率返回1、2、3、4、5这些数字,那么就要想办法根据这几个数字生成1、2、3、4、5、6、7这些数字。

random方法返回值加2吗?不行的,返回值加2得到的是3、4、5、6、7

random方法如下,该方法不可修改,可以理解为系统方法,仅能调用。

public static int random() {
    return (int) (Math.random() * 5) + 1;
}

思路分析

等概率返回[0,1]

首先,我们能否根据random方法的返回值得出一个能够等概率返回[0,1]的方法?

我们来看下如何等概率返回[0,1]

设计出一个方法func01,根据random方法的返回值,如果random方法返回1,2该方法就返回0,如果random方法返回3,4该方法就返回1,如果random方法返回5,则重新计算。

func01方法实现如下:

// 等概率返回0或1
public static int func01() {
    int ret = 0;
    do {
        ret = random();
    } while (ret == 5);
    return ret <= 2 ? 0 : 1;
}
等概率返回[0,6]

为什么要计算出等概率返回[0,6]呢?因为得到范围[0,6]后,再对结果进行加1就能得到[1,7]范围等概率随机数。

我们来看下如何等概率返回[0,6]

从二进制方面出发,一个二进制位由01两个元素,1个二进制位可以表示0~1范围的数,2个二进制位可以表示0~3范围的数,3个二进制位可以表示0~7范围的数。

func01()方法能返回0~1范围的数,如果能调用func01()方法3次,并且把3次结果拼起来,就可以得到一个3位的二进制数。

因为调用3次func01()方法,每次调用都是相对独立的,所以3次调用,得到的结果也是等概率的,也就是得到这些数000001010011111的概率是相等的。

来看下等概率返回[0,7]的方法实现:

// 等概率返回0 ~ 7
public static int func07() {
    return (func01() << 2) + (func01() << 1) + func01();
}

func01() << 2表示,如果func01()返回1则左移2位变为100,如果如果func01()返回0则左移2位变为000。

func01() << 1表示,如果func01()返回1则左移1位变为10,如果如果func01()返回0则左移1位变为00。

最终得到的数据为000001010011111,也就是0 ~ 7

接下来,对这些数进行处理,得到[0,6]这个范围。

// 等概率返回0 ~ 6
public static int func06() {
    int ret = 0;
    do {
        ret = func01();
    } while (ret == 7);
    return ret;
}
等概率返回[1,7]

得到[0,6]这个范围后,我们只需要对结果进行加1即可,就能得到[1,7]这个范围的数。

// 等概率返回1 ~ 7
public static int func17() {
    return func06() + 1;
}

当然,也可以对[0,7]这个范围的0元素进行处理,如果是0的话,让其重新生成,这样也能得到[1,7]这个数据范围。

public static int func17() {
    int ret = 0;
    do {
        ret = func07();
    } while (ret == 0);
    return ret;
}

结果验证

拿到[1,7]这个数据范围这个范围后,我们来做一个验证,验证思路如下:

  1. 初始化一个数组arr,用来存放 1~7 被调用的次数。
  2. 设置一个循环1千万次,来调用func17()这个方法。
  3. 对比数组arr每个元素的个数是否大致相同。
  4. 如果大致相同的话,说明func17()方法是等概率返回1~7中的元素。

验证逻辑如下:

public static void main(String[] args) {
    int times = 10000000;
    int[] arr = new int[7];
    for (int i = 0; i < times; i++) {
        int ret = func17();
        arr[ret - 1]++;
    }
    for (int i = 0; i < 7; i++) {
        System.out.println("元素:" + (i + 1) + ", 出现的次数为: " + arr[i]);
    }
}

执行结果为:

元素:1, 出现的次数为: 1428558
元素:2, 出现的次数为: 1427992
元素:3, 出现的次数为: 1428278
元素:4, 出现的次数为: 1426736
元素:5, 出现的次数为: 1430771
元素:6, 出现的次数为: 1430227
元素:7, 出现的次数为: 1427438

可以看到,这些元素出现的次数是非常接近的,由此可以得出func17()方法是等概率返回1~7中的元素。

拓展

如果给出一个等概率数据范围为[2,20],能不能得出一个范围为[30,68]的等概率数据?

当然是可以的,思路和上面一样,先根据[2,20]得到[0,1]这个范围,然后再计算出[0,38]数据范围,最后对[0,38]进行加30即可。

同理,如果给出一个等概率数据范围为[a,b],我们也能得出一个范围为[c,d]的等概率数据。