这几天写代码的时候,发现自己需要一个随机数生成器。平方取中,线性同余以及java自带的Random(这个好像用的也是线性同余?)看起来效果不够理想,而梅林旋转虽然质量高但是真的好复杂。
我对于这个生成器期望大约是:
1.≥一亿的循环周期。
2.理论上任意种子都有落在取值范围内任意一个非无限小区间的可能。
3.知道初始种子以后可以快速推算使用这个种子进行第n次生成获得的数字。
4.低时间复杂度,中等以下代码量和理解难度。
也许看起来好他妈苛刻?但是我脑筋一转还真就想到一个。
那就是使用三角函数(这里以sin为例)。
以一个long或者double作为种子-seed,以一个整数或者有限位浮点数为a。生成的第n个随机数为sin((n-1)*a+seed)。
代码量低到一两行代码就能写完,原理简单到义务教育毕业大约就能看懂。简单运算就能得知使用某个种子进行第N次生成获得的数字为何。而且sin的周期是2π,一个整数或者有限位的小数加多少次也加不出π的整数倍,所以在忽略精度溢出的情况下,可以说应该永不重复,同时可能落在取值范围内任意一个非无限小区间。
当然,由于sin函数的形状,这样子处理落在不同区间内的概率会不同。但思路想到利用π这里了,改进一下也不难,比如(((n-1)*a+seed)%π)/π就可以保证落在各个区间的概率基本相等(除的时候可以用BigDecimal,更精准)。
最后再补充一下,如果觉得(((n-1)*a+seed)%π)/π这个方法输出数字变化频率较为明显(易于看出是每次加一个固定值后取模),将这个公式输出的随机值变换后作为自己的增量效果并不好,例如下图。
for(int i=0;i<10000;i++){
valD=BigDecimal.valueOf(val).divideAndRemainder(pi)[1].divide(pi,6).doubleValue();
val=(long)(val+1+4.2*valD);
System.out.println(valD);
}
我试过自己能想到的几种变换,都会导致随机数落点变得很不均匀。
但是,可以进行两次运算,将第一次运算输出的随机数变换后作为第二个式子的增量。例如下图。
for(int i=0;i<10000;i++){
valD=BigDecimal.valueOf(time+i).divideAndRemainder(pi)[1].divide(pi,6).doubleValue();
val=(long)(val+1+10*valD);
System.out.println(BigDecimal.valueOf(val).divideAndRemainder(pi)[1].divide(pi,6).doubleValue());
}
这样子输出的随机数落点既然均匀,同时输出数字规律就不是那么明显了,没那么易于一眼看出是加某个固定值再取模。但是呢,不再可以知道种子和固定增量瞬间推算第N次输出的随机值。。
不知道如此简单的方案之前有没有人想到过,但不论有没有,我当时想到的时候觉得自己像个小天才(厚着脸皮夸一下自己)。