为啥 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()
被定义为返回一个介于0
和RAND_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 > 0
,他们可以假设 a + b > a
。他们还可以假设如果稍后执行语句a + 5
,则当前值低于INT_MAX - 5
。因此,即使在没有陷阱的 2 的补码处理器/解释器上,程序的行为也可能不像 int
s 是没有陷阱的 2 的补码。【参考方案2】:
问题是加法。 rand()
返回 int
值 0...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 > INT_MAX
,这在我所知道的所有实现中都是正确的(涵盖了过去 30 年的相当多的嵌入式架构、DSP 以及所有桌面、移动和服务器平台)。
警告:
虽然这里已经撒了cmets,但请注意,添加两个随机值并不会得到均匀分布,而是像掷两个骰子一样的三角形分布:得到12
(两个骰子)两者骰子必须显示6
。对于11
,已经有两种可能的变体:6 + 5
或5 + 6
,等等。
所以,从这方面来说,加法也是不好的。
还要注意rand()
生成的结果不是相互独立的,因为它们是由pseudorandom number generator 生成的。另请注意,该标准并未指定计算值的质量或均匀分布。
【讨论】:
@badmad:如果两个调用都返回 0 怎么办? @badmad:我只是想知道UINT_MAX > 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()
产生介于0
和RAND_MAX
之间的数字,在你的情况下是2 RAND_MAX > 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
可以工作,但如果 x
是int
可能会导致有关从无符号到有符号的隐式转换的警告。
@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。
更糟糕的是,我想起来了。它实际上会使1
和2
的概率翻倍(都假定为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() 会产生负数?的主要内容,如果未能解决你的问题,请参考以下文章