为啥 rand() + rand() 会产生负数?

Posted

技术标签:

【中文标题】为啥 rand() + rand() 会产生负数?【英文标题】:Why does rand() + rand() produce negative numbers?为什么 rand() + rand() 会产生负数? 【发布时间】:2015-08-29 13:06:18 【问题描述】:

我观察到rand() 库函数在循环中仅被调用一次时,几乎总是产生正数。

for (i = 0; i < 100; i++) 
    printf("%d\n", rand());

但是当我添加两个rand() 调用时,生成的数字现在有更多的负数。

for (i = 0; i < 100; i++) 
    printf("%d = %d\n", rand(), (rand() + rand()));

谁能解释为什么我在第二种情况下看到负数?

PS:我在循环之前将种子初始化为srand(time(NULL))

【问题讨论】:

rand() 不能为负数... rand() + rand() 可以溢出 您的编译器的RAND_MAX 是什么?您通常可以在stdlib.h 中找到它。 (搞笑:查看man 3 rand,上面有一行描述“坏随机数生成器”。) 做每个理智的程序员都会做的事情abs(rand()+rand())。我宁愿有一个积极的UB而不是消极的! ;) @hexa:这对 UB 来说不是解决方案,因为添加已经发生。你不能让 UB 成为定义的行为。一个理智的程序员会像地狱一样避免UB。 【参考方案1】:

rand() 被定义为返回一个介于0RAND_MAX 之间的整数。

rand() + rand()

可能会溢出。您观察到的可能是整数溢出引起的undefined behaviour 的结果。

【讨论】:

@JakubArnold:每种语言如何以不同方式指定溢出行为?例如 Python 没有(嗯,最多可用内存),因为 int 只是增长。 @Olaf 这取决于语言如何决定表示有符号整数。 Java 没有检测整数溢出的机制(直到 java 8)并将其定义为回绕,而 Go 仅使用 2 的补码表示并将其定义为合法的有符号整数溢出。 C 显然支持多于 2 的补码。 @EvanCarslake 不,这不是普遍行为。你说的是关于 2 的补码表示。但是 C 语言也允许其他表示。 C 语言规范说有符号整数溢出是undefined。所以一般来说,任何程序都不应该依赖这种行为,并且需要仔细编码以避免导致有符号整数溢出。但这不适用于无符号整数,因为它们会以明确定义(约简模 2)的方式“环绕”。 [继续]... 这是 C 标准中有关有符号整数溢出的引述:如果在计算表达式期间出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围),行为未定义。 @EvanCarslake 稍微偏离了 C 编译器确实使用标准的问题,对于有符号整数,如果他们知道 b &gt; 0,他们可以假设 a + b &gt; a。他们还可以假设如果稍后执行语句a + 5,则当前值低于INT_MAX - 5。因此,即使在没有陷阱的 2 的补码处理器/解释器上,程序的行为也可能不像 ints 是没有陷阱的 2 的补码。【参考方案2】:

问题是加法。 rand() 返回 int0...RAND_MAX。因此,如果您添加其中两个,您将获得RAND_MAX * 2。如果超过INT_MAX,则加法的结果会溢出int 可以容纳的有效范围。有符号值溢出是未定义的行为,可能会导致您的键盘用外语与您交谈。

由于添加两个随机结果在这里没有任何好处,简单的想法就是不要这样做。或者,您可以在加法之前将每个结果转换为unsigned int,如果它可以保持总和。或者使用更大的类型。请注意long 不一定比int 宽,如果int 至少为 64 位,long long 也是如此!

结论:只是避免添加。它不提供更多的“随机性”。如果您需要更多位,您可以连接值sum = a + b * (RAND_MAX + 1),但这也可能需要比int 更大的数据类型。

正如您所说的原因是避免零结果:这无法通过添加两个 rand() 调用的结果来避免,因为两者都可以为零。相反,您可以增加。如果RAND_MAX == INT_MAX,则无法在int 中完成。但是,(unsigned int)rand() + 1 会非常非常有可能。可能(不是绝对),因为它确实需要UINT_MAX &gt; INT_MAX,这在我所知道的所有实现中都是正确的(涵盖了过去 30 年的相当多的嵌入式架构、DSP 以及所有桌面、移动和服务器平台)。

警告:

虽然这里已经撒了cmets,但请注意,添加两个随机值并不会得到均匀分布,而是像掷两个骰子一样的三角形分布:得到12(两个骰子)两者骰子必须显示6。对于11,已经有两种可能的变体:6 + 55 + 6,等等。

所以,从这方面来说,加法也是不好的。

还要注意rand() 生成的结果不是相互独立的,因为它们是由pseudorandom number generator 生成的。另请注意,该标准并未指定计算值的质量或均匀分布。

【讨论】:

@badmad:如果两个调用都返回 0 怎么办? @badmad:我只是想知道UINT_MAX &gt; INT_MAX != false 是否受到标准的保证。 (听起来可能,但不确定是否需要)。如果是这样,您可以只转换一个结果并递增(按该顺序!)。 当你想要一个非均匀分布时,添加多个随机数会有好处:***.com/questions/30492259/… 为了避免0,简单的“当结果为0时,重新滚动”? 添加它们不仅是避免 0 的不好方法,而且还会导致分布不均匀。您会得到类似于掷骰子结果的分布:7 的可能性是 2 或 12 的 6 倍。【参考方案3】:

这是对this answer评论中问题的澄清的回答,

我添加的原因是为了避免在我的代码中将“0”作为随机数。 rand()+rand() 是我想到的快速肮脏的解决方案。

问题是要避免 0。建议的解决方案存在(至少)两个问题。一个是,正如其他答案所表明的那样,rand()+rand() 可以调用未定义的行为。最好的建议是永远不要调用未定义的行为。另一个问题是不能保证rand() 不会连续两次产生 0。

以下拒绝零,避免未定义的行为,并且在绝大多数情况下将比两次调用 rand() 更快:

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) 
// or do rnum = rand(); while (rnum == 0);

【讨论】:

rand() + 1 呢? @askvictor 这可能会溢出(尽管不太可能)。 @gerrit - 取决于 MAX_INT 和 RAND_MAX @gerrit,如果他们相同,我会感到惊讶,但我想这是一个学究的地方:) 如果 RAND_MAX==MAX_INT,rand() + 1 将溢出的概率与 rand() 的值为 0 的概率完全相同,这使得该解决方案完全没有意义。如果您愿意冒险并忽略溢出的可能性,您也可以按原样使用 rand() 并忽略它返回 0 的可能性。【参考方案4】:

基本上rand() 产生介于0RAND_MAX 之间的数字,在你的情况下是2 RAND_MAX &gt; INT_MAX

您可以使用数据类型的最大值取模以防止溢出。这当然会破坏随机数的分布,但rand 只是一种快速获取随机数的方法。

#include <stdio.h>
#include <limits.h>

int main(void)

    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;

【讨论】:

【参考方案5】:

您可以尝试一种相当棘手的方法,确保 2 rand() 的总和返回的值永远不会超过 RAND_MAX 的值。一种可能的方法是 sum = rand()/2 + rand()/2;这将确保对于 RAND_MAX 值为 32767 的 16 位编译器,即使两个 rand 恰好返回 32767,即使是 (32767/2 = 16383) 16383+16383 = 32766,也不会导致负和。

【讨论】:

OP 想要从结果中排除 0。加法也不提供随机值的均匀分布。 @Olaf:不能保证对rand() 的两次连续调用不会都产生零,因此避免零的愿望并不是添加两个值的好理由。另一方面,如果要确保不会发生溢出,那么希望具有非均匀分布将是添加两个随机值的一个很好的理由。【参考方案6】:

我添加的原因是为了避免在我的代码中将“0”作为随机数。 rand()+rand() 是我想到的快速肮脏的解决方案。

一个永远不会产生零结果并且永远不会溢出的简单解决方案(好吧,称之为“Hack”)是:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

这将限制您的最大值,但如果您不关心这一点,那么这对您来说应该可以正常工作。

【讨论】:

旁注:注意有符号变量的右移。它仅对非负值定义明确,对于负值,它是实现定义的。 (幸运的是,rand() 总是返回一个非负值)。但是,我会将优化留给编译器。 @Olaf:一般来说,有符号的除以 2 比移位效率低。除非编译器编写者努力告诉编译器 rand 将是非负数,否则移位将比除以有符号整数 2 更有效。除以 2u 可以工作,但如果 xint 可能会导致有关从无符号到有符号的隐式转换的警告。 @supercat:请仔细阅读我的评论 car3efully。您应该很清楚,任何合理的编译器无论如何都会对/ 2 使用移位(我什至对于-O0 之类的东西也看到了这一点,即没有明确要求优化)。这可能是对 C 代码最简单和最成熟的优化。重点是整个整数范围的标准很好地定义了除法,而不仅仅是非负值。再次:将优化留给编译器,首先编写正确并清除代码。这对初学者来说更为重要。 @Olaf:我测试过的每个编译器在将 rand() 右移 1 或除以 2u 时都比除以 2 时生成更高效的代码,即使在使用 -O3 时也是如此。有人可能会合理地说这种优化不太重要,但是说“将这种优化留给编译器”意味着编译器可能会执行它们。你知道 任何 编译器实际上会吗? @supercat:那么您应该使用更现代的编译器。上次我检查生成的汇编程序时,gcc 刚刚生成了很好的代码。尽管如此,尽管我很高兴有一个 groopie,但我不想被你上次出现的程度所困扰。这些帖子已有多年历史,我的 cmets 完全有效。谢谢。【参考方案7】:

为了避免0,试试这个:

int rnumb = rand()%(INT_MAX-1)+1;

您需要包含limits.h

【讨论】:

这将使获得 1 的概率加倍。如果rand() 产生 0,则它与有条件地加 1 基本相同(但可能更慢)。 是的,你是对的奥拉夫。如果 rand() = 0 或 INT_MAX -1,则 rnumb 将为 1。 更糟糕的是,我想起来了。它实际上会使12 的概率翻倍(都假定为RAND_MAX == INT_MAX)。我确实忘记了- 1 -1 在这里没有任何价值。 rand()%INT_MAX+1; 仍然只会生成 [1...INT_MAX] 范围内的值。【参考方案8】:

谢谢。我添加的原因是避免在我的代码中将“0”作为随机数。 rand()+rand() 是我想到的快速肮脏的解决方案

对我来说这听起来像是一个 XY 问题,为了不从 rand() 得到 0,你调用 rand() 两次,程序运行速度变慢,有新的挫折和获得 0 的可能性还在。

另一种解决方案是使用uniform_int_distribution,它在定义的区间内创建一个随机且均匀分布的数字:

https://wandbox.org/permlink/QKIHG4ghwJf1b7ZN

#include <random>
#include <array>
#include <iostream>
 
int main()

    const int MAX_VALUE=50;
    const int MIN_VALUE=1;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(MIN_VALUE, MAX_VALUE);
    std::array<int,MAX_VALUE-MIN_VALUE> weight=0;

    for(int i=0; i<50000; i++) 
        weight[distrib(gen)-MIN_VALUE]++;
    
    
    for(int i=0;i<(int)weight.size();i++) 
        std::cout << "value: " << MIN_VALUE+i << " times: " << weight[i] << std::endl; 
    

【讨论】:

【参考方案9】:

虽然其他人所说的可能的溢出很可能是负数的原因,即使您使用无符号整数也是如此。真正的问题实际上是使用时间/日期功能作为种子。如果你真正熟悉了这个功能,你就会知道我为什么这么说。它真正做的是给出自给定日期/时间以来的距离(经过的时间)。虽然使用日期/时间功能作为 rand() 的种子是一种非常常见的做法,但它确实不是最佳选择。你应该寻找更好的替代方案,因为关于这个话题有很多理论,我不可能全部研究。您将溢出的可能性添加到这个等式中,这种方法从一开始就注定要失败。

那些发布 rand()+1 的人正在使用最常用的解决方案,以确保他们不会得到负数。但是,这种方法也确实不是最好的方法。

您能做的最好的事情是花额外的时间编写和使用适当的异常处理,并且仅在和/或最终结果为零时添加到 rand() 数。并且,要正确处理负数。 rand() 功能并不完美,因此需要与异常处理结合使用,以确保最终得到所需的结果。

花费额外的时间和精力来调查、研究和正确实现 rand() 功能是非常值得的。只是我的两分钱。祝您工作顺利...

【讨论】:

rand() 没有指定使用什么种子。标准 确实 指定它使用伪随机生成器,而不是与任何时间的关系。它也没有说明发电机的质量。实际问题显然是溢出。注意rand()+1是用来避开0的; rand() 不返回负值。对不起,但你确实错过了这里的重点。这与 PRNG 的质量无关。 ... ... 在 GNU/Linux 下的良好做法是从 /dev/random 播种,然后使用良好的 PRNG(不确定来自 glibc 的 rand() 的质量)或继续使用该设备 -如果没有足够的熵可用,则冒着阻止您的应用程序的风险。试图在应用程序中获取熵很可能是一个漏洞,因为这可能更容易受到攻击。现在谈到硬化 - 不在这里

以上是关于为啥 rand() + rand() 会产生负数?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C++ rand() 似乎只生成相同数量级的数字?

为啥添加 RAND() 会导致 MySQL 过载?

rand()随机函数产生的值的范围?

php的array_rand 函数在多次被执行后为啥会出现一样的结果?

为啥我在使用 rand() 时会得到这种特殊的颜色模式?

c语言中rand()函数怎么用?