在C#中生成随机小数

78

如何获取一个随机的 System.Decimal?System.Random 并不直接支持它。


9
生成一个在 1 到 999 之间的随机整数,然后将结果除以 100,这样做不是更简单吗?例如,随机数 1 会变成 0.01,而 999 会变成 9.99。 - Stefan Z Camilleri
@StefanZCamilleri 取决于您需要随机小数的用途。请阅读下面一些答案。可以表示的可能Decimal值的完整范围非常大,需要一些思考才能获得随机整数以馈入Decimal构造函数。然后就是如何均匀生成的随机分布的十进制值的问题。 - Daniel Ballinger
此外,如果我没记错的话,0.01不在1和999之间。它小于1,因此不是有效的响应。 - Gary O. Stenstrom
14个回答

61

编辑:删除旧版本

这与Daniel的版本类似,但将提供完整范围。它还引入了一个新的扩展方法来获取随机的“任何整数”值,我认为这很方便。

请注意,这里小数的分布不是均匀的。

/// <summary>
/// Returns an Int32 with a random value across the entire range of
/// possible values.
/// </summary>
public static int NextInt32(this Random rng)
{
     int firstBits = rng.Next(0, 1 << 4) << 28;
     int lastBits = rng.Next(0, 1 << 28);
     return firstBits | lastBits;
}

public static decimal NextDecimal(this Random rng)
{
     byte scale = (byte) rng.Next(29);
     bool sign = rng.Next(2) == 1;
     return new decimal(rng.NextInt32(), 
                        rng.NextInt32(),
                        rng.NextInt32(),
                        sign,
                        scale);
}

3
@Hosam:从小数的数量级来看,它绝对是不均匀的。但每个位模式出现的概率是相同的,因此在这方面是均匀的。如果要生成均匀的模式,最好生成0到1之间的数字。 - Jon Skeet
@JonSkeet,为什么您创建了NextInt32而不是直接使用Next(0, Int.MaxValue)? - johnny 5
@JonSkeet 所以这只是为了包含那个值,这样分布就更均匀?所以如果使用 next 没有什么问题,我只需要处理少 1 个值? - johnny 5
1
@LosManos:Next() 永远不会返回负数。 NextInt32 可以返回任何 int - Jon Skeet
1
@D.R.:是的,没错。我已经记不起来(8年半后)为什么要加上它了,但可能之前有一些非位运算操作。现在已经删除了。 - Jon Skeet
显示剩余18条评论

14

通过易于操作的工具,也可以轻松实现:

var rand = new Random();
var item = new decimal(rand.NextDouble());

2
这可能适用于某些用例,但它不能生成 .NET decimal 可支持精度的小数。 - Michael Fry

12

您通常期望随机数生成器不仅生成随机数字,而且这些数字是均匀随机生成的。

均匀随机有两个定义:离散均匀随机连续均匀随机

对于具有有限数量不同可能结果的随机数生成器,离散均匀随机是有意义的。例如,在1和10之间生成一个整数。然后您期望得到4的概率与得到7的概率相同。

当随机数生成器在范围内生成数字时,连续均匀随机是有意义的。例如,在0和1之间生成实数的生成器。然后您期望得到介于0和0.5之间的数字的概率与得到介于0.5和1之间的数字的概率相同。

当随机数生成器生成浮点数时(基本上就是System.Decimal - 它只是带有10进制基数的浮点数),均匀随机的正确定义是有争议的:

一方面,由于计算机中浮点数是用固定数量的位表示的,很明显有有限数量的可能结果。因此,有人认为适当的分布是具有相同概率的离散连续分布,每个可表示数字都属于这种分布。这基本上就是Jon SkeetJohn Leidegren的实现方法。
另一方面,有人认为,由于浮点数应该是实数的近似值,我们最好尝试近似连续随机数生成器的行为——即使我们实际上的RNG是离散的。这是通过Random.NextDouble()获得的行为,即使在范围0.00001-0.00002中有大约与范围0.8-0.9中的可表示数字一样多的数字,你在第二个范围内得到数字的可能性要高一千倍——这是你所期望的。
因此,Random.NextDecimal()的适当实现可能是连续均匀分布的。
这里是Jon Skeet答案的一个简单变化,它在0到1之间均匀分布(我重复使用了他的NextInt32()扩展方法):
public static decimal NextDecimal(this Random rng)
{
     return new decimal(rng.NextInt32(), 
                        rng.NextInt32(),
                        rng.Next(0x204FCE5E),
                        false,
                        0);
}

您还可以讨论如何在所有小数范围内获得均匀分布。可能有一种更简单的方法,但是对John Leidegren的答案进行轻微修改应该可以产生相对均匀的分布:

private static int GetDecimalScale(Random r)
{
  for(int i=0;i<=28;i++){
    if(r.NextDouble() >= 0.1)
      return i;
  }
  return 0;
}

public static decimal NextDecimal(this Random r)
{
    var s = GetDecimalScale(r);
    var a = (int)(uint.MaxValue * r.NextDouble());
    var b = (int)(uint.MaxValue * r.NextDouble());
    var c = (int)(uint.MaxValue * r.NextDouble());
    var n = r.NextDouble() >= 0.5;
    return new Decimal(a, b, c, n, s);
}

基本上,我们确保比例的值与相应范围的大小成比例地选择。
这意味着我们应该90%的时间获取0的比例尺 - 因为该范围包含了可能范围的90% - 1的比例尺9%的时间等等。
实现仍然存在一些问题,因为它没有考虑到某些数字具有多个表示方式 - 但是它应该比其他实现更接近均匀分布。

这与 28 * r.NextDouble() 有何不同? - Hosam Aly
提示:r.Next(2)==0 似乎比 r.NextDouble()>=0.5 更快。 - D.R.
@RasmusFaber:我在想是否有办法以某种方式加快GetDecimalScale的速度,最好只需调用一次Random。有什么想法或指针吗? - D.R.
这里呈现的代码非常有用且受到赞赏。然而,不幸的是,这个答案在整个过程中误用了“连续”这个词和概念,当它似乎在谈论非整数时。 “连续”是一个数学概念,但不是计算机科学领域中真实世界有效的术语。在编程中,一切都是技术上离散的,没有“连续”,只有对其的离散近似,您对“离散”与“连续”的区分最好用“整数”或“积分”与“非积分”,“数字”或“分数”等进行替换。 - RBarryYoung
经过进一步的思考,我认为如果你只是将“continuous”更改为“continuous distribution”,那么这一切都会很好。始终包括“distribution”可以清楚地表明你正在引用随机分布的数学概念。目前的写法似乎是在谈论来自这些分布的值的计算机表示,这些值根本不是连续的。 - RBarryYoung
显示剩余5条评论

9
我知道这是一个老问题,但 Rasmus Faber描述的分布问题一直困扰着我,所以我想到了以下解决方法。我没有深入研究Jon Skeet提供的NextInt32实现,并且假设(希望)它与Random.Next()具有相同的分布。
//Provides a random decimal value in the range [0.0000000000000000000000000000, 0.9999999999999999999999999999) with (theoretical) uniform and discrete distribution.
public static decimal NextDecimalSample(this Random random)
{
    var sample = 1m;
    //After ~200 million tries this never took more than one attempt but it is possible to generate combinations of a, b, and c with the approach below resulting in a sample >= 1.
    while (sample >= 1)
    {
        var a = random.NextInt32();
        var b = random.NextInt32();
        //The high bits of 0.9999999999999999999999999999m are 542101086.
        var c = random.Next(542101087);
        sample = new Decimal(a, b, c, false, 28);
    }
    return sample;
}

public static decimal NextDecimal(this Random random)
{
    return NextDecimal(random, decimal.MaxValue);
}

public static decimal NextDecimal(this Random random, decimal maxValue)
{
    return NextDecimal(random, decimal.Zero, maxValue);
}

public static decimal NextDecimal(this Random random, decimal minValue, decimal maxValue)
{
    var nextDecimalSample = NextDecimalSample(random);
    return maxValue * nextDecimalSample + minValue * (1 - nextDecimalSample);
}

6
这是我使用的范围内的十进制随机实现,很好用。
public static decimal NextDecimal(this Random rnd, decimal from, decimal to)
{
    byte fromScale = new System.Data.SqlTypes.SqlDecimal(from).Scale;
    byte toScale = new System.Data.SqlTypes.SqlDecimal(to).Scale;

    byte scale = (byte)(fromScale + toScale);
    if (scale > 28)
        scale = 28;

    decimal r = new decimal(rnd.Next(), rnd.Next(), rnd.Next(), false, scale);
    if (Math.Sign(from) == Math.Sign(to) || from == 0 || to == 0)
        return decimal.Remainder(r, to - from) + from;

    bool getFromNegativeRange = (double)from + rnd.NextDouble() * ((double)to - (double)from) < 0;
    return getFromNegativeRange ? decimal.Remainder(r, -from) + from : decimal.Remainder(r, to);
}

System.Data是必需的吗? - Sonic Soul

2

我对此感到困惑了一段时间。这是我能想到的最好解释:

public class DecimalRandom : Random
    {
        public override decimal NextDecimal()
        {
            //The low 32 bits of a 96-bit integer. 
            int lo = this.Next(int.MinValue, int.MaxValue);
            //The middle 32 bits of a 96-bit integer. 
            int mid = this.Next(int.MinValue, int.MaxValue);
            //The high 32 bits of a 96-bit integer. 
            int hi = this.Next(int.MinValue, int.MaxValue);
            //The sign of the number; 1 is negative, 0 is positive. 
            bool isNegative = (this.Next(2) == 0);
            //A power of 10 ranging from 0 to 28. 
            byte scale = Convert.ToByte(this.Next(29));

            Decimal randomDecimal = new Decimal(lo, mid, hi, isNegative, scale);

            return randomDecimal;
        }
    }

编辑:正如评论中所指出的那样,lo、mid和hi永远不可能包含int.MaxValue,因此无法完全涵盖所有Decimal范围。


不太对... Random.Next(int.MinValue, int.MaxValue) 永远不会返回 int.MaxValue。我有一个答案,但我认为我可以改进它。 - Jon Skeet
1
统计学不是我的强项,所以我可能错了,但我担心分布可能不太均匀。 - Michael Burr
你可以使用BitConverter.ToInt32()和Random.NextBytes()来获得一个完整范围的随机整数。 - undefined

1

由于OP的问题非常广泛,只需要一个没有任何限制的随机System.Decimal,下面是我使用的非常简单的解决方案。

我并不关心生成的数字的任何统一性或精度,所以如果您有一些限制,其他答案可能更好,但在简单情况下,这个答案很好用。

Random rnd = new Random();
decimal val;
int decimal_places = 2;
val = Math.Round(new decimal(rnd.NextDouble()), decimal_places);

在我的具体情况中,我正在寻找一个随机小数作为货币字符串使用,所以我的完整解决方案是:
string value;
value = val = Math.Round(new decimal(rnd.NextDouble()) * 1000,2).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);

1
说实话,我不相信C# decimal的内部格式与许多人想象的方式相同。因此,这里提出的一些解决方案可能无效或不一致。请考虑以下两个数字及其在decimal格式中的存储方式:
0.999999999999999m
Sign: 00
96-bit integer: 00 00 00 00 FF 7F C6 A4 7E 8D 03 00
Scale: 0F

并且

0.9999999999999999999999999999m
Sign: 00
96-bit integer: 5E CE 4F 20 FF FF FF 0F 61 02 25 3E
Scale: 1C

请注意比例尺的不同,但两个值几乎相同,即它们都仅比1小一点点。看起来比例尺和数字位数有直接关系。除非我漏掉了什么,否则这应该会对任何改变十进制小数的96位整数部分但保持比例不变的代码造成问题。
在实验中,我发现数字0.9999999999999999999999999999m(有28个9)是小数点前最大数量的9,此后小数点将四舍五入为1.0m。
进一步实验证明以下代码将变量“Dec”设置为值0.9999999999999999999999999999m:
double DblH = 0.99999999999999d;
double DblL = 0.99999999999999d;
decimal Dec = (decimal)DblH + (decimal)DblL / 1E14m;

这个发现启发了我对Random类的扩展,下面的代码中可以看到。我相信这段代码是完全可用和良好运作的,但很高兴有其他人来检查是否有错误。我不是统计学家,所以无法确定此代码是否产生真正均匀分布的小数,但如果必须猜测,我会说它没有达到完美,但非常接近(例如,51万亿次调用中只有1次偏向某个数字范围)。
第一个NextDecimal()函数应该产生大于等于0.0m且小于1.0m的值。do/while语句防止RandH和RandL超过0.99999999999999d的值,通过循环直到它们低于该值。我相信这个循环重复的概率是51万亿分之一(强调相信这个词,我不信任我的数学)。这反过来应该防止函数将返回值四舍五入为1.0m。
第二个 NextDecimal() 函数应该与 Random.Next() 函数相同,只是使用十进制值而不是整数。实际上我一直没有使用这个第二个 NextDecimal() 函数并且也没有测试过它。它非常简单,所以我认为我写对了,但是再次强调,我没有测试过它 - 因此在依赖它之前,您需要确保它正常工作。
public static class ExtensionMethods {
    public static decimal NextDecimal(this Random rng) {
        double RandH, RandL;
        do {
            RandH = rng.NextDouble();
            RandL = rng.NextDouble();
        } while((RandH > 0.99999999999999d) || (RandL > 0.99999999999999d));
        return (decimal)RandH + (decimal)RandL / 1E14m;
    }
    public static decimal NextDecimal(this Random rng, decimal minValue, decimal maxValue) {
        return rng.NextDecimal() * (maxValue - minValue) + minValue;
    }
}

1
static decimal GetRandomDecimal()
    {

        int[] DataInts = new int[4];
        byte[] DataBytes = new byte[DataInts.Length * 4];

        // Use cryptographic random number generator to get 16 bytes random data
        RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

        do
        {
            rng.GetBytes(DataBytes);

            // Convert 16 bytes into 4 ints
            for (int index = 0; index < DataInts.Length; index++)
            {
                DataInts[index] = BitConverter.ToInt32(DataBytes, index * 4);
            }

            // Mask out all bits except sign bit 31 and scale bits 16 to 20 (value 0-31)
            DataInts[3] = DataInts[3] & (unchecked((int)2147483648u | 2031616));

          // Start over if scale > 28 to avoid bias 
        } while (((DataInts[3] & 1835008) == 1835008) && ((DataInts[3] & 196608) != 0));

        return new decimal(DataInts);
    }
    //end

1

在这里...使用crypt库生成一些随机字节,然后将它们转换为十进制值...请查看MSDN的十进制构造函数

using System.Security.Cryptography;

public static decimal Next(decimal max)
{
    // Create a int array to hold the random values.
    Byte[] randomNumber = new Byte[] { 0,0 };

    RNGCryptoServiceProvider Gen = new RNGCryptoServiceProvider();

    // Fill the array with a random value.
    Gen.GetBytes(randomNumber);

    // convert the bytes to a decimal
    return new decimal(new int[] 
    { 
               0,                   // not used, must be 0
               randomNumber[0] % 29,// must be between 0 and 28
               0,                   // not used, must be 0
               randomNumber[1] % 2  // sign --> 0 == positive, 1 == negative
    } ) % (max+1);
}

修改为使用不同的十进制构造函数,以提供更好的数字范围

public static decimal Next(decimal max)
{
    // Create a int array to hold the random values.
    Byte[] bytes= new Byte[] { 0,0,0,0 };

    RNGCryptoServiceProvider Gen = new RNGCryptoServiceProvider();

    // Fill the array with a random value.
    Gen.GetBytes(bytes);
    bytes[3] %= 29; // this must be between 0 and 28 (inclusive)
    decimal d = new decimal( (int)bytes[0], (int)bytes[1], (int)bytes[2], false, bytes[3]);

        return d % (max+1);
    }

这是否意味着我们只能在巨大的可能范围内限制为65536个值? - Jon Skeet
2
我们是否生活在16位计算机的时代?这是什么意思? - John Leidegren

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