標籤:
前些天在蘑菇街的面試中碰到一道洗牌的演算法題,拿出來和大家分享一下!
原題是:54張有序的牌,如何無序的發給3個人?
這個題是運用經典的洗牌演算法完成。首先介紹一種經典的洗牌演算法--Fisher-Yates.現在大家在網上看到,大多是Fisher-Yates演算法的變形。將本來O(n2),簡化到了O(n).代碼如下:
#include<stdio.h>#include <stdlib.h>void func(char *, int);void main(){ char a[7] = {‘a‘,‘b‘,‘c‘,‘d‘,‘e‘,‘f‘}; func(a,6); //a[7] = ‘\0‘; puts(a);}void func(char *date, int length){ char t; //t為交換字元空間 int i, j; while(--length){ srand(time(0)); i = rand()%(length+1); t = date[i]; date[i] = date[length]; date[length] = t; }}
演算法的思路就是:
a,選中未操作的最後1個位置,在1至該位置之間產生一個隨機數(包含這兩個值),
b,然後交換這兩個值,造成的結果就是在未操作的所有牌中隨機的取一張放到未操作的牌底(數組尾),然後length-1使未操作的範圍減一,
c,重複上面步驟,直到未操作的值為1停止
代碼寫的很清晰,也很簡單。網上還有很多種方法。但通過大量的測試,可以給出肯定,改進後的Fisher-Yates演算法,即上述代碼,已經是洗牌演算法中很好的一個了。即簡單,隨機性又好。所以如果在面試中給出上述答案,就基本可以進行下一個問題了。如果面試官還覺得不夠。可以從代碼的完整性和魯棒性(劍指offer中有提到具體做法),如參數正確性,遇到無效輸入等方面改進。或者最佳化隨機值。
關於最佳化隨機值,這裡我們還可以再提一點。
首先必須肯定的一點是:電腦中的隨機數並不是真正的隨機數,它是一種偽隨機數,是通過某些公式計算出來的隨機數,通過改變式子中參數的值從而達到隨機的效果。雖然是偽隨機數,但其實已經可以達到我們編程中的要求了。
C語言中產生隨機數的函數有4個,兩兩搭配使用:
int rand(void); 返回 0 ------- RAND_MAX 之間的一個 int 類型整數,該函數為非安全執行緒函數。並且產生隨機數的效能不是很好,已經不推薦使用。
void srand(unsigned int seed); 設定種子值,一般與“目前時間 + 進程ID”作為種子,如果沒用調用該函數,則通過rand返回的預設種子值為1。
long int random(void); 返回 0 ------- RAND_MAX 之間的一個 long 類型整數,該函數會產生一個非常大的隨機值,最大為 16*((2**31)-1)。
random 函數使用非線性反饋隨機數發生器產生預設大小為31個長整數表所返回的連續偽隨機數。
void srandom(unsigned int seed); 設定種子值,一般與“目前時間 + 進程ID”作為種子,如果沒用調用該函數,則通過random返回的預設種子值為1。
上面的這四個函數都是C語言產生隨機數時用到的函數,
如果你使用 srandom 種植種子, 則你應該使用 random 返回隨機數, 如果你使用 srand 種植種子, 則你應該使用rand返回隨機數。
不過srand和rand官方已經不推薦使用。原因是產生隨機數的效能不是很好, 另外是隨機數的隨機性沒有random好, 再者就是不是安全執行緒。
至於rand的線程不安全,如果你在linux下看過它的原始碼的話,這句話就很容易理解了。
/* 這兩個函數是C庫中產生隨機數的程式。你需要先 使用srand()函數賦隨機數種子值。然後再使用 rand()函數來產生隨機數。但是產生隨機數的演算法 較簡單,srandom()和random()函數是對這兩個函數 的改良,用法也很類似。*/#define RANDOM_MAX 0x7FFFFFFFstatic long my_do_rand(unsigned long *value){ /* 這個演算法保證所產生的值不會超過(2^31 - 1)這裡(2^31 - 1)就是 0x7FFFFFFF。而 0x7FFFFFFF 等於127773 * (7^5) + 2836,7^5 = 16807。 整個演算法是通過:t = (7^5 * t) mod (2^31 - 1) 這個公式來計算隨機值,並且把這次得到的值,作為下次計算的隨機種子值。 */ long quotient, remainder, t; quotient = *value / 127773L; remainder = *value % 127773L; t = 16807L * remainder - 2836L * quotient; if (t <= 0) t += 0x7FFFFFFFL; return ((*value = t) % ((unsigned long)RANDOM_MAX + 1));}static unsigned long next = 1;int my_rand(void){ return my_do_rand(&next);} void my_srand(unsigned int seed){ next = seed;}
從代碼中可以清晰的看到,srand()函數是將參數賦值給了一個全域變數,這也是為什麼srand()使用int型參數且沒有傳回值卻可以影響rand()的值。srand()修改了next的值後,rand()函數可能隱式的調用了該參數(間接調用)。看到這裡,你就能明白為什麼它是線程不安全函數,如果再多線程操作中,一個線程線調用srand,再未完成時另一個線程修改了next則該次操作無意義,這就是基本的多線程編程中的進程的同步與互斥。random()的改進就是基於這一點進行改進。除此之外,我們還可以看到rand()的傳回值是基於一個類似於y=ax+b的Function Compute出來的。而該運算式中a,b都是常量值,而這裡的x就是隨機種子,即通過srand()修改種子。
最後要提的一點是:
假如你想產生 1 ------10 之間的一個隨機數, 建議您像下面這樣編碼
j = 1 + (int) (10.0 * (rand() / (RAND_MAX + 1.0)));
而不是下面這樣的代碼
j = 1 + (rand() % 10);
第一行代碼的優勢在於使用實型代替整形,資料是連續的,從而隨機的更均勻,從而提高隨機性。
如果你想要產生1-8的隨機數,j = 1 + (int) (8.0 * (rand() / (RAND_MAX + 1.0))); //即可
1-x: j = 1 + (int) (x.0 * (rand() / (RAND_MAX + 1.0))); //即可
洗牌演算法Fisher-Yates以及C語言隨機數的產生