如何评价一个随机数算法?个人认为,一个好的随机数算法,能提供足够的安全性和公平性,并拥有优异的性能。
真随机数发生器由于价格昂贵,并没有得到普遍采用。那么,伪随机数就出来了,比如sha1prng。伪随机数的安全性依赖于种子。怎么实现sha1prng和其他的伪随机数算法,我们现在不做讨论。java和C#都有相应的API,java的SecureRandom和C#的RNGCryptoServiceProvider。我们就不重复再轮子了。
上面的两个类都可以为我们提供伪随机的字节流。同时java还提供产生安全随机整数的API了,这里就不讨论java的安全随机数了。下面说说,怎么利用伪随机的字节流产生安全的高强度的随机整数。
有网友是这样实现,暂且命名为FastNextInt。
public int FastNextInt(int max)
{
byte[] bs = new byte[4];
generator.GetBytes(bs);
int value = BitConverter.ToInt32(bs, 0);
value = value % max;
if (value < 0) value = -value;
return value;
}
这种算法可以快速地得到一个“随机”整数,但是这种方法使得在[0,max)范围内,各整数出现的频率是不一样的。很明显,前面k=0x100000000/max*max个整数,先使得0~(max-1)中的整数各出现了0x100000000/max次。然后0~(0x100000000-k)再会出现一次。所以容易计算得,第(0x100000000-k+1)~max-1个数出现的概率为0x100000000/max/0x100000000=1/max;前面第0~(0x100000000-k)个数出现的概率为(0x100000000/max+1)/0x10000000。其概率比1/max大了1/0x10000000.因此,出现次数多的数就比出现次数少的数多出了约(1/0x100000000)/(1/max)=max/0x100000000的可能。比如,假设max=0x7FFFFFFF;2,3,4....出现了两次,其他数只出现了3次。就相对多出了1/2的可能。举个例子,如果面向21亿观众抽奖,明显这样就显得不公平了。
是不是这样就无解了?首先回过头思考一下,多出来的可能等于max/0x100000000。如果把分母扩大会怎么样?假如将分母扩大到0x10000000000000000,max在32位的范围内,多出来的可能约为几十亿分之一,是不是可以忽略不计?于是就有了FastNextLong
public long FastNextLong(long max)
{
byte[] bs = new byte[8];
generator.GetBytes(bs);
long value = BitConverter.ToInt64(bs, 0);
value = value % max;
if (value < 0) value = -value;
return value;
}
这样,既保持了良好的运算性能,又获得更公平的随机数。
有没有更加公平、安全的随机数算法。答案是有的,并且是符合数学理论的。由于我们的计算机式二进制的,所以只能产生1,2,3,……位的随机数,也就是说可以产生0~1,0~3,0~7,0~15,……这种范围随机数。每个数出现的可能只能是1/(2^1),1/(2^2) ,……其实,我们可以这样做,如果我们要产生0~n的随机数,我们先求出K,其中K是满足n<=2^k的k的最小整数值。接着产生一个K位的随机序列。如果这个序列表示的值属于0~n,那么输出这个数值。在获得输出的条件下,容易得到0~n出现的可能性都是相等的。如果这个序列表示的值大于n,那么再产生一个新数列,循环操作,直到这个序列满足输出条件。容易得到,一个序列获得输出的可能性大于1/2.继续下一轮循环的可能性小于1/2.根据数学的N次独立重复试验概率计算公式,N次没有获得输出的可能性小于(1/2)^N。例如,操作10轮后,没有获得输出的可能性为1/1024.又由于这个概率是指数级减小的,同时,只用到了移位,比较就可以实现。所以这个算法并不需要多少计算资源就可以获得输出。由于FastNextInt用到了耗时的模除,所以这个算法并不比FastNextInt更耗多少资源。
下面是完整的源代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace RNG
{
public class SecureRandom
{
private readonly static double DOUBLE_UNIT = 1.0 / (1L << 53);
RandomNumberGenerator generator;
public SecureRandom(RandomNumberGenerator generator)
{
this.generator = generator;
}
//产生bits位的随机整数
// bits>=1
protected uint Next(int bits)
{
byte[] bs = new byte[4];
generator.GetBytes(bs);
uint x = BitConverter.ToUInt32(bs, 0);
return x >> (32 - bits);
}
//产生bits位的随机整数
//bits>=1
protected ulong Next2(int bits)
{
byte[] bs = new byte[8];
generator.GetBytes(bs);
ulong x = BitConverter.ToUInt64(bs, 0);
return x >> (64 - bits);
}
protected int BitLength(int x)
{
int len = 0;
while (x > 0)
{
len++;
x >>= 1;
}
return len;
}
protected int BitLength(long x)
{
int len = 0;
while (x > 0)
{
len++;
x >>= 1;
}
return len;
}
public int NextInt()
{
return (int)Next(32);
}
//max>=1,不包括max
public int NextInt(int max)
{
if (max <= 0) throw new ArgumentException("max <= 0");
int len = BitLength(max);
uint x = Next(len);
while (x >= max)
{
x = Next(len);
}
return (int)x;
}
//不包括max
public int NextInt(int min, int max)
{
return NextInt(max - min) + min;
}
//max>=1,不包括max
public int FastNextInt(int max)
{
byte[] bs = new byte[4];
generator.GetBytes(bs);
int value = BitConverter.ToInt32(bs, 0);
value = value % max;
if