概述
Column 1由一個問題展開討論:如何給一個包含0到9,999,999編號的記錄檔案排序並輸出到另一個檔案,其中編號不重複,只允許1M左右的記憶體空間。
最優的解法自然是採用位元組,1.25×106B大小的位元組含10,000,000位,可以儲存規定範圍內數是否存在。這種方法很好的利用了編號不重複的特性。
產生隨機數檔案
要對演算法進行實現,面臨的第一個問題就是,如何產生這麼一個記錄檔案?這也練習4提出的問題:如何從範圍0到n-1範圍內產生k個不重複的隨機數。以下是我的一個解法(Column 12對這個問題有詳細的分析並提出了一些解法):
首先產生一個大小為n的整型數組,從第0位到第n-1位元組內容分別填對應的數組下標,這樣數組就包含了不重複的從0到n-1的所有數字。那麼接下來的工作就是每次從該數組未被選擇的數中隨機播放出一個數直到選出k個數。對此我們可以將每次選出的數和當前剩餘的未選出的數的最後一個交換,這樣選擇結束後數組的最後k個數就是我們“產生”的隨機數。以下是產生隨機數檔案的程式碼
#include<stdio.h><br />#include<stdlib.h><br />#include<time.h><br />#define MAXN 10000000<br />#define SELECTK 1000000<br />int a[MAXN];<br />template <typename T><br />void swap(T &a, T &b){<br /> if(a == b)<br /> return;<br /> a = a^b;<br /> b = b^a;<br /> a = a^b;<br /> return;<br /> }</p><p>int main(){<br />for(int i=0; i < MAXN; i++)<br />a[i] = i;<br /> srand((unsigned int)time(NULL));<br /> int select;<br />for(int k=0; k < SELECTK ;k++){<br />select = rand()%(MAXN-k);<br />swap(a[select],a[MAXN-k]);<br />}<br />FILE * fp = fopen("ranfile.txt","w");<br />if(fp == NULL){<br /> perror("can not open output file");<br /> return -1;<br /> }<br /> for(int i=0; i<SELECTK; i++)<br />fprintf(fp,"%d/n",a[MAXN-i]);<br />fclose(fp);<br />return 0;<br />}
使用這個程式後產生了一個隨機數檔案,發現產生的檔案明顯不隨機!檔案開始的一些數字很小,但是之後的數字幾乎全部集中在9,000,000之上!這顯然是存在問題的。試著減小了MAXN和SELECTK的規模到1000和100,就很正常了。查了一下rand()函數的說明,產生的偽隨機數的範圍是在0到RAND_MAX之間,我的第一感覺就是肯定RAND_MAX偏小!在程式中加入了列印RAND_MAX的語句後,可以看到RAND_MAX的值為32767。看來我的猜測是正確的。所以需要修改程式增大隨機數產生範圍,最簡單的辦法就是rand()產生的隨機數再平方作為最終的隨機數。所以上述代碼進行以下修改
/×<br /> int select;<br /> for(int k=0; k < SELECTK ;k++){<br /> select = rand()%(MAXN-k);<br />swap(a[select],a[MAXN-k]);<br />}<br />×/<br /> unsigned long select,num;<br /> for(int k=0; k < SELECTK ;k++){<br />num=rand();<br />select = (num*num)%(MAXN-k);<br />swap(a[select],a[MAXN-k]);<br />}<br />
再次進行測試,發現這次產生的結果就比較正常了。這樣,有了ranfile.txt檔案,就可以接下來進行排序演算法實現了。
排序演算法
C++提供了bit vector的模板類bitset,我們可以很方便的像使用普通數組那用利用bitset而不必關心位操作的細節。當然也可以自己實現位元組,書中也進行了一些討論並給出了樣本。實現的代碼如下
/**<br />***sort.cpp<br />***description: read for "ranfile.txt" and write the sorted result to "sortfile.txt"<br />**/<br />#include<stdio.h><br />#include<bitset><br />using namespace std;<br />#define MAXN 10000000<br />bitset<MAXN> bs;//use global object to avoid stack overflow!<br />int main(){<br />FILE * fp = fopen("ranfile.txt","r");<br />if(fp == NULL){<br /> perror("can not open output file");<br /> return -1;<br /> }<br /> int number;<br />while(EOF != fscanf(fp,"%d",&number)){<br /> bs[number]=1;<br /> }<br /> fclose(fp);<br /> fp = fopen("sortfile.txt","w");<br />if(fp == NULL){<br /> perror("can not open output file");<br /> return -1;<br /> }<br />for(int i=0; i < MAXN; i++){<br />if(1==bs[i])<br /> fprintf(fp,"%d/n",i);<br />}<br />fclose(fp);<br />return 0;<br />}
bitset模板的預設是全部被初始化為0的。注意的是我在這裡給位賦值的時候並沒有用true或者false,因為true的定義不一定是1,有可能是除0以外的某個整數,所以使用0和1可以保證最大的相容性。
還有一個問題是大變數的定義,我們應該盡量不定義在棧上而是全域變數區或者堆上,這樣可以避免棧溢出。雖然這種情況在使用者空間的應用程式中幾乎不可能發生因為應用程式的堆是可以動態增長的,但放在棧資源緊缺的核心空間下,這樣的做法就顯得危險了。所以沒有必要我認為還是不要在棧上定義大變數。
問題討論
問題5:在解決這個問題時我們使用的實際記憶體是1.25M。如果記憶體的限制不是粗略的1M而是精確的,那麼怎麼解決呢?
這個問題可以結合書中之前給出的解法綜合一下解決。我們可以將給出的大檔案先分成兩份,一份只含有0-4,999,999而另一份只含5,000,000-9,999,999。這樣按照之前的方法進行兩次排序,最後將兩個排序結果檔案合并。這樣的話我們和之前相比多出了若干次檔案操作,檔案操作相對記憶體操作是要費時很多的,所以我們應盡量減少檔案操作,可以兩次讀取ranfile.txt檔案,一次只處理0-4,999,999將結果寫入輸出檔案,然後再次讀取ranfile.txt並只處理5,000,000-9,999,999並將結果繼續寫入輸出檔案。這樣我們就只多進行了一次檔案讀取操作,加上檔案系統的緩衝策略,這種方法的效率和1.25M記憶體的解法差不了太多。
問題6:如果每個數不是最多出現一次,而是最多10次,那麼該怎麼解決呢?
我們可以對這個bitset進行改進,沒個單元不是1位,而是4位,每一單中繼存放區這個數出現的次數,問題就得到解決了。當然,如果空間還是1M限制的話,呵呵,參見問題5~