瘋狂位圖之——位圖生成12GB無重復隨機亂序大整數集


  上一篇講述了用位圖實現無重復數據的排序,排序算法一下就寫好了,想弄個大點數據測試一下,因為小數據在內存中快排已經很快。

一、生成的數據集要求

  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天,需要探測的次數變多后變得很慢,這次屬於瞎折騰了,不過結果不重要,通過這次折騰還是熟悉了位圖的基本操作,並對隨機數有了新的認識,而且我認為這個位圖+沖突處理的方法還是很有啟發的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM