srand() - 为什么只需要调用一次?

98

这个问题涉及到关于推荐的srand()初始化方式的评论。第一条评论中提到,srand()在应用程序中只应该被调用一次。为什么会这样呢?


尝试在循环中调用srand和rand函数。 - Foo Bah
11
另请参阅Dilbert的会计之旅 - Jonathan Leffler
1
参见初始化srand()的推荐方法? 使用time(0)作为传递给srand()的值是简单的,因此可能足够,因为srand()通常也是简单的(请参见C标准中的示例实现,它假定RAND_MAX为32,767)。 链接的Q&A中的一个解决方案使用一个函数来混合3个值——来自clock()time(0)getpid()的结果。使用混合函数是一个好主意。也可以使用CRC。 - Jonathan Leffler
7个回答

130
这取决于您要实现什么目标。
随机化是作为一个函数执行的,它具有一个起始值,即“种子”。
因此,对于相同的种子,您将始终获得相同的值序列。
如果您每次需要一个随机值都尝试设置种子,并且种子是相同的数字,则始终会获得相同的“随机”值。
种子通常来自当前时间,即秒数,例如 "time(NULL)",因此,如果您在获取随机数之前始终设置种子并多次调用 srand/rand 组合,则只要在“同一秒钟”内调用,则将获得相同的数字。
为了避免这个问题,srand 仅在应用程序中设置一次,因为很难在同一秒钟内初始化两个应用程序实例,因此每个实例将具有不同的随机数序列。
但是,您可能会在一秒钟内多次运行应用程序(特别是如果它是短暂的,或者是命令行工具之类的东西),那么您将不得不采用其他选择种子的方式(除非您认为不同应用程序实例中的相同序列可以接受)。但正如我所说,这取决于您的应用程序使用情况。
此外,您可能希望尝试将精度提高到微秒级(最小化相同种子的机会),需要("sys/time.h"):
struct timeval t1;
gettimeofday(&t1, NULL);
srand(t1.tv_usec * t1.tv_sec);

4
附注:在 POSIX 2008 中,gettimeofday 已经过时。取而代之的是 clock_gettime,可能需要链接 -lrt。然而,在许多平台上它可能还不可用。在 Linux 上,这是可以的。在 Mac 上,我认为它尚不可用。在 Windows 上,它可能永远不会可用。 - Shahbaz
1
t1.tv_usec是一个长整型,srand函数的输入参数是无符号整型。(我刚遇到了一个问题,这点很重要。) - Jiminion
做到了。通过增加精度,我摆脱了重复项。非常感谢您。我有一个交付期限,这拯救了我的后路。 - Beezer

26

生成的随机数其实是伪随机数。首先设置一个种子,每次调用 rand 函数都会得到一个随机数,并修改内部状态,下一次调用 rand 时就会使用这个新状态来获取另一个随机数。因为使用某个公式生成这些“随机数”,所以在每次调用 rand 后设置相同的种子值会返回相同的数字。例如 srand(1234); rand(); 将返回相同的值。仅用种子值初始化初始状态一次将生成足够多的随机数,因此当你不使用 srand 设置内部状态时,这些数字更有可能是随机的。

通常我们使用 time(NULL) 返回的秒数值来初始化种子值。假设 srand(time(NULL)); 在一个循环中,那么循环可以在一秒内迭代多次,因此循环中的每次调用 rand 都将返回相同的“随机数”,这是不希望发生的。在程序启动时初始化一次将只设置一次种子,每次调用 rand 都会生成一个新的数字并修改内部状态,因此下一次调用 rand 将返回足够随机的数字。

例如来自 http://linux.die.net/man/3/rand 的这段代码:

static unsigned long next = 1;
/* RAND_MAX assumed to be 32767 */
int myrand(void) {
    next = next * 1103515245 + 12345;
    return((unsigned)(next/65536) % 32768);
}
void mysrand(unsigned seed) {
    next = seed;
}

内部状态 next 被声明为全局变量。每个 myrand 调用都会修改内部状态并更新它,然后返回一个随机数。因此,每次调用 myrand 都将有不同的 next 值,导致该方法每次调用都返回不同的数字。

看一下 mysrand 的实现;它只是将你传递给 next 的种子值设置为相同的。因此,如果你在调用 rand 之前每次设置相同的 next 值,它将返回相同的随机值,因为应用于它的公式是相同的,这是不希望发生的,因为该函数是用来生成随机数的。

但是根据你的需求,你可以将种子设置为某个固定值,以便每次运行时生成相同的“随机序列”,例如用于基准测试或其他情况。


你是不是指的是mysrand()函数的参数(unsigned long seed)? - Jiminion
@Jiminion 这是来自 man srand 的代码片段。范围从0到32767(假设RAND_MAX),远小于long的范围。状态变量next被设置为long,因为内部乘法和加法将超出unsigned int的范围。之后,结果在上述指定的范围内进行缩放或修改。虽然您可以使种子成为long - phoxis
1
请注意,C标准中也包含了所示代码片段。 - Jonathan Leffler

19
简短回答:调用srand()不像为随机数生成器“掷骰子”,也不像洗牌。如果说有什么类似的话,它更像是只切一副牌。

可以这样想,rand()从一副大牌中发牌,每次调用时,它只是从牌堆顶部选择下一张牌,给您该值,并将该牌返回到牌堆底部。(是的,这意味着“随机”序列会在一段时间后重复。不过这是非常大的一副牌:通常为4,294,967,296张。) 此外,每次程序运行时,都会从游戏商店购买全新的牌组,而且每个全新的牌组总是具有相同的序列。 因此,除非您进行特殊操作,否则每次程序运行时,都将从rand()获得完全相同的“随机”数字。
现在,你可能会说,“好的,那么我该如何洗牌?”至少就randsrand而言,答案是没有办法洗牌。
那么srand是做什么的呢?基于我一直在构建的类比,调用srand(n)基本上就像是在说:“从顶部切掉n张牌”。但是等等,还有一件事:实际上是开始使用另一个全新的牌组,并将其从顶部切掉n
因此,如果您每次都使用相同的n调用srand(n)rand()srand(n)rand()...,您不仅会得到一个不太随机的序列,而且实际上每次都会从rand()中得到相同的数字。(可能不是您传递给srand的相同数字,但是从rand中反复得到相同的数字。)
所以你能做的最好的事情就是只切牌一次,也就是在程序开始时调用srand()一次,并使用一个相当随机的n,这样每次运行程序时都会从大牌堆的不同随机位置开始。对于rand(),这确实是你能做到的最好的。

[附言:是的,我知道,在现实生活中,当你购买全新的扑克牌时,它通常是有序的,而不是随机的顺序。为了让这里的比喻起作用,我想象每个你从游戏商店购买的牌组都是看似随机的顺序,但确切相同的看似随机的顺序。有点像桥牌比赛中使用的完全相同洗牌的牌组。]


补充说明:如果一个伪随机数生成算法和一个给定的种子值相同,那么你总是会得到相同的序列。可以参考this question进行非常可爱的演示(该问题与C语言无关,但仍然适用)。


8
原因在于 srand() 设置了随机生成器的初始状态,如果您在其间不干扰该状态,生成器产生的所有值都足够“随机”。例如,您可以这样做:
int getRandomValue()
{
    srand(time(0));
    return rand();
}

如果您多次调用该函数,使得time()在相邻的调用中返回相同的值,则生成的值也将相同——这是设计如此的。


3

srand函数用于初始化伪随机数生成器。如果你调用它超过一次,就会重新初始化该随机数生成器。而且,如果你使用相同的参数再次调用它,它将重新启动相同的序列。

为了证明这一点,如果你执行以下简单操作,你将看到相同的数字被打印100次:

#include <stdlib.h>
#include <stdio.h>
int main() {
    for(int i = 0; i != 100; ++i) {
        srand(0);
        printf("%d\n", rand());
    }
}

3
一种更简单的使用srand()生成在同一秒运行的应用程序实例不同种子的解决方案如下所示。
srand(time(NULL)-getpid());

此方法使您的种子非常接近随机,因为无法猜测线程启动的时间并且 pid 也会不同。


0
  1. 似乎每次运行 rand(),它都会为下一个 rand() 设置一个新的种子。

  2. 如果 srand() 运行多次,问题在于如果两个运行发生在同一秒钟内(time(NULL) 不变),那么下一个 rand() 将与前一个 srand() 后的 rand() 相同。


重点是使用相同的种子多次使用srand()进行初始化将导致rand()返回相同的值。 - King Thrushbeard

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