隨機數的生成

問題描述:

        現要求產生 0~n-1 範圍內的 m 個隨機整數的有序列表,且不允許重複,m <= n。

        考慮到 n 的值可能很大,而通常 C/C++ 提供的隨機數產生器所能返回的隨機數在 [0,RAND_MAX],其中,RAND_MAX 爲 0x7FFF。也就是說只有 15 位的隨機性。因此,我們需要要有自己的隨機數產生器,以便能夠返回更多位數的隨機數,通常爲 30 位。下面的函數可以滿足我們的要求:

int bigrand()
{
return (RAND_MAX * rand() + rand());
}

注意,這裏也可以採用將第一個 rand() 的返回值左移 15 位,然後加上第二個 rand() 的返回值的方法來提速。

解法一、

        下面,我們就可以用這個隨機數產生器來解決上面的問題了。看到這裏,你是不是想到了什麼解決方法呢。下面的方法來自算法大師 Donald Knuth。

void genknuth(int m, int n)
{
srand((unsigned int)time(NULL));

for(int i = 0; i < n; i++)
{
   if((rand() % (n - i)) < m)
   {
    cout<<i<<"/n";
    m--;
   }
}
}

其中的 srand 語句是我爲了在每次運行時能夠得到不同的隨機數序列而加的。

         不知道這個方法有沒有出乎你的意料,算上 for 循環才 4 句話。不過,就是這 4 句話,已經很完美地解決了上面提出的問題,有序、互異、m 個數。

        這個方法通過順序掃描 0~n-1 範圍內的每個數保證產生的隨機數有序和互異。

        那產生 m 個數怎麼保證呢?又沒有類似循環的語句來控制產生的隨機數的個數。

        要記住,程序語句只是我們實現某種功能的一種表現形式,這也就是說,我們要實現某種功能,可以由很多種表現形式。下面就來分析一下這個方法是如何保證能夠產生 m 個隨機數的。

         想必大家都已經看到產生的隨機數都是在 if 語句中輸出的,那也就是說 if 語句控制了產生的隨機數的個數。因此,要說明能夠產生 m 個隨機數,只要說明 if 語句能夠執行且僅能執行 m 次。

         首先,在 for 循環沒有推出的情況下,if 語句最多能夠執行 m 次,因爲 if 語句的每次執行都會使 m 的值減 1,當 m=0 時,if 語句就不會再執行了。因爲 (rand() % (n - i)) 是一個非負數。

         其次,我們分析 if 語句最少能夠執行多少次。如果 if 語句開始一直得不到執行,那麼 (n-i) 便會一直減小,直到與 m 相等。這時 (rand() % (n - i)) 一定小於 (n-i),也就是 m,從而使得 if 語句得到一次執行。而此時 for 循環已經執行了 (n-m+1) 次,離推出循環只剩下 (m-1) 次,要使得 if 語句能夠執行 m 次,那麼接下來的 (m-1) 次循環 if 語句必須都得能夠執行。事實是什麼樣的呢?第一次 if 語句開始執行時,(n-i)=m,執行完時,(n-i) 和 m 都減少 1,從而仍然有 (n-i)=m 成立。這個過程一直持續到 m=0。剛好是 m-1 次。

         因此,我們可以相信 if 語句確實能夠執行且只能執行 m 個。

        Donald Knuth 實在是太厲害了!

        順便說一句,據說在大學期間,有關程序設計之類的比賽,只要有他參加,第一名非他莫屬。

 

解決方法二:就是在一個初始爲空的集合中插入隨機整數,直到足夠的整數。
僞代碼如下所示:
initialize set S to empty
size = 0
while size < m do
      t = bigrand()%n
      if t is not in S
           insert t into S
           size++
print the elements of S in sorted order
該算法在選擇元素時能夠保證所有的元素都具有相同的選中概率,它的輸出是隨機的
void genknuth(int m, int n)
{
 set<int> S;
 while( S.size() < m)
  S.insert(bigrand()%n);
 set<int>::iterator i;
 for(i = S.begin(); i != S.end(); ++i)
  cout<<*i<<"/n";
}
C++標準模板庫規範能夠保證在O(log m)時間內完成每個插入操作,集合內部迭代需要O(m),所以整個程序需要的時間爲O(m log(m)(和n相比,m較小時)。然而,該數據結構的空間消耗較大。

 

解法三、
for i = [0,n)
      swap(i, randint(i,n-1))
這個是弄亂數據x[0...n-1]。在這個問題中,只需要攪亂數組前面m個元素
int bigrand()
{
 return RAND_MAX*rand() + rand();
}

int randint(int l, int u)
{
 return l + bigrand() % (u-l+1);
}

void genknuth(int m, int n)
{
 int i,j;
 int *x = new int[n];
 for(i = 0; i < n; i++)
  x[i] = i;
 for(i = 0; i < m; i++)
 {
  j = randint(i,n-1);
  swap(x[i],x[j]);
 }
 sort(x, x+m);
 for(i=0; i<m; i++)
  cout<<x[i]<<"/n";
}

int main()
{
 srand((unsigned)time(NULL));
 int m,n;
 while(1)
 {
  cout<<"Please enter m and n:";
  cin>>m>>n;
  genknuth(m,n);
 }
 return 0;
}
該算法使用了n個字的內存並需要O(n+mlogm)時間


解法四:(遞歸)
void randselect(int m,int n)
{
    assert(m>=0 && m <= n);
    if(m>0)
    if((bigrand()%n) < m)
    {
        randselect(m-1,n-1);
        cout<<n-1<<",";
    }
    else
          randselect(m, n-1);
}

 

原文鏈接:

http://blog.csdn.net/ztj111/archive/2007/10/23/1840190.aspx

http://hi.baidu.com/xwf_like/blog/item/ff2a9b98293022006e068c2d.html

發佈了30 篇原創文章 · 獲贊 66 · 訪問量 65萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章