随机浮点数生成

323

如何在C++中生成随机浮点数?

我以为可以使用整数rand并除以一些值,这样是否足够呢?


2
这取决于您需要数字的用途以及需要多么随机。通常rand()会提供15位的随机性,但浮点数具有23位精度,因此它会错过一些值。 - Pete Kirkham
2
我已经更新了我的答案,包括所有主要选项,而我选择关注在C++11中添加的“random”头文件,它得到了标准文档N3924:C++14中不鼓励使用rand()的进一步支持。我在我的答案中包括了rand(),主要是出于历史考虑,但也意识到遗留应用程序确实存在。 - Shafik Yaghmour
1
我的回答包括如何使用<random>头文件来避免每次得到相同的数字。 - Andreas DM
15个回答

443

rand()函数可以在C++中生成伪随机数。结合RAND_MAX和一些数学知识,您可以生成任意区间内的随机数。这对于学习目的和玩具程序是足够的。如果您需要带有正态分布的真正的随机数,则需要使用更高级的方法。


这将生成一个从0.0到1.0(包括0.0和1.0)的数字。

float r = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);

这将生成一个从 0.0 到某个随机 floatX 的数字:

float r2 = static_cast <float> (rand()) / (static_cast <float> (RAND_MAX/X));

这将生成一个数字,范围从任意的LO到任意的HI

float r3 = LO + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(HI-LO)));
请注意,如果您需要真正随机的数字,则rand()函数通常不足够。在调用rand()之前,必须首先通过调用srand()来“种子化”随机数生成器。这应该在程序运行期间仅执行一次,而不是每次调用rand()时都执行。通常像这样完成:
srand (static_cast <unsigned> (time(0)));

如果要调用randsrand函数,必须添加#include <cstdlib>头文件。

如果要调用time函数,必须添加#include <ctime>头文件。


28
别忘了先播种! - Klaim
15
请注意,两个限制都包含在内。 - dmckee --- ex-moderator kitten
16
这个答案是误导性的。它在上周的Going Native 2013会议上已经讨论过了;rand()函数被认为是有害的,http://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful 提供了非常详细的解释。 - Ade Miller
22
我不明白为什么有那么多人点赞这个答案。它在数学上是不正确的。RAND_MAX是一个非常小的数字(通常为2^16)。这意味着从浮点数的23位中,只有15位是随机的。其他位可能会是零。确实会得到均匀分布的随机数,但精度很低。例如,您的随机生成器可以生成0.00001和0.00002,但无法生成0.000017。因此,您拥有低精度的均匀分布(比实际浮点数精度低256倍)。 - DanielHsH
16
@DanielHsH:该帖子的提问者明确询问使用rand()生成随机浮点数的机制。这个问题和我的答案都专门聚焦于学习基础,而不关心高度精度。你必须先学会走,才能学会奔跑。 - John Dibling
显示剩余20条评论

176

C++11给你带来了很多新的选择,包括random。该主题的经典论文是N3551,《C++11中的随机数生成》

要了解为什么使用rand()可能会有问题,请参阅Stephan T. Lavavej在GoingNative 2013活动期间提供的rand()被视为有害演示资料。幻灯片在评论中,这里是一个直接链接

我还涵盖了boost以及使用rand,因为旧代码可能仍需要其支持。

下面的示例是从cppreference网站中提取出来的,它使用了std::mersenne_twister_engine引擎和std::uniform_real_distribution分布,该分布在区间[0,10)内生成数字,其他引擎和分布被注释掉了(点击这里查看实时演示)。
#include <iostream>
#include <iomanip>
#include <string>
#include <map>
#include <random>

int main()
{
    std::random_device rd;

    //
    // Engines 
    //
    std::mt19937 e2(rd());
    //std::knuth_b e2(rd());
    //std::default_random_engine e2(rd()) ;

    //
    // Distribtuions
    //
    std::uniform_real_distribution<> dist(0, 10);
    //std::normal_distribution<> dist(2, 2);
    //std::student_t_distribution<> dist(5);
    //std::poisson_distribution<> dist(2);
    //std::extreme_value_distribution<> dist(0,2);

    std::map<int, int> hist;
    for (int n = 0; n < 10000; ++n) {
        ++hist[std::floor(dist(e2))];
    }

    for (auto p : hist) {
        std::cout << std::fixed << std::setprecision(1) << std::setw(2)
                  << p.first << ' ' << std::string(p.second/200, '*') << '\n';
    }
}

输出将类似于以下内容:

0 ****
1 ****
2 ****
3 ****
4 *****
5 ****
6 *****
7 ****
8 *****
9 ****

输出将根据您选择的分布而异,因此如果我们决定使用具有2的值的std::normal_distribution来表示meanstddev,例如dist(2, 2),则输出将类似于这个(查看实例):

-6 
-5 
-4 
-3 
-2 **
-1 ****
 0 *******
 1 *********
 2 *********
 3 *******
 4 ****
 5 **
 6 
 7 
 8 
 9 

以下是对《N3551》中提供的一些代码的修改版本(点击此处查看实际效果):
#include <algorithm>
#include <array>
#include <iostream>
#include <random>

std::default_random_engine & global_urng( )
{
    static std::default_random_engine u{};
    return u ;
}

void randomize( )
{
    static std::random_device rd{};
    global_urng().seed( rd() );
}

int main( )
{
  // Manufacture a deck of cards:
  using card = int;
  std::array<card,52> deck{};
  std::iota(deck.begin(), deck.end(), 0);
 
  randomize( ) ;  
    
  std::shuffle(deck.begin(), deck.end(), global_urng());
  // Display each card in the shuffled deck:
  auto suit = []( card c ) { return "SHDC"[c / 13]; };
  auto rank = []( card c ) { return "AKQJT98765432"[c % 13]; };
 
  for( card c : deck )
      std::cout << ' ' << rank(c) << suit(c);
 
   std::cout << std::endl;
}

结果将类似于:

5H 5S AS 9S 4D 6H TH 6D KH 2S QS 9H 8H 3D KC TD 7H 2D KS 3C TC 7D 4C QH QC QD JD AH JC AC KD 9D 5C 2H 4H 9C 8C JH 5D 4S 7C AD 3S 8S TS 2C 8D 3H 6C JS 7S 6S

增强

当然,Boost.Random也是一个选择,这里我使用的是boost::random::uniform_real_distribution

#include <iostream>
#include <iomanip>
#include <string>
#include <map>
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_real_distribution.hpp>

int main()
{
    boost::random::mt19937 gen;
    boost::random::uniform_real_distribution<> dist(0, 10);

    std::map<int, int> hist;
    for (int n = 0; n < 10000; ++n) {
        ++hist[std::floor(dist(gen))];
    }

    for (auto p : hist) {
        std::cout << std::fixed << std::setprecision(1) << std::setw(2)
                  << p.first << ' ' << std::string(p.second/200, '*') << '\n';
    }
}

rand()

如果您必须使用 rand() ,那么我们可以查看 C FAQ,以获取有关 如何生成浮点随机数的指南 的信息,该指南基本上提供了一个类似于以下示例的方法来生成介于区间 [0,1) 内的随机数:

#include <stdlib.h>

double randZeroToOne()
{
    return rand() / (RAND_MAX + 1.);
}

生成一个在范围 [M,N) 内的随机数:

double randMToN(double M, double N)
{
    return M + (rand() / ( RAND_MAX / (N-M) ) ) ;  
}

1
请问你能否修复你的randMToN函数?你可以在函数中注明参数为[M,N],或者将之前的+1加回到randZeroToOne里。例如,在调用时输入randMToN(0.0, 1.0) - BeyelerStudios
1
(N-M)处进行除零操作时要小心。在此处找到处理此错误的好方法:https://dev59.com/ulwY5IYBdhLWcg3wEEN2#50539103 - DrBeco
如果有人想看看标准库中如何实现以及为什么它比朴素的除法给出更好的值,请参见此处:https://github.com/microsoft/STL/blob/568a21bec058291e551e05b87233b37dd54e9112/stl/inc/random#L242 - Azmisov

68

看看Boost.Random。你可以像这样做:

float gen_random_float(float min, float max)
{
    boost::mt19937 rng;
    boost::uniform_real<float> u(min, max);
    boost::variate_generator<boost::mt19937&, boost::uniform_real<float> > gen(rng, u);
    return gen();
}

可以尝试一下,也许你会发现每次传递相同的mt19937对象比构建新对象更好,但希望你能明白这个想法。


1
uniform_real使用半开区间[min,max),这意味着您将获得最小值,但永远不会达到最大值。虽然如果以某种方式四舍五入,可以解决这个问题,但需要考虑这一点。 - Signal
22
这现在是C++11的一部分。 - Tomas Andrle
@Wolf 在实际应用中,命中任何特定浮点值的几率都非常低,因此包含或排除端点并不重要。如果您需要 max 但可以使用开放式的 min,则可以轻松地反转区间:return min + max - gen(); - Mark Ransom

51
在现代的C++中,您可以使用随C++11一起提供的<random>头文件。
要获取随机的float,您可以使用std::uniform_real_distribution<>
您可以使用一个函数来生成数字,如果您不想让数字一直相同,则将引擎和分布设置为static
例如:
float get_random()
{
    static std::default_random_engine e;
    static std::uniform_real_distribution<> dis(0, 1); // range [0, 1)
    return dis(e);
}

最好将 float 放在容器中,例如 std::vector

int main()
{
    std::vector<float> nums;
    for (int i{}; i != 5; ++i) // Generate 5 random floats
        nums.emplace_back(get_random());

    for (const auto& i : nums) std::cout << i << " ";
}

示例输出:

0.0518757 0.969106 0.0985112 0.0895674 0.895542

1
std::uniform_real_distribution<> dis(0, 1); // rage 0 - 1 is technically incorrect, 1.0 will never be generated, see http://en.cppreference.com/w/cpp/numeric/random/uniform_real_distribution To create a distribution over the closed interval [a,b], std::nextafter(b, std::numeric_limits<RealType>::max()) may be used as the second parameter. - Troyseph
7
这应该是被采纳的答案,已经到了2020年了。 - Alex

34

使用两个 float 值调用代码,该代码适用于任何范围。

float rand_FloatRange(float a, float b)
{
    return ((b - a) * ((float)rand() / RAND_MAX)) + a;
}

1
值得一提的是,这是C99或C++11中fmaf()(或C++中的浮点fma()重载)的潜在用例,它可能会保留更多的精度。例如:fmaf((float)rand() / RAND_MAX, b - a, a) - Tim Čas
这个方法是可行的,但请注意它会在结果中引入一个(非常)小的偏差。请参考此处获取更多信息(您的方法是“FP Multiply (Biased)”):https://www.pcg-random.org/posts/bounded-rands.html - Venryx
除了@Venryx的回答,显然还有一整篇关于这个主题的论文:https://hal.archives-ouvertes.fr/hal-03282794/document。虽然很平凡,但这并不是一个真正解决的问题! - Mingye Wang

22
如果你使用的是C++而不是C,那么请记住,在技术报告1(TR1)和C++0x草案中,他们已经在头文件中添加了一个随机数生成器,我相信它与Boost.Random库相同,而且比C库函数rand更灵活、更"现代"。
这个语法提供了选择生成器(比如mersenne twistermt19937)和选择分布(正态分布、伯努利分布、二项式分布等)的能力。
语法如下(从this site无耻地借鉴):
  #include <iostream>
  #include <random>

  ...

  std::tr1::mt19937 eng;  // a core engine class 
  std::tr1::normal_distribution<float> dist;     

  for (int i = 0; i < 10; ++i)        
      std::cout << dist(eng) << std::endl;

2
这现在是在C++11中,同时dist可以用最小值和最大值进行初始化。 - Étienne
对我来说,在初始化器中放置最小值和最大值并在获取值时提供生成器似乎有些奇怪 - 我更喜欢反过来,但没关系。 - yoyo

6

在某些系统上(比如使用VC编译的Windows),RAND_MAX非常小,只有15位二进制数。因此,当你将随机数除以RAND_MAX时,实际上只生成了15位的尾数,而不是可能的23位。这可能或可能不会对你造成问题,但在这种情况下你会错过一些值。

顺便提一下,我注意到已经有人针对这个问题发表了评论。以下是可能解决这个问题的一些代码:

float r = (float)((rand() << 15 + rand()) & ((1 << 24) - 1)) / (1 << 24);

未经测试,但可能有效 :-)

那么对于 float r = (float)((rand() << 9) | rand()) / RAND_MAX 呢?(也未经测试) - Trap
啊,抱歉,除以RAND_MAX不会有任何用处...这个技巧的整个重点是要有一个比RAND_MAX大的东西...这也为我解决了问题。 - Joey
2
在生成随机数时要小心,不要没有理论地随意组合。连续调用rand()可能不会完全独立。提示:如果使用线性同余生成器,请观察连续调用的最低位:它会在0和1之间交替。 - RBerteig
我知道。对于某些应用程序来说,这可能已经足够了。但是,在这种情况下,您应该使用不止两个调用。在这种情况下,没有万能解决方案,您甚至不能依赖它是LCG。其他伪随机数生成器高位弱。在这里,Boost解决方案应该是最好的。 - Joey
rand返回的最低位不是RNG状态的最低位。进行100次rand()调用,我得到了以下结果:1100100000111111101010010010011010101110110110111010011111100100000000010100011011000000100101100011。Java使用48位LCG,只使用32位,VC似乎也是这样做的。 - Joey

4

drand48(3)是POSIX标准的方法。GLibC还提供了可重入版本drand48_r(3)

该函数在SVID 3中被声明为过时,但没有提供足够的替代方案,因此IEEE Std 1003.1-2013仍然将其包含在内,并且没有注明它会很快消失。

在Windows中,标准方法是CryptGenRandom()


2
如果您知道您的浮点数格式是IEEE 754(几乎所有现代CPU,包括Intel和ARM),那么您可以使用位操作方法从随机整数构建随机浮点数。只有在没有访问C++11的randomBoost.Random时才应考虑这种方法,因为它们都更好。
float rand_float()
{
    // returns a random value in the range [0.0-1.0)

    // start with a bit pattern equating to 1.0
    uint32_t pattern = 0x3f800000;

    // get 23 bits of random integer
    uint32_t random23 = 0x7fffff & (rand() << 8 ^ rand());

    // replace the mantissa, resulting in a number [1.0-2.0)
    pattern |= random23;

    // convert from int to float without undefined behavior
    assert(sizeof(float) == sizeof(uint32_t));
    char buffer[sizeof(float)];
    memcpy(buffer, &pattern, sizeof(float));
    float f;
    memcpy(&f, buffer, sizeof(float));

    return f - 1.0;
}

这将比使用除法得到更好的分配。

8
我不确定你为什么说这样会得到“更好的分布”。事实上,这将会和仅仅使用return (float)random23 / (1 << 23)得到完全相同的分布。(是的,我刚刚测试过,修改了你的函数以接受random32作为参数,并对从零到(1 << 23)-1的所有值运行它。而且,你的方法确实给出与除以1 << 23相同的结果。) - Ilmari Karonen

2

迄今为止,我对所有答案都不满意,所以我编写了一个新的随机浮点数函数。该函数对浮点数据类型进行位假设。它仍需要一个至少有15个随机位的rand()函数。

//Returns a random number in the range [0.0f, 1.0f).  Every
//bit of the mantissa is randomized.
float rnd(void){
  //Generate a random number in the range [0.5f, 1.0f).
  unsigned int ret = 0x3F000000 | (0x7FFFFF & ((rand() << 8) ^ rand()));
  unsigned short coinFlips;

  //If the coin is tails, return the number, otherwise
  //divide the random number by two by decrementing the
  //exponent and keep going. The exponent starts at 63.
  //Each loop represents 15 random bits, a.k.a. 'coin flips'.
  #define RND_INNER_LOOP() \
    if( coinFlips & 1 ) break; \
    coinFlips >>= 1; \
    ret -= 0x800000
  for(;;){
    coinFlips = rand();
    RND_INNER_LOOP(); RND_INNER_LOOP(); RND_INNER_LOOP();
    //At this point, the exponent is 60, 45, 30, 15, or 0.
    //If the exponent is 0, then the number equals 0.0f.
    if( ! (ret & 0x3F800000) ) return 0.0f;
    RND_INNER_LOOP(); RND_INNER_LOOP(); RND_INNER_LOOP();
    RND_INNER_LOOP(); RND_INNER_LOOP(); RND_INNER_LOOP();
    RND_INNER_LOOP(); RND_INNER_LOOP(); RND_INNER_LOOP();
    RND_INNER_LOOP(); RND_INNER_LOOP(); RND_INNER_LOOP();
  }
  return *((float *)(&ret));
}

8
有趣的方法,我想点赞,但是我真的不明白正在发生什么。 - hasen

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