Effective Java 条款47:了解并使用你的类库 - 有缺陷的随机整数方法示例

17

在Josh提供的例子中,展示了一种存在缺陷的随机方法,该方法可生成一个给定上限n的正随机数。我不理解他所谈到的两种缺陷。

书中的方法如下:

private static final Random rnd = new Random();

//Common but deeply flawed
static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}
  • 他说如果 n 是 2 的一个小幂,生成的随机数序列将在短时间内重复。为什么会这样?Random.nextInt() 的文档说明为“从该随机数生成器的序列中返回下一个伪随机、均匀分布的 int 值”。那么如果 n 是一个小整数,序列不应该重复吗?为什么只适用于 2 的幂次方?
  • 接下来他说,如果 n 不是 2 的幂,则某些数字将平均更频繁地返回。如果 Random.nextInt() 生成的整数是均匀分布的,为什么会出现这种情况?(他提供了一个代码片段,清楚地证明了这一点,但我不明白为什么会这样,以及这与 n 是 2 的幂次方有什么关系)。

你为什么会用那个方法呢?rnd.nextInt(n) - Elliott Frisch
2
@Elliott 这就是书中例子的重点。 - Kevin
我很惊讶作者忽略了最大的缺陷:这段代码有时会返回负数! - Mooing Duck
@MooingDuck 它如何返回负数? - FDinoff
1
Math.abs 的文档说明:“请注意,如果参数等于 Integer.MIN_VALUE,即可表示的最小负整数值,则结果是相同的值,即为负数。” http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#abs(int) (我还确认了 rnd.nextInt() 确实可以返回 Integer.MIN_VALUE)。负值对正数 n 取模的结果是负值。 - Mooing Duck
@Mooing,实际上他确实提到了你指出的缺陷,只是我没有问到它。 - Derek Mok
2个回答

38

问题1: 如果n是2的小幂次方,则生成的随机数序列将在短时间内重复自身。

这不是Josh所说的任何必然结果,而是线性同余生成器的一个已知属性。维基百科如下所述:

LCG的另一个问题是,如果m设置为2的幂,则生成的序列的低阶位期限远远短于整个序列。一般来说,在输出序列的基b表示中,其中b k = m(其中k是某个整数)的第n个最低有效数字最多重复b n 次。

这也在Javadoc中有所提及:

像该类实现的那样,线性同余伪随机数生成器被称为其低位值序列的短期周期。

函数的另一个版本Random.nextInt(int)通过在这种情况下使用不同的位来解决这个问题(我强调):

该算法会特别处理n是2的幂的情况:它从基础伪随机数生成器返回正确数量的高位位数。

这是偏好于使用Random.nextInt(int)而不是使用Random.nextInt()并进行自己的范围转换的一个很好的理由。

问题2: 接下来,他说如果n不是2的幂,则某些数字将平均更频繁地返回。

nextInt()可以返回2的32次方个不同的数字。如果您使用% n将它们放入n个桶中,并且n不是2的幂,则一些桶将具有比其他桶更多的数字。这意味着尽管原始分布是均匀的,但有些结果会更频繁地发生。

让我们使用小数字来看一下这种情况。假设nextInt()返回四个等概率的结果0、1、2和3。让我们看看如果我们对它们应用% 3会发生什么:

0 maps to 0
1 maps to 1
2 maps to 2
3 maps to 0

正如您所看到的,该算法返回0的频率是返回1和2的频率的两倍。

当n是2的幂时,这种情况不会发生,因为两个幂次方中一个被另一个整除。考虑 n=2

0 maps to 0
1 maps to 1
2 maps to 0
3 maps to 1

这里,0和1以相同的频率出现。

附加资源

以下是一些额外的-虽然只是间接相关-LCG相关的资源:


我意识到我晚了三年,但是想要说一句话,虽然将2^32个值分成3个bin的效果会导致bin大小之间几乎可以忽略不计的差异,但如果增加bin的数量,则会变得更加明显。例如,将3 * (Integer.MAX_VALUE / 4)个bin分配,平均约有1/3的bin最终会有两倍的条目数。 - Ironcache

5

1) 当n是2的幂时,rnd % n等同于选择原始数字的几个低位。由Java使用的生成器类型生成的数字的低位比高位“不那么随机”。这只是用于生成数字的公式的属性。

2) 想象一下,random()返回的最大可能值为10,n = 7。现在执行n % 7将数字7、8、9和10映射到0、1、2、3。因此,如果原始数字均匀分布,则结果会严重偏向较低的数字,因为它们将比4、5和6出现两次。在这种情况下,无论n是否是2的幂,都会发生这种情况,但是,如果我们选择15(即2 ^ 4-1)而不是10,则任何是2的幂的n都会导致均匀分布,因为在范围末尾不会留下“多余”的数字来引起偏差,因为可能的总值数量可以被可能的余数数量完全整除。


1
个人认为第二个说法基本上是胡说八道。最大值不是10,而是2^32-1,因此在最坏情况下(平均情况下),每个箱子中的物品数量可能会有+/-1的差异。剩余数字出现的次数将非常少,例如如果n = 100,则它们甚至被选中的几率只有极小的一部分百分比。 - Alnitak
是的,我更正了措辞...你抓住了我正在编辑的过程中 :) - Dima
2
@Alnitak,是的,对于小的n,差异并不太明显。但是如果n是像2 * Integer.MAX_INT / 3这样的数字,你会发现范围内较低的数字出现的频率是其他数字的两倍。 - Dima
1
是的,它需要在那个数量级上才能产生任何显著的差异。不过,这对于 RNG 来说是一个非常不寻常的用途(以我的经验来看)。 - Alnitak
@Alnitak,不,除了大学级别的“玩具程序”之外,随机数的大范围实际上比小范围更有用。 - Dima

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接