C++標準模板庫規範每次插入操作都在O(log m)時間內完成,而遍歷集合則需要O(m)時間,所以每次查找並插入一個元素的時間是O(mlog m).
一些隨機數的庫函數:(以下來自百度百科)
C庫函數 rand():rand()函數是產生隨機數的一個隨機函數。C語言裏還有srand()函數等。
(1)使用該函數首先應在開頭包含頭文件stdlib.h
#include<stdlib.h>
(2)在標準的C庫中函數rand()可以生成0~RAND_MAX之間的一個隨機數,其中RAND_MAX是stdlib.h 中定義的一個整數,它與系統有關。
(3)rand()函數沒有輸入參數,直接通過表達式rand()來引用;例如可以用下面的語句來打印兩個隨機數:
printf("Random numbers are: %i%i\n",rand(),rand());
(4)因爲rand()函數是按指定的順序來產生整數,因此每次執行上面的語句都打印相同的兩個值,所以說C語言的隨機並不是真正意義上的隨機,有時候也叫僞隨機數。
(5)爲了使程序在每次執行時都能生成一個新序列的隨機值,我們通常通過爲隨機數生成器提供一粒新的隨機種子。函數 srand()(來自stdlib.h)可以爲隨機數生成器播散種子。只要種子不同rand()函數就會產生不同的隨機數序列。srand()稱爲隨機數生成器的初始化器。
rand()產生僞隨機數,srand函數提供種子,種子不同產生的隨機數序列也不同,所以通常先調用srand函數 time(0)返回的是系統的時間(從1970.1.1午夜算起),單位:秒,種子不同當然產生的隨機數相同機率就很小了。
unsigned int seed;
srand(seed);// 我自己常這樣寫: srand((unsigned int) time(NULL));
rand();//以這三條語句爲基礎,如果seed = time(0) 的話,每次種子不同,就可以產生隨機數了。
注意一點:
RAND_MAX是VC中stdlib.h中宏定義的一個字符常量:
#define RAND_MAX 0x7FFF
其值最小爲32767(2^15 -1, 即0x7FFF-1 ),最大爲2147483647,
通常在產生隨機小數時可以使用RAND_MAX。
以上是隨機數問題的基礎知識。下面討論幾種方法。
題目:給定n個數,存放在數組中,抽取其中的m個數輸出,要求每個數被選中的概率相等。
設數組爲 a[n]
輸出a[0] 的概率爲: m/n;
輸出a[1] 的概率爲: m/n * (m-1)/(n-1) + (1-m/n) * m/(n-1) = m/n; 其中m/n爲算則a[0]的概率,(1-m/n)爲不選擇a[0]的概率,(m-1)/(n-1) 和 m/(n-1)分別表示對於a[0]的兩種情況中選擇a[1] 的概率,最後a[1]被選擇的總概率爲m/n.
以此類推。
以上證明來自概率論的條件概率知識,很容易理解的。
解法一:
i=0:rad = rand(), 如果rad%(n-i)=rad%n<m,則輸出a[i],另m=m-1; 如果 如果rad%n>=m,則不輸出a[i], 從剩餘的n-1個數中選出m個隨機數
i=1:rad= rand(), 如果rad%(n-i)=rad%(n-1) <m,則輸出a[i],另m=m-1;如果 如果rad%n>=m,則不輸出a[i], 從剩餘的n-1個數中選出m個隨機數
以此類推。這種方法到底正確不,可以考慮一個極限的情況,就是前 n-m 個數都沒輸出,就剩最後的m個數,理論上講着m個數就必須全部輸出才正確。
到數第m個數時爲
i= n-(m-1) = n-m+1:rad%(n-i) = rad% (n-(n-m+1)=rad%(m-1),這個等式的最後結果肯定屬於[0,m-1]區間內,該區間內的所有數都小於m,所以a[i] 肯定輸出,最後另M=m-1了,M表示要輸出的剩餘數的個數;
這就是 i 指向a[n]中倒數第m個數的情況,i=i+1,後,i 指向 a[n] 中的倒數m-1個數, 而上面已經輸出一個數了,m已經減掉1了,所以就變成了, i 指向 a[n] 中的倒數m-1個數, 需要輸出的數爲M=m-1個。 又回到了剛纔的狀態。
重複以上過程,可知,對於假設的極限情況,就是前 n-m 個數都沒輸出,剩餘的m個數會全部輸出的!OK啦!!
代碼:
void rad_m_from_n(int m, int n)
{
for(i=0; i<n; i++)
{
if(rand() %(n-i) <m)
{
cout<<a[i]<<endl;
m--;
}
}
}
解法二:
將原始數組打亂,然後將數組的前m個數輸出。
打亂的方法就是對於每一個a[i] ,從a[i] ~ a[n-1]中隨機挑選一個a[j],交換a[i] a[j] 的值,由於只輸出前m個,所以執行m次循環就可以了。
void rad_m_from_n2(int m, int n)
{
int i,j;
for(i=0; i<m; i++)
{
j = rand(i,n-1);//從區間[i],n-1]內隨機抽取一個數,函數實現在下面有講解
int t = a[i];
a[i] = a[j];
a[j] = t;
}
//sort(a,m)//如果需要按順序輸出的話,就將數組的前m個數排序
for(i=0; i<m ;i++)
cout << a[i]<<endl;
}
題目變形
變形一:《編程珠璣》第12章中的習題,rand()通常返回約15個隨機位,使用該函數實現函數bigrand()和randint(l,u),要求前者至少返回30個隨即位,後者返回[l,u]範圍內的一個隨機整數。
先來分析bigrand():初始是15位,現在是30位,自然想到了倍數的關係,由於2^30 = 2^15 * 2^15 ,可見bigrand()的返回值最大應當是RAND_MAX *RAND_MAX 了。答案最後爲
int bigrand()
{
return RAND_MAX*rand() + rand();
}
金子分析:這個至少30個隨機位,說實話我還沒有搞懂,應該是最大值至少爲30bits表示的最大數吧,如果按照答案的辦法,產生的隨即數集中在(2^15)~(2^30 -1)吧,確切的邊界我沒有比較,當然前提是rand()不等於0,到底經過隨機後rand()會不會結果爲0,我沒實驗過,上述的辦法導致 1 ~ (2^15 -1) 無法產生阿。
我能想到的一種改進方法是 RAND_MAX*(rand() %2)+rand(),利用第一個rand()產生的數的奇偶性來指定輸出前半段還是後半段的數。
randint 就好實現多了
int randint (int l,int u)
{
return l + bigrand() % ( u-l + 1);//注意邊界不要弄錯了。邊界應該理解爲u - (l -1).
}
變形二:(百度的一道題目)
爲了分析用戶的行爲,系統往往需要存儲用戶的一些query,但是因爲query非常多,所以系統不能夠存下每一條。假設我們的系統每天只能夠存儲m個query,現在需要設計一個算法,對用戶時時請求的query進行隨機選擇m個,請給出一個方案,使得每一個query被抽中的概率儘量相等,也請附加相應的分析。需要注意的是,不到最後一刻你並不知道用戶的總請求量是多少。
金子分析:本題需要一個假設,假設每天query的總數爲n,題目就變成了n個數,隨機抽取m個數的問題,但是因爲題目不是將數存儲在數組中,而是將抽取的數存放在a[m]中,所以具體實現如下:
1)先將每天的前m個query存放在數組a[m]中。
2)
for(i=m; i<n; i++)
{
r = rand() % i; //隨機[0,i]
if (r>=0 && r <m)
{
a[r] = a[i];
}
}
3) 驗證每個query被抽中的概率是否相等。
首先看a[n-1],它被抽中的概率是 m/n;
再看a[n-2], 隨機到[0,m-1]的概率爲 m/(n-1),要是它最後存在數組中的前m個,要保證對於a[n-1]時,獲得的隨機數不能與a[n-2]時的隨機數相等,這樣a[n-1]的隨機數有 (n-1)種選擇,則a[n-2]最後存在a[0...m-1]的概率爲 m/(n-1) * (n-1)/n = m/n;
以此類推,對於a[m,...,n-1]來說,被抽中的概率相等,且都爲 m/n.
最後我們在看已經放到數組中的前m個query, 對於a[0], 要想使a[0]不被替換,則從i=m 開始,一直到i=n-1,每個元素的隨機數都不能爲0,
則 i=m時,隨機數不爲0的概率爲, m/(m+1)
i = m+1, 爲(m+1)/(m+2).
......
i = n-1時,爲 (n-1)/n.
最後,a[0]不被替換的概率爲m/(m+1) * (m+1)/(m+2) * (m+2)/(m+3)* ...... * (n-1)/n = m/n;也就是說,a[0]不被替換,即被抽中的概率是 m/n.
以上方法得證!