本篇介紹 C/C++ 中使用 rand 函數產生亂數的方法,並且提供各種常用的範例程式碼。

在撰寫 C/C++ 程式時,如果需要產生一些簡單的亂數,最方便的作法就是使用 rand 這個亂數產生函數,以下介紹這個函數的相關用法與範例。

rand 只能提供基本的亂數,如果您需要更進階的功能或是品質比較好的亂數,建議改用 C++ 的 <random> 函式庫

基本亂數產生方法

C 語言中若要產生亂數,可以使用 stdlib.h 中的 rand 函數,而在呼叫 rand 函數之前,要先使用 srand 函數設定初始的亂數種子:

#include <stdio.h>
#include <stdlib.h> /* 亂數相關函數 */
#include <time.h>   /* 時間相關函數 */
int main(){
  /* 設定亂數種子 */
  srand( time(NULL) );

  /* 產生亂數 */
  int x = rand();

  printf("x = %d\n", x);
  return 0;
}

執行後的輸出為:

x = 159136674

rand 所產生的亂數是一個整數,其值介於 0RAND_MAX 之間(最小是 0,最大則為 RAND_MAX),若想要看 RAND_MAX 的實際數值,可以用 printf 將其輸出後查看。

printf("RAND_MAX = %d\n", RAND_MAX);

在 C++ 中的亂數產生方式也跟 C 幾乎相同,只是標頭檔換一下而已:

#include <iostream>
#include <cstdlib> /* 亂數相關函數 */
#include <ctime>   /* 時間相關函數 */
int main(){
  /* 固定亂數種子 */
  srand( time(NULL) );

  /* 產生亂數 */
  int x = rand();

  std::cout << "x = " << x << std::endl;
  return 0;
}

接下來的範例我就以 C 語言的寫法為準,需要 C++ 的人就自己將標頭檔改一下。

固定亂數種子

由於電腦實際上並沒有辦法自己產生「真正的亂數」,只能透過複雜的數學演算法模擬出類似亂數的數值資料,而在模擬亂數時,需要設定一個亂數種子,電腦會根據這個亂數種子來計算出一連串的亂數,相同的亂數種子就會產生相同的亂數序列,所以如果要讓產生的亂數每次都不同,就要設定不同的亂數種子。

在上面的範例中,我們使用時間來當作亂數種子,所以每次產生的亂數都不同,如果是用於數值模擬的程式,通常都會需要讓模擬的結果具有可重復性(repeatability),方便除錯與驗證,這種狀況就可以將亂數種子固定不變,這樣就可以確保每次的模擬結果完全相同:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
  /* 固定亂數種子 */
  srand(5);

  int x = rand();
  printf("x = %d\n", x);
  return 0;
}
x = 84035

亂數種子是使用一個整數來做設定的,若要固定亂數種子的話,其數值要設定為多少其實不重要,只要每次執行程式時都使用相同的亂數種子即可。

最常用到固定亂數種子的時機可能就是除錯或是驗證演算法是否有錯誤的時候,如果沒有固定亂數種子的話,每次跑出來的結果都不同,會讓除錯的難度提高。

[0, 1) 浮點數亂數

若要產生 01 之間的浮點數亂數,可以這樣寫:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
  srand( time(NULL) );

  /* 產生 [0, 1) 的浮點數亂數 */
  double x = (double) rand() / (RAND_MAX + 1.0);

  printf("x = %f\n", x);
  return 0;
}

上面的程式中我們將 rand 函數所產生整數除以 RAND_MAX + 1.0,就可以得到 [0, 1) 這個範圍的浮點數亂數(也就是 0 <= x < 1)。

特定範圍浮點數亂數

若要產生特定範圍的浮點數亂數,可以這樣寫:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
  srand( time(NULL) );

  /* 指定亂數範圍 */
  double min = 3.6;
  double max = 7.8;

  /* 產生 [min , max) 的浮點數亂數 */
  double x = (max - min) * rand() / (RAND_MAX + 1.0) + min;

  printf("x = %f\n", x);
  return 0;
}

這樣即可產生 [min , max) 之間的浮點數亂數。

特定範圍整數亂數

若想要產生特定範圍的整數亂數,可以這樣寫:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
  srand( time(NULL) );

  /* 指定亂數範圍 */
  int min = 4;
  int max = 10;

  /* 產生 [min , max] 的整數亂數 */
  int x = rand() % (max - min + 1) + min;

  printf("x = %d\n", x);
  return 0;
}

這樣會將 rand 產生出來的整數轉換為 [min , max] 的整數亂數(也就是 min <= x <= max)。

上面這種使用餘數運算(%)的方式只是比較方便的寫法,事實上使用餘數運算所產生的整數亂數在理論上不是標準的均勻分布,我們以一個簡單的例子來解釋,假設 RAND_MAX 的值為 10,而我們要產生介於 35 之間的整數亂數(亦即 min = 3max = 5),以下是所有的可能性對照表:

轉換後的整數亂數rand 函數產生的亂數出現機率
303694/11
4147104/11
52583/11

rand 函數所產生的每一個整數其出現的機率是均等的,但是經過於數運算的轉換之後,因為 RAND_MAX 通常不會被整除,所以轉換之後的整數亂數出現機率就存在有細微的偏差,以這個例子來說,345 三個數字出現的機率比是 4:4:3

另外有些人會先產生固定範圍的浮點數亂數,再將浮點數轉型為整數,例如產生 [3, 6) 的浮點數亂數,然後轉型為 [3, 5] 的整數亂數,其實這種方式跟餘數運算一樣會有每個整數出現機率不均等的問題,簡單來說就是現在有 11 個球要放進 3 個籃子裡,不管怎麼放,每個籃子的球都不可能一樣多。

如果您需要非常標準的均勻分布(uniform distribution),可以使用這個版本:

/* 產生 [0, n) 均勻分布的整數亂數 */
int randint(int n) {
  if ((n - 1) == RAND_MAX) {
    return rand();
  } else {
    /* 計算可以被整除的長度 */
    long end = RAND_MAX / n;
    assert (end > 0L);
    end *= n;

    /* 將尾端會造成偏差的幾個亂數去除,
       若產生的亂數超過 limit,則將其捨去 */
    int r;
    while ((r = rand()) >= end);

    return r % n;
  }
}

使用這個 randint 函數產生特定範圍整數亂數:

/* 指定亂數範圍 */
int min = 4;
int max = 10;

/* 產生 [min , max] 的整數亂數 */
int x = randint(max - min + 1) + min;

這種作法就好像要把 11 個球要放進 3 個籃子裡,而最後多出來的 2 顆球就直接丟掉,確保每個籃子都一樣只有 3 顆,這樣大家的機率就可以相等了。

這種使用截斷分布(truncated distribution)來校正機率的方式雖然在理論上是正確的,但是 rand 函數是使用 LCG(Linear Congruential Generator)來產生亂數的,他的優點只是快速、方便而已,但它本身所產生的亂數品質沒有非常好,再怎麼校正效果都有限,若需要高品質的亂數,請改用 C++11 標準的 <random> 函式庫

參考資料:Edison.X. BlogtutorialspointStackOverflowStackOverflow