今天看到Random() 中的nextInt()方法采用的是线性同余法;
同余法(Congruential method)是很常用的一种随机数生成方法,在很多编程语言中有应用,最明显的就是java了,java.util.Random类中用的就是同余法中的一种——线性同余法(Linear congruential method),除此之外还有乘同余法(Multiplicative congruential method)和混合同余法(Mixed congruential method)。好了,现在我们就打开java的源代码,看一看线性同余法的真面目!
线性同余法是一个很古老的随机数生成算法,它的数学形式如下:
Xn+1 = (a*Xn+c)(mod m) 其中,m>0,a<a<m,0<c<m
这里Xn这个序列生成一系列的随机数,X0是种子。随机数产生的质量与m,a,c三个参数的选取有很大的关系。这些随机数并不是真正的随机,而是满足某一周期内随机分布,这个周期的最长为m(一般来说是小于M的)。根据Hull-DobellTheorem,当且仅当:
1.c和m互素;
2.a-1可被所有m的质因数整除;
3.当m是4的整数倍,a-1也是4的整数倍,周期为m。所以m一般都设置的很大,以延长周期。
现在我们回头来看刚才的程序,注意这这段代码:
nextseed = (oldseed * multiplier + addend) & mask;
这一行代码用到了线性同余法公式!
private static final long multiplier = 0x5DEECE66DL; private static final long addend = 0xBL; private static final long mask = (1L << 48) - 1;
其中multiplier和addend分别代表公式中的a和c,很好理解,但mask代表什么呢?其实,x & [(1L << 48)–1]与 x(mod 2^48)等价。解释如下:
x对于2的N次幂取余,由于除数是2的N次幂,如:
0001,0010,0100,1000。。。。
相当于把x的二进制形式向右移N位,此时移到小数点右侧的就是余数,如:
13 = 1101 8 = 1000
13 / 8 = 1.101,所以小数点右侧的101就是余数,化成十进制就是5
然而,无论是C语言还是java,位运算移走的数显然都一去不复返了。(什么,你说在CF寄存器中?好吧,太高端了点,其实还有更给力的方法)有什么好办法保护这些即将逝去的数据呢?
学着上面的mask,我们不妨试着把2的N次幂减一:
0000,0001,0011,0111,01111,011111。。。
怎么样,有启发了吗?
我们知道,某个数(限0和1)与1作与(&)操作,结果还是它本身;而与0作与操作结果总是0,即:
a & 1 = a, a & 0 = 0
而我们将x对2^N取余操作希望达到的目的可以理解为:
1、所有比2^N位(包括2^N那一位)全都为0
2、所有比2^N低的位保持原样
因此, x & (2^N-1)与x(mod 2^N)运算等价,还是13与8的例子:
1101 % 1000 = 0101 1101 & 0111 = 0101
二者结果一致。
嘿嘿,讲明白了这个与运算的含义,我想上面那行代码的含义应该很明了了,就是线性同余公式的直接套用,其中a = 0x5DEECE66DL, c = 0xBL, m = 2^48,就可以得到一个48位的随机数,而且这个谨慎的工程师进行了迭代,增加结果的随机性。再把结果移位,就可以得到指定位数的随机数。
接下来我们研究一下更常用的一个函数——带参数n的nextInt:
1 public int nextInt(int n) { 2 if (n <= 0) 3 throw new IllegalArgumentException("n must be positive"); 4 5 if ((n & -n) == n) // i.e., n is a power of 2 6 return (int)((n * (long)next(31)) >> 31); 7 8 int bits, val; 9 do { 10 bits = next(31); 11 val = bits % n; 12 } while (bits - val + (n-1) < 0); 13 return val; 14 }
显然,这里基本的思路还是一样的,先调用next函数生成一个31位的随机数(int类型的范围),再对参数n进行判断,如果n恰好为2的方幂,那么直接移位就可以得到想要的结果;如果不是2的方幂,那么就关于n取余,最终使结果在[0,n)范围内。另外,do-while语句的目的应该是防止结果为负数。
你也许会好奇为什么(n & -n) == n可以判断一个数是不是2的次方幂,其实我也是研究了一番才弄明白的,其实,这主要与补码的特性有关:
众所周知,计算机中负数使用补码储存的(不懂什么是补码的自己百度恶补),举几组例子:
2 :0000 0010 -2 :1111 1110
8 :0000 1000 -8 :1111 1000
18 :0001 0010 -18 :1110 1110
20 :0001 0100 -20 :1110 1100
不知道大家有没有注意到,补码有一个特性,就是可以对于两个相反数n与-n,有且只有最低一个为1的位数字相同且都为1,而更低的位全为0,更高的位各不相同。因此两数作按位与操作后只有一位为1,而能满足这个结果仍为n的只能是原本就只有一位是1的数,也就是恰好是2的次方幂的数了。
不过个人觉得还有一种更好的判断2的次方幂的方法:
n & (n-1) == 0
感兴趣的也可以自己研究一下^o^。
好了,线性同余法就介绍到这了,下面简要介绍一下另一种同余法——乘同余法(Multiplicative congruential method)。
上文中的线性同余法,主要用来生成整数,而某些情景下,比如科研中,常常只需要(0,1)之间的小数,这时,乘同余法是更好的选择,它的基本公式和线性同余法很像:
Xn+1=(a*Xn )(mod m )
其实只是令线性公式中的c=0而已。只不过,为了得到小数,我们多做一步:
Yn = Xn/m
由于Xn是m的余数,所以Yn的值介于0与1之间,由此到(0,1)区间上的随机数列。
除此之外,还有混合同余法,二次同余法,三次同余法等类似的方法,公式类似,也各有优劣,在此不详细介绍了。
同余法优势在计算速度快,内存消耗少。但是,因为相邻的随机数并不独立,序列关联性较大。所以,对于随机数质量要求高的应用,特别是很多科研领域,并不适合用这种方法。
原文地址: 点击打开链接