使用 rand() 获取数字但该数字不能是上次生成的数字

Using rand() to get a number but that number can't be the number that was last generated

我想用std::rand()生成一个介于0amountOfNumbers之间的号码,但是生成的号码不能与上次生成的号码相同。

我写了这个函数:

void reroll() {
  int newRand;

  while (true) {
    newRand = std::rand() % (amountOfNumbers);
    if (newRand == lastNumber) {
      continue;
    }
    lastNumber = newRand;
    break;
  }

  // do something with newRand
  // ...
}

amountOfNumbers 只是一个定义上限的 int (> 1)(例如 5,因此可能的数字范围是 0 到 4)。 lastNumber 最初是 -1 存储最后生成的数字。

我想知道是否有更好的写法。

该功能到目前为止似乎可以正常工作,但我不确定我的代码是否完美无缺...看起来有点糟糕。那个while (true)让我有点不安。

代码有效,但我会像这样构造它

int reroll(int n, int last) {
  while(true) {
    int v = std::rand() % n;
    if(v!=last)
      return v;
  }
}

void reroll() {
   ...
   int v = reroll(n, last);
   ...
}

您还可以通过生成较小范围内的值(少 1)并在 last 左右调整来完全避免 while 循环的需要。

int reroll(int n, int last) {
  if(last==-1) 
    return std::rand() % n;
  int v = std::rand() % (n-1);
  if (v<last) return v
   return v+1;
}

我有几点建议。

由于您从未声明 lastNumber 和 amountOfNumbers,我假设它们是全局的。最好将它们作为变量传递给函数。此外,您应该 return 函数中的新数字,而不是将其设置为无效。

下面的代码将计算新的滚动。我们不会重新滚动直到有新的滚动,我们只会随机取一组数字,但少一个。如果数字大于或等于,我们将加回一个,从而避免最后一个数字。然后函数 returns newRand。这样做将避免(尽管很低)无限循环的风险,并且它总是 运行 在常数时间内。

int reroll(int lastNumber, int amountOfNumbers) 
{
  int newRand;

  newRand = std::rand() % (amountOfNumbers - 1);
  if (newRand >= lastNumber) {
    newRand++;
  }

  return newRand;
}

while true 循环绝对不是好的做法,我建议做这样的事情。但是您应该按照与上面迈克尔的回答相同的结构来制作它,如下所示:

void reroll(int lastNumber, int amountOfNumbers) {
  int newRand = std::rand() % (amountOfNumbers);

  while (newRand == lastNumber) {
    newRand = std::rand() % (amountOfNumbers);
  }

  lastNumber = newRand;
  return newRand;
}

根据您对 amountOfNumbers 的取值,您使用的取模运算可能无法保证均匀分布(这只是您问题的一小部分)。

  • 首先,如果 amountOfNumbers 大于 RAND_MAX,将有一些数字是您永远看不到的。
  • 接下来,考虑您是否使用它为骰子生成 0 到 6 ([0,1,2,3,4,5]) 之间的值,而 RAND_MAX 是 7。您会看到值 01 是其他人的两倍!实际上,RAND_MAX 必须至少为 32767(不管那是什么意思,根据标准这并不多)...但是 that 不能被整除6,所以当然会有一些值有轻微的 bias.

可以使用模数来缩小该范围,但您可能希望在第二个问题中放弃偏差。不幸的是,由于 rand 的常见实现,将范围扩展到 max 之外会引入更多偏差。

unsigned int rand_range(unsigned int max) {
    double v = max > RAND_MAX ? max : RAND_MAX;
    v /= RAND_MAX;

    int n;
    do {
        n = rand();
    } while (RAND_MAX - n <= RAND_MAX % (unsigned int)(max / v));

    return (unsigned int)(v * n) % max;
}

这个仍然并不能保证不会有任何重复值,但至少任何导致重复值的偏差都会大大减少。我们现在可以使用与(当前)接受的答案类似的方法来删除任何重复的值,并且偏差最小:

unsigned int non_repetitive_rand_range(unsigned int max) {
    if (max <= 1) {
        return 0; // Can't avoid repetitive values here!
    }

    static unsigned int previous;
    unsigned int current;
    do {
        current = rand_range(max);
    } while (current != previous);

    previous = current;
    return current;
}

在技术说明上,这也不一定保证解决方案会出现问题。标准中的这句话解释了原因:

There are no guarantees as to the quality of the random sequence produced and some implementations are known to produce sequences with distressingly non-random low-order bits. Applications with particular requirements should use a generator that is known to be sufficient for their needs.

因此,一些愚蠢、晦涩的实现可能会像这样实现 rand

int rand(void) {
    return 0;
}

对于这样的实现,无法避免:rand 的这种实现会导致此答案(以及所有其他当前答案)中的代码进入 无限循环。您唯一的希望是重新实现 randsrand。这是标准中给出的示例实现,如果您必须这样做:

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

您可能希望将它们分别重命名为 my_randmy_srand,并使用 #define rand my_rand#define srand my_srand 或使用 find/replace 操作。