如何从随机字节数组值中获取随机双精度浮点数值?

5

我想使用RNGCryptoServiceProvider作为我的随机数源。由于它只能将随机数输出为字节数组,那么如何将它们转换为0到1之间的双精度浮点数,并保持结果的均匀性?

4个回答

12
byte[] result = new byte[8];
rng.GetBytes(result);
return (double)BitConverter.ToUInt64(result,0) / ulong.MaxValue;

2
@bruce965 第一个问题:这是因为ulong.MaxValue是分子中最大的数字。第二个问题:不,因为分子始终为正(它是无符号的)。第三个问题:我不知道你在说什么... 3与任何事情有什么关系?2^64double.MaxValue小得多,所以没有Infinity,它唯一可能变成NaN的方式是分母为零,但它不是。编辑:也许你认为将其转换为double是位拷贝?它不是。 - AnorZaken
我不知道当时我在想什么,我对你给出的三个答案都非常正确,谢谢。 - Fabio Iotti
@AnorZaken Double类型只能保留53位整数精度。我不确定这会带来什么影响。但是,如果你只是简单地做 x / ulong.MaxValue 你就会遇到一种情况,即你可能会返回一个值为 1.0 的结果,而通常 NextDouble API 应该返回小于 1.0 的值,这样你就可以安全地使用它来生成数组的索引,并且也不必担心越界的问题。例如, arr[(int)(r * arr.Length)] 其中r在范围 [0, 1)内 ,即 0.0 <= r && r < 1.0 - John Leidegren
@JohnLeidegren 这意味着几件事情!(54位。)首先:10个rng位被浪费了,因此如果使用长整型和7个字节的C#等效结构联合体(其中最高字节的2个最高位始终为零),则速度会更快,而不是使用BitConverter...但也有代码可读性的问题。其次,您对1.0的担忧:(double)(ulong.MaxValue - 1) / ulong.MaxValue由于强制转换时的截断,仍将导致1.0!这是仅使用54位随机数的更强烈原因!使用超过54位的任何数字都会使正确防止1.0变得棘手(续...) - AnorZaken
更正:就像你所说的那样,它是53位,但是(2^54 - 2) * (1.0 / (2^54 - 1))将产生最大的可能小于1.0的双精度值,正如所述。 - AnorZaken
显示剩余5条评论

6
这是我会做的方法。
private static readonly System.Security.Cryptography.RNGCryptoServiceProvider _secureRng;
public static double NextSecureDouble()
{
  var bytes = new byte[8];
  _secureRng.GetBytes(bytes);
  var v = BitConverter.ToUInt64(bytes, 0);
  // We only use the 53-bits of integer precision available in a IEEE 754 64-bit double.
  // The result is a fraction, 
  // r = (0, 9007199254740991) / 9007199254740992 where 0 <= r && r < 1.
  v &= ((1UL << 53) - 1);
  var r = (double)v / (double)(1UL << 53);
  return r;
}

巧合的是,9007199254740991 / 9007199254740992 约等于 0.99999999999999988897769753748436,这也是 Random.NextDouble 方法将返回的最大值(请参阅 https://msdn.microsoft.com/en-us/library/system.random.nextdouble(v=vs.110).aspx)。
一般情况下,连续均匀分布的标准差为 (max - min) / sqrt(12)。
对于样本量为1000,我能够在2%的误差范围内得到可靠的结果。
对于样本量为10000,我能够在1%的误差范围内得到可靠的结果。
以下是我验证这些结果的方法。
[Test]
public void Randomness_SecureDoubleTest()
{
  RunTrials(1000, 0.02);
  RunTrials(10000, 0.01);
}

private static void RunTrials(int sampleSize, double errorMargin)
{
  var q = new Queue<double>();

  while (q.Count < sampleSize)
  {
    q.Enqueue(Randomness.NextSecureDouble());
  }

  for (int k = 0; k < 1000; k++)
  {
    // rotate
    q.Dequeue();
    q.Enqueue(Randomness.NextSecureDouble());

    var avg = q.Average();

    // Dividing by n−1 gives a better estimate of the population standard
    // deviation for the larger parent population than dividing by n, 
    // which gives a result which is correct for the sample only.

    var actual = Math.Sqrt(q.Sum(x => (x - avg) * (x - avg)) / (q.Count - 1));

    // see http://stats.stackexchange.com/a/1014/4576

    var expected = (q.Max() - q.Min()) / Math.Sqrt(12);

    Assert.AreEqual(expected, actual, errorMargin);
  }
}

我认为这应该是被接受的答案,因为它与 Random 的行为相同,从不返回 1.0。如果我错了,请纠正我...你可以简化代码的一部分: v &= ((1UL << 53) - 1)v >> 11 是一样的,对吗? - Yellowfive
@Yellowfive 当然可以,不过 v &= ((1UL << 53) - 1) 是一种常见的位掩码模式。我从未见过使用位移来进行位掩码。此外,格式中包含53,这将告诉您在掩码操作完成后还剩下确切的53个有效位信息。这就是为什么我更喜欢这种掩码方法的原因。 - John Leidegren

-2
你可以使用BitConverter.ToDouble(...)方法。它接受一个字节数组并返回一个Double类型的值。对于大多数其他基本类型,都有相应的方法,以及将基本类型转换为字节数组的方法。

有些特殊情况需要避免使用 double(例如 NaN)。 - Jon B
Jon是对的。最糟糕的是这个数字不在0和1之间。 - Mehrdad Afshari
尝试这个:BitConverter.GetBytes(double.NaN)。返回一个以248、255结尾的8字节数组。 - Jon B

-2
使用BitConverter将一系列随机字节转换为Double:
byte[] random_bytes = new byte[8];  // BitConverter will expect an 8-byte array
new RNGCryptoServiceProvider().GetBytes(random_bytes);

double my_random_double = BitConverter.ToDouble(random_bytes, 0);

请看我对迈克尔帖子的评论。可能会导致NaN,-Infinity等结果。 - Jon B
谢谢,但结果会非常不均匀,它们的密度向0增加,没有机会得到介于0.5和1之间的任何东西。 - Kamil Zadora

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