如果你可以忍受float 和大约 800 万步,你可以简单地丢弃几乎一半的数字(比一半的数字少一个):
private static Random rnd=new Random();
public static float next0to1() {
float f;
do {
f=rnd.nextFloat();
} while(f>0.5);
return f*2;
}
此函数将生成 0 到 1 之间的随机浮点数,包括两端。
一个测试sn-p之类的
long start=System.currentTimeMillis();
int tries=0;
while(next0to1()!=0)
tries++;
System.out.println(tries);
while(next0to1()!=1)
tries++;
System.out.println(tries);
System.out.println(System.currentTimeMillis()-start);
或更长的,带有您的实际数字和一些额外的检查
long start=System.currentTimeMillis();
int tries=0;
float min=-0.1f;
float max=0.25f;
tries=0;
float current;
do {
tries++;
current=min+next0to1()*(max-min);
if(current<min)
throw new RuntimeException(current+"<"+min);
if(current>max)
throw new RuntimeException(current+">"+max);
} while(current!=min);
System.out.println(tries);
do {
tries++;
current=min+next0to1()*(max-min);
if(current<min)
throw new RuntimeException(current+"<"+min);
if(current>max)
throw new RuntimeException(current+">"+max);
} while(current!=max);
System.out.println(tries);
System.out.println(System.currentTimeMillis()-start);
通常会显示几千万次尝试生成 0 和 1,对于我使用 5 年的笔记本电脑来说,在不到一秒的时间内完成。
旁注:通常尝试超过 800 万次是正常的:虽然 nextFloat() 生成 24 位,通过丢弃几乎一半的数字减少到 ~23,生成器本身在 48 位上工作。
你可以用Random 做的最好的仍然是nextInt(),如Sasang 的回答所示。可用范围为2^30:
static double next0to1() {
return rnd.nextInt(0x40000001)/(double)0x40000000;
}
这需要更多的时间(分钟)并尝试(数十亿,tries 最好更改为long)来生成 0 和 1。
Random.nextDouble(),或“破解”随机数
nextDouble() 需要比生成器单步生成的精度更高的精度,而是将 26 位和 27 位数字组合在一起:
public double nextDouble() {
return (((long)next(26) << 27) + next(27))
/ (double)(1L << 53);
}
next() 被描述为
种子 = (种子 * 0x5DEECE66DL + 0xBL) & ((1L
返回(int)(种子>>>(48位))
(但实际上也可以找到,比如https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/Random.java#L183)
这意味着为了生成 0,a=next(26) 和连续的b=next(27) 都必须返回 0,所以
seeda=00000000 00000000 00000000 00xxxxxx xxxxxxxx xxxxxxxx (binary, 48 bits)
seedb=00000000 00000000 00000000 000xxxxx xxxxxxxx xxxxxxxx (binary, 48 bits)
从更新中:
seedb = (seeda * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)
它可以在一瞬间被暴力破解(400 万种可能性)seeda 必须是什么:
long mask = ((long) ((1 << 27) - 1)) << 21;
System.out.println(Long.toBinaryString(mask));
for (long i = 0; i < 1 << 22; i++)
if (((i * 0x5DEECE66DL + 0xBL) & mask) == 0)
System.out.println(i);
循环在低 22 位运行(我们知道其余部分为零),mask 是11111111 11111111 11111111 11100000 00000000 00000000,用于检查下一个种子的相关 27 位是否为零。
所以seeda=0。
接下来的问题是是否存在之前的seedx来生成seeda,所以
seeda = (seedx * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)
只是这一次我们不知道这些位,所以暴力破解将无济于事。但是这种方程是一个实际的东西,叫做congruence relation,是可以解的。
0 "=" seedx * 0x5DEECE66DL + 0xBL (mod 2^48)
WolframAlpha 需要 Ax "=" B (mod C) 的形式,并且是十进制的,所以输入是
25214903917x <- 0x5DEECE66D
281474976710656 <- 2^48
-11 <- 0xB, went to the other side
一种可能的解决方案是 107048004364969。学习/知道Random XOR-s 与幻数的种子,可以测试:
double low=-0.1,
high=0.25;
Random rnd=new Random(0x5DEECE66DL ^ 107048004364969L);
System.out.println(low+(high-low)*rnd.nextDouble()==low);
将导致true。所以是的,Random.nextDouble() 可以生成精确的 0。
下一部分是0.5:
seeda=10000000 00000000 00000000 00xxxxxx xxxxxxxx xxxxxxxx (binary, 48 bits)
seedb=00000000 00000000 00000000 000xxxxx xxxxxxxx xxxxxxxx (binary, 48 bits)
seeda 的第 48 位设置为 1。
暴力循环来了:
long mask = ((long) ((1 << 27) - 1)) << 21;
System.out.println(Long.toBinaryString(mask));
for (long i = 0; i < 1 << 22; i++)
if ((((i + 0x800000000000L) * 0x5DEECE66DL + 0xBL) & mask) == 0)
System.out.println(i);
糟糕,没有解决方案。 5DEECE66D 有它的最低位设置(它是一个奇数),所以当我们在 0x80 中正好有一个位设置为 1 时......,乘法后它将保持为 1 - 当然,如果我们尝试移动它,这也适用右边一位)。
TL;DR:Random.nextDouble()(因此Math.random())永远不会生成精确的 0.5。(或 0.25、0.125 等)