上一篇講述了用位圖實現無重復數據的排序,排序算法一下就寫好了,想弄個大點數據測試一下,因為小數據在內存中快排已經很快。
一、生成的數據集要求
1、數據為0--2147483647(2^31-1)范圍內的整數;
2、數據集包含60%的0--2^31-1的整數,即踢去40%的數;
3、數據集中無重復數據,即任意兩個數不相等;
4、生成的數據盡可能亂序。
二、方案分析
開始只是想弄個大點數據玩一下而已,覺得測試數據應該要滿足上面的要求,動手寫的時候發現,滿足前3個要求都很容易,實現盡可能的亂序不好處理,計算一下這樣的數據大概有多大,每個整數按10個字符計算,60%2^31*10B=12GB,存在磁盤中需要12GB空間,如果能放入內存,整數按4字節整數計算60%*2^31*4B=4.8GB。
《編程珠璣》第一章習題的第4題與這里的要求類似,書上給的解是這樣的:
//生成k個0-n之間的隨機整數 for i = [0,n) x[i] = i for i = [0,k) swap(x[i],x[randint(i,n-1)])//randint(a,b)生成的是[a,b]之間的隨機數,swap(a,b)表示交換a,b的值 save(x[i])
上面的解法是建立在n比較小,大小為n的數組能放在內存的條件下的,按之前的分析,如果建立一個n=2^31-1的數組,需要的內存是8GB,因此內存放不下,swap(x[i],x[random])這樣的操作無法進行。也許我們可以先生成滿足1-3的條件的數據:
for(long num=0;num<=LONG_MAX;num++){ if (rand() <= 0.6*RAND_MAX)//利用隨機數實現抽樣60% saveData(num); }
接下來再進行亂序處理,和排序算法一下,一個可選的方案是進行分段歸並亂序處理。
不過我在想既然可以用位圖排序,為什么不能用位圖生成。受到散列表的啟發,設計了一個用位圖生成的方法。步驟如下:
1、在內存中申請一個2147483647位大小的位圖B,需要內存為2^31/8B=256MB的內存;
2、將位圖的所有位設置為0(B[i]=0),表示0-2147483647的所有數均未使用過;
3、生成一個0-2147483647之間的隨機數random,在位圖中檢查B[random]是否等於0,如果為0,表示這個數沒有用過,把random寫入文件,並置B[random]為1;如果為1,表示這個數已經被使用過了,此時去檢查random+1是否等於0,等於0就保存(random+1),並置(random+1)為1,如果不為0,則再探測random-1,random+2,random-2...,直到遇到一個為0的位,這和散列表的沖突處理類似,我這里用擺動線性探測。
偽代碼如下:
void generatorData(){ B = new bitset(LONG_MAX); B.reset();//將位圖置0 count = 0;//計數器 while(count <= 0.6*LONG_MAX){ random = getLongRand(); offset = 0; while(B[random+offset]==1){ offset = getNextIndex();//獲取下一個探測偏移量 } saveData(random+offset); count++; B[random+offset]==1//該數已經被使用 } } }
按照算法思想,每次產生一個隨機數,如果這個隨機數未被使用過,就保存,否則就找一個離這個隨機數最近且未被使用的數保存。這里有兩個關鍵的地方,一個是getLongRand(),這個產生0-LONG_MAX隨機數的隨機性直接影響了整個數據集的隨機性,如果getLongRand()滿足隨機,那生產的數據也會是隨機的。另外一個就是getNextIndex(),如果隨機數已經被使用,需要在其周圍探測,這個探測序列的設計的優劣將影響算法的實現效率,如果總是探測失敗,就會在探測上花費太多時間,特別是在后期,很多數都已經被使用了,需要的探測的次數變得很多。如果用這個算法生成100%的數而不是60%,將會非常耗時,試想最后幾個數總是要遍歷整個數空間,但我們只生成60%的數據,位圖中的0還不至於非常稀疏,不需要進行耗時的查詢。
實現代碼如下:
1 /*********生成一個左右擺動的序列:1,-1,2,-2...**************/ 2 long getNextIndex(long size,long index){ 3 static short tag = -1; 4 static long left = 0; 5 static long right = 0; 6 if (index == -1){//對不同的index,需要將static變量重置 7 tag = -1; 8 left = 0; 9 right = 0; 10 } 11 if(index + (left - 1) < 0 && index +(right + 1) >= size) 12 return 0;//已經遍歷完,不需要再找了 13 if (index + (left - 1) < 0) 14 return ++right;//左邊已經越出界限了,試探右邊 15 if (index+(right + 1)>=size) 16 return --left;//右邊已經越出界限了,試探左邊 17 if (tag == -1){//左右都沒有出界,左右依次試探 18 tag *= -1; 19 return ++right; 20 }else{ 21 tag *= -1; 22 return --left; 23 } 24 } 25 26 void makePhoneNum(unsigned char *bitmap,long maxNum,short bitSize){ 27 FILE * phoneNumFile = fopen("phoneNumber.txt","w"); 28 long count = 0; 29 long percent = 0.6*maxNum; 30 while(true){ 31 long index = randLong(bitSize); 32 long offset = 0; 33 while(find(bitmap,index+offset) == 1){//這個數已經用過或者不存在 34 offset = getNextIndex(maxNum,index); 35 if(offset == 0){//查找的偏移量為0說明數都用過了 36 fclose(phoneNumFile); 37 return; 38 } 39 } 40 getNextIndex(maxNum,-1);//將static變量重置 41 long loc = index+offset; 42 setOne(bitmap,loc); 43 fprintf(phoneNumFile,"%ld\n",loc); 44 if(++count > percent)//保存了80%終止 45 break; 46 if(count%1000000==0) 47 printf("count:\t%ld\n",count); 48 } 49 fclose(phoneNumFile); 50 }
生成隨機數randLong()在下一篇單獨介紹,下一篇會總結下隨機數,也可以在Github上查看。
數據生成完后,發現其實可以生成一個降序的文件,再按升序排序也能驗證排序算法。最后發現生成12G的數據將近要2天,需要探測的次數變多后變得很慢,這次屬於瞎折騰了,不過結果不重要,通過這次折騰還是熟悉了位圖的基本操作,並對隨機數有了新的認識,而且我認為這個位圖+沖突處理的方法還是很有啟發的。