如何从一定范围内生成随机整数


108

这是先前发布的问题的后续内容:

如何在C中生成一个随机数?

我希望能够在特定范围内生成一个随机数,例如1到6,以模拟骰子的侧面。

我将如何去做呢?


3
如果您查看所指问题的第二个答案,那么您就会找到答案。rand()%6.
Mats Fredriksson 2010年

2
我不了解它是如何工作的,所以为了清楚起见,我决定提出一个单独的问题。
杰米·基林

2
随机的想法:如果您对程序员的随机截面进行调查,您会发现随机数的程序员正在随机考虑随机生成数字的方法。考虑到宇宙受精确和可预测的法律支配,我们尝试随机产生事物是否有趣?像这样的问题总是倾向于带出1万多张海报。
阿姆斯特朗最2010年

2
@Mats rand()%6可以返回0。
new123456 2011年

您可以将stackoverflow.com/a/6852396/419标记为接受的答案,而不是链接到该答案的答案:)谢谢。
凯夫2012年

Answers:


173

到目前为止,所有答案在数学上都是错误的。除非将返回间隔的长度除以2(即2的幂),否则返回rand() % N不会均匀地给出该范围内的数字。此外,人们不知道的模数是否独立:它们可能会走,这是均匀的,但不是很随机。似乎合理的唯一假设是得出泊松分布:相同大小的任何两个不重叠的子间隔都可能相同且独立。对于一组有限的值,这意味着分布均匀,并且还确保的值分散良好。[0, N)Nrand()rand()0, 1, 2, ...rand()rand()

这意味着更改范围的唯一正确方法rand()是将其分成多个框。例如,如果RAND_MAX == 11并且您想要的范围1..6,则应将其分配{0,1}给1,{2,3}到2,依此类推。这些是不相交的,大小相等的间隔,因此均匀且独立地分布。

使用浮点除法的建议在数学上是合理的,但原则上存在四舍五入的问题。也许double是足够高的精度才能使其工作;也许不是。我不知道,我也不想弄清楚;无论如何,答案取决于系统。

正确的方法是使用整数算术。也就是说,您需要以下内容:

#include <stdlib.h> // For random(), RAND_MAX

// Assumes 0 <= max <= RAND_MAX
// Returns in the closed interval [0, max]
long random_at_most(long max) {
  unsigned long
    // max <= RAND_MAX < ULONG_MAX, so this is okay.
    num_bins = (unsigned long) max + 1,
    num_rand = (unsigned long) RAND_MAX + 1,
    bin_size = num_rand / num_bins,
    defect   = num_rand % num_bins;

  long x;
  do {
   x = random();
  }
  // This is carefully written not to overflow
  while (num_rand - defect <= (unsigned long)x);

  // Truncated division is intentional
  return x/bin_size;
}

循环是获得完美均匀分布所必需的。例如,如果给您从0到2的随机数,并且只希望从0到1的数字,那么您就一直拉直到没有得到2为止;不难检查这是否等于0或1。在nos给出答案的链接中也描述了此方法,尽管编码方式不同。我使用random()而不是rand()因为它具有更好的分布(如的手册页所指出rand())。

如果要获取默认范围之外的随机值[0, RAND_MAX],则必须做一些棘手的事情。也许最有利的是定义一个函数random_extended(),拉n位(使用random_at_most())和回报[0, 2**n),然后应用random_at_most()random_extended()到位的random()(而2**n - 1代替RAND_MAX)拉一个随机值小于2**n,假设你有一个数值类型,它可以保持这样的一个值。最后,当然,您可以[min, max]使用来获取值min + random_at_most(max - min),包括负值。


1
@Adam Rosenfield,@ Ryan Reich:在亚当回答过的一个相关问题中:stackoverflow.com/questions/137783/…最受支持的答案:那么“模数”的使用是不正确的,不是吗?要从1..21生成1..7,应使用Ryan描述的过程。如果我错了,请纠正我。
2013年

1
在进一步审查时,这里的另一个问题是,当时max - min > RAND_MAX,它将不起作用,这比我上面提到的问题更严重(例如,VC ++ RAND_MAX只有32767)。
2013年

2
while循环可以提高可读性。与其在条件中执行赋值,不如说您想要一个do {} while()
theJPster

4
嘿,Comet OS书中引用了这个答案;)我第一次在教学书中看到它
vpuente 17-10-10

3
OSTEP书中也引用了它:) pages.cs.wisc.edu/~remzi/OSTEP(第9章,第4页)
rafascar

33

在@Ryan Reich的回答之后,我想我会提供清理后的版本。在给定第二个边界检查的情况下,不需要第一个边界检查,并且我已经使其迭代而不是递归。它返回[min,max]范围内的值,其中max >= min1+max-min < RAND_MAX

unsigned int rand_interval(unsigned int min, unsigned int max)
{
    int r;
    const unsigned int range = 1 + max - min;
    const unsigned int buckets = RAND_MAX / range;
    const unsigned int limit = buckets * range;

    /* Create equal size buckets all in a row, then fire randomly towards
     * the buckets until you land in one of them. All buckets are equally
     * likely. If you land off the end of the line of buckets, try again. */
    do
    {
        r = rand();
    } while (r >= limit);

    return min + (r / buckets);
}

28
注意,如果范围> = RAND_MAX,它将陷入无限循环。问我怎么知道:/
theJPster

24
你怎么知道的!?
很棒的福克斯先生

1
请注意,您正在将一个int与一个无符号int(r> =限制)进行比较。由于< 和<= limit,可以通过将int(以及可选地bucket)设置为int 来轻松解决此问题。编辑:我已经提交和编辑提案。RAND_MAX / rangeINT_MAXbuckets * rangeRAND_MAX
rrrrrrrrrrrrrrrr

@Ryan Reich的解决方案仍然为我提供了更好的(较少偏见的)分配
Vladimir

20

如果您知道某个范围的最大值和最小值,并且想要生成介于该范围之间的数字,则可以使用以下公式:

r = (rand() % (max + 1 - min)) + min

9
正如瑞安(Ryan)的回答所指出的那样,这会产生偏差。
David Wolever 2014年

6
偏差的结果,可能有int溢出max+1-min
chux-恢复莫妮卡2014年

1
仅适用于整数min和max。如果最小值和最大值是浮动的,则无法执行%运算
Taioli Francesco

17
unsigned int
randr(unsigned int min, unsigned int max)
{
       double scaled = (double)rand()/RAND_MAX;

       return (max - min +1)*scaled + min;
}

有关其他选项,请参见此处


2
@ S.Lott-并非如此。每种情况以不同的方式分配几率较高的案例。双重数学给人的印象是那里有更高的精度,但是您可以轻松使用(((max-min+1)*rand())/RAND_MAX)+min并获得完全相同的分布(假设RAND_MAX相对于int足够小而不会溢出)。
2010年

4
这是稍微危险:有可能为这(很少)的回报max + 1,如果有一个rand() == RAND_MAXrand()非常接近RAND_MAX和浮点错误将最终的结果过去max + 1。为了安全起见,您应在返回结果之前检查结果是否在范围内。
马克·迪金森

1
@Christoph:我同意RAND_MAX + 1.0。不过,我仍然不确定这是否足以防止max + 1退货:特别是,+ min结尾处的回合可能最终产生max + 1大量rand()。完全放弃这种方法而使用整数算法。
马克·迪金森

3
如果按Christoph的建议RAND_MAX替换RAND_MAX+1.0为,那么我相信这是安全的,只要+ min使用整数算术即可完成: return (unsigned int)((max - min + 1) * scaled) + min。(不明显的)原因是,假设IEEE 754算术和二分之一到四舍五入,(也max - min + 1可以精确地表示为双精度,但是在典型机器上是正确的),x * scaled < x对于任何积极的双重x和任何双重scaled令人满意0.0 <= scaled && scaled < 1.0
马克·迪金森

1
失败的randr(0, UINT_MAX):总是产生0
恢复莫妮卡- chux

12

您不只是这样做:

srand(time(NULL));
int r = ( rand() % 6 ) + 1;

%是模运算符。本质上,它将只是除以6并返回余数...从0-5


1
它将得出1-6的结果。这就是+ 1的含义。
阿姆斯特朗(Armstrongest)2010年

4
Simon,请告诉我一个正在使用的libc,其中rand()包括生成器状态的低位(如果使用LCG)。到目前为止,我还没有看到一个—所有这些(是的,包括MSVC的RAND_MAX仅为32767)都删除了低位。出于其他原因,不建议使用模数,即使用模数会偏向较小的数字。
乔伊

@Johannes:所以可以肯定地说老虎机不使用模数吗?
阿姆斯特朗(Armstrongest)2010年

如何排除0?看来,如果我以30的循环运行它,则可能是第二次或第三次运行,其中大约有一半是0。这是fl幸吗?
Jamie Keeling 2010年

@Johannes:也许现在不是什么大问题,但传统上不建议使用低位。c-faq.com/lib/randrange.html
jamesdlin

9

对于那些了解偏差问题但无法忍受基于拒绝的方法的不可预测的运行时间的人,此系列在[0, n-1]区间中产生的偏差逐渐减小:

r = n / 2;
r = (rand() * n + r) / (RAND_MAX + 1);
r = (rand() * n + r) / (RAND_MAX + 1);
r = (rand() * n + r) / (RAND_MAX + 1);
...

它通过合成高精度的定点随机数i * log_2(RAND_MAX + 1)位(其中i为迭代数)并通过n

当位数比 n,偏差将变得非常小。

这不要紧,如果RAND_MAX + 1是低于n(在这个问题),或者如果它不是2的幂,但如果必须小心,以避免整数溢出RAND_MAX * n大。


2
RAND_MAX通常INT_MAXRAND_MAX + 1-> UB(例如INT_MIN)
恢复莫妮卡

@chux是我的意思,即“如果整数RAND_MAX * n太大,则必须小心避免整数溢出”。您需要安排使用适合您需求的类型。
sh1 2014年

@chux“ RAND_MAX通常INT_MAX是”是,但是仅在16位系统上!任何合理的现代建筑技术都将以INT_MAX2 ^ 32/2和2 ^ 16/2 放置RAND_MAX。这是不正确的假设吗?

2
@cat今天试射2个32位int编译器,我发现RAND_MAX == 32767一个和RAND_MAX == 2147483647另一个。我的整体经验(数十年) RAND_MAX == INT_MAX更多。如此不同意,以至于合理的现代32位体系结构肯定会有RAND_MAXat 2^16 / 2。由于C规范允许使用32767 <= RAND_MAX <= INT_MAX,所以无论如何我还是要编写代码,而不是倾向于编写代码。
chux-恢复莫妮卡

3
仍然涵盖“必须小心避免整数溢出”。
sh1

4

为了避免模偏差(在其他答案中建议),您可以始终使用:

arc4random_uniform(MAX-MIN)+MIN

其中“ MAX”是上限,“ MIN”是下限。例如,对于10到20之间的数字:

arc4random_uniform(20-10)+10

arc4random_uniform(10)+10

简单的解决方案,比使用“ rand()%N”更好。


1
哇,这比其他答案要好十亿倍。值得注意的是您需要#include <bsd/stdlib.h>先。另外,有什么想法如何在没有MinGW或CygWin的Windows上获得此功能?

1
不,它本身并不比其他答案更好,因为其他答案更为通用。在这里,您只能使用arc4random,其他答案则可以让您选择其他随机源,使用不同的数字类型,...,最后但同样重要的一点是,它们可以帮助某人理解问题。不要忘了,这个问题也是谁可能有一些特殊的要求或arc4random用不上......然而,其他人有趣,如果你有机会获得它,并希望得到一个快速的解决方案,这的确是一个很好的答案😊
K.Biermann

4

这比Ryan Reich的解决方案稍微简单一些:

/// Begin and end are *inclusive*; => [begin, end]
uint32_t getRandInterval(uint32_t begin, uint32_t end) {
    uint32_t range = (end - begin) + 1;
    uint32_t limit = ((uint64_t)RAND_MAX + 1) - (((uint64_t)RAND_MAX + 1) % range);

    /* Imagine range-sized buckets all in a row, then fire randomly towards
     * the buckets until you land in one of them. All buckets are equally
     * likely. If you land off the end of the line of buckets, try again. */
    uint32_t randVal = rand();
    while (randVal >= limit) randVal = rand();

    /// Return the position you hit in the bucket + begin as random number
    return (randVal % range) + begin;
}

Example (RAND_MAX := 16, begin := 2, end := 7)
    => range := 6  (1 + end - begin)
    => limit := 12 (RAND_MAX + 1) - ((RAND_MAX + 1) % range)

The limit is always a multiple of the range,
so we can split it into range-sized buckets:
    Possible-rand-output: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
    Buckets:             [0, 1, 2, 3, 4, 5][0, 1, 2, 3, 4, 5][X, X, X, X, X]
    Buckets + begin:     [2, 3, 4, 5, 6, 7][2, 3, 4, 5, 6, 7][X, X, X, X, X]

1st call to rand() => 13
     13 is not in the bucket-range anymore (>= limit), while-condition is true
         retry...
2nd call to rand() => 7
     7 is in the bucket-range (< limit), while-condition is false
         Get the corresponding bucket-value 1 (randVal % range) and add begin
    => 3

1
RAND_MAX + 1可以很容易地溢出int加法。那样的话,(RAND_MAX + 1) % range会产生可疑的结果。考虑(RAND_MAX + (uint32_t)1)
chux-恢复莫妮卡

2

尽管Ryan是正确的,但根据有关随机性来源的已知信息,解决方案可以简单得多。重述该问题:

  • 有一个随机性源,输出范围内的整数 [0, MAX)均匀且分布均匀的。
  • 我们的目标是在范围产生均匀分布的随机整数[rmin, rmax],其中0 <= rmin < rmax < MAX

以我的经验,如果箱(或“盒子”)的数量显着小于原始数量的范围,并且原始来源在密码学上很强大-则不需要经历所有的rigamarole,简单的模除法就可以足够(例如output = rnd.next() % (rmax+1),如果rmin == 0),并产生均匀分布的“足够”的随机数,而不会降低速度。关键因素是随机性来源(例如,孩子们,不要在家尝试rand())。

这是它在实践中如何工作的示例/证明。我想生成1到22之间的随机数,并具有产生随机字节(基于Intel RDRAND)的强密码源。结果是:

Rnd distribution test (22 boxes, numbers of entries in each box):     
 1: 409443    4.55%
 2: 408736    4.54%
 3: 408557    4.54%
 4: 409125    4.55%
 5: 408812    4.54%
 6: 409418    4.55%
 7: 408365    4.54%
 8: 407992    4.53%
 9: 409262    4.55%
10: 408112    4.53%
11: 409995    4.56%
12: 409810    4.55%
13: 409638    4.55%
14: 408905    4.54%
15: 408484    4.54%
16: 408211    4.54%
17: 409773    4.55%
18: 409597    4.55%
19: 409727    4.55%
20: 409062    4.55%
21: 409634    4.55%
22: 409342    4.55%   
total: 100.00%

就我的目的而言,这几乎是统一的(掷骰子,为第二次世界大战的密码机生成加密强度高的密码本,例如 http://users.telenet.be/d.rijmenants/en/kl-7sim.htm)等))。输出没有显示任何明显的偏差。

这是加密强(真实)随机数生成器的来源: 英特尔数字随机数生成器 和示例代码,可生成64位(无符号)随机数。

int rdrand64_step(unsigned long long int *therand)
{
  unsigned long long int foo;
  int cf_error_status;

  asm("rdrand %%rax; \
        mov $1,%%edx; \
        cmovae %%rax,%%rdx; \
        mov %%edx,%1; \
        mov %%rax, %0;":"=r"(foo),"=r"(cf_error_status)::"%rax","%rdx");
        *therand = foo;
  return cf_error_status;
}

我在Mac OS X上使用clang-6.0.1(直接)和gcc-4.8.3使用“ -Wa,q”标志对其进行了编译(因为GAS不支持这些新指令)。


gcc randu.c -o randu -Wa,q(Ubuntu 16上的GCC 5.3.1)或clang randu.c -o randu(Clang 3.8.0)兼容,但在运行时使用来转储核心Illegal instruction (core dumped)。有任何想法吗?

首先,我不知道您的CPU是否真正支持RDRAND指令。您的操作系统是相当新的,但CPU可能不是。第二(但这不太可能)-我不知道Ubuntu包括哪种汇编程序(而Ubuntu往往相当落后,更新软件包)。请访问我提到的英特尔站点,以测试您的CPU是否支持RDRAND。
鼠标

您确实有好点。我仍然无法得到的是什么错rand()。我尝试了一些测试并发布了此问题,但仍找不到确切答案。
myradio

1

如前所述,模数不足,因为它会使分布偏斜。这是我的代码,它掩盖了位并使用它们来确保分布不偏斜。

static uint32_t randomInRange(uint32_t a,uint32_t b) {
    uint32_t v;
    uint32_t range;
    uint32_t upper;
    uint32_t lower;
    uint32_t mask;

    if(a == b) {
        return a;
    }

    if(a > b) {
        upper = a;
        lower = b;
    } else {
        upper = b;
        lower = a; 
    }

    range = upper - lower;

    mask = 0;
    //XXX calculate range with log and mask? nah, too lazy :).
    while(1) {
        if(mask >= range) {
            break;
        }
        mask = (mask << 1) | 1;
    }


    while(1) {
        v = rand() & mask;
        if(v <= range) {
            return lower + v;
        }
    }

}

以下简单代码可让您查看分布:

int main() {

    unsigned long long int i;


    unsigned int n = 10;
    unsigned int numbers[n];


    for (i = 0; i < n; i++) {
        numbers[i] = 0;
    }

    for (i = 0 ; i < 10000000 ; i++){
        uint32_t rand = random_in_range(0,n - 1);
        if(rand >= n){
            printf("bug: rand out of range %u\n",(unsigned int)rand);
            return 1;
        }
        numbers[rand] += 1;
    }

    for(i = 0; i < n; i++) {
        printf("%u: %u\n",i,numbers[i]);
    }

}

当您拒绝rand()的数字时,效率将变得非常低下。当范围的大小可以写为2 ^ k + 1时,这将特别无效。然后,从慢rand()调用进行的所有尝试中,几乎有一半将被条件拒绝。计算RAND_MAX模数范围可能会更好。像:v = rand(); if (v > RAND_MAX - (RAND_MAX % range) -> reject and try again; else return v % range;我知道模运算比屏蔽运算要慢得多,但是我仍然认为...应该进行测试。
奥伊斯坦Schønning -约翰森

rand()返回int范围内的[0..RAND_MAX]。该范围很容易成为该范围的子范围,uint32_t因此randomInRange(0, ,b)永远不会生成该范围内的值(INT_MAX...b]
chux-恢复莫妮卡

0

将返回范围为[0,1]的浮点数:

#define rand01() (((double)random())/((double)(RAND_MAX)))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.