《Programming Pearls》(編程珠璣下載)第一章講述了如何用位元影像排序無重複的資料集,整個思想很簡潔,今天實踐了下。
一、主要思想
位元影像排序的思想就是在記憶體中申請一塊連續的空間作為位元影像,初始時將位元影像的每一位都置為0,然後依次讀取待排序檔案的整數,將整數所在的位設定為1,最後掃描位元影像,如果某一位為1,則說明這個數存在,輸出到已排序檔案。比如待排序的資料S={3,0,4,1,7,2,5},max(S)=7,我們可以設定一個八位的位元影像B,將位元影像的每一位初始為0,即B=[0,0,0,0,0,0,0,0],對S中的每一個整數d,設定B[d]=1,即B=[1,1,1,1,1,1,0,1],最後掃描位元影像,對位元影像的每一位i,如果B[i]==1,則輸出i到已排序檔案,排序後的S={0,1,2,3,4,5,7}。
整個過程只需要遍曆一遍待排序檔案和位元影像,時間複雜度O(n),需要的輔助空間為(max(S)/8)B。雖然這個排序演算法只能在無重複的整數集上運行,但對於有些需求,確實做到高效實現,比如說給手機號碼排序,手機號碼11位,第一位始終為1,理論上可以有10^10個號碼,但一些號碼未發放,即有些號碼在系統中不存在,假設系統中有50%的合法號碼,每個號碼用long int表示,這麼多號碼所需要的空間為50%*(10^10)*4B=20GB,不能放在記憶體中進行快速排序。一個可選的方案是分多趟進行歸併排序,但需要較長的時間。我們申請一個10^10位的位元影像,需要的記憶體是10^10/8B=1.25GB,完全可以在當代的PC機上運行,在掃描位元影像時,假設某一位i為1,輸出檔案時,在前面添加一個1,例如i=3885201314,輸出為13885201314。
二、演算法實現
用c語言實現的話,需要自己封裝位元影像操作,這裡需要用到三個操作:設定位元影像的所有位為0(setAllZero);設定指定的位為1(setOne);查看指定的位是否為1(find);代碼如下:
複製代碼 代碼如下:
#include <malloc.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#define MAX_NUM 16777216//最大的數,也就是需要的位
#define BYTE_NUM (1+MAX_NUM/8)//位元組數
#define MASK 0x07
void setAllZero(unsigned char *p,long size);
void setOne(unsigned char *p,long loc);
int find(unsigned char *p,long loc);
bool getSorted(unsigned char *bitmap,char *fileName);
bool setBitmap(unsigned char *bitmap,char *fileName);
int bitmapSort();
int main(){
return bitmapSort();
}
int bitmapSort(){
unsigned char *bitmap; //位元影像指標
bitmap = (unsigned char *)malloc(BYTE_NUM*sizeof(unsigned char));
if(bitmap == NULL){
printf("Malloc failed\n");
return -1;
}
setAllZero(bitmap,BYTE_NUM);//將位元影像所有位設定為0
setBitmap(bitmap,"phoneNumber.txt");//掃描待排檔案,將位元影像對應位設定為1
getSorted(bitmap,"bitmapSort.txt"); //掃描位元影像,將位元影像為1的位號輸出到檔案
free(bitmap);//釋放位元影像
return 0;
}
/***********設定待排序資料的位元影像**************/
bool setBitmap(unsigned char *bitmap,char *fileName){
FILE *readFp;
printf("Setting bitmap...\n");
readFp = fopen(fileName,"r");
if(readFp == NULL)
return false;
long phoneNum=0;
while(fscanf(readFp,"%ld\n",&phoneNum) != EOF){
setOne(bitmap,phoneNum);//將 phoneNum位設定為1
}
fclose(readFp);
return true;
}
/*****順序遍曆位元影像輸出記錄,從而實現排序****************/
bool getSorted(unsigned char *bitmap,char *fileName){
printf("Search bitmap...\n");
FILE *writeFp;
writeFp = fopen(fileName,"w");
if(writeFp == NULL)
return false;
long phoneNum=0;
for(phoneNum = 0; phoneNum < MAX_NUM; phoneNum += 1){
if(find(bitmap,phoneNum)){
fprintf(writeFp,"%ld\n",phoneNum);
}
}
fclose(writeFp);
return true;
}
/******先將位元影像清零********/
void setAllZero(unsigned char *bitmap,long size){
for(long i=0;i<size;i++)
*(bitmap+i) &= 0;
}
/*************************************************
將指定的位置為1
(loc>>3)相當於整除2^3=8,即定位到位元組數,MASK=0x07,loc&MASK相當於loc%8
***************************************************/
void setOne(unsigned char *bitmap,long loc){
*(bitmap+(loc>>3)) |= (1<<(loc&MASK));//
}
/******尋找指定的位是否為1********/
int find(unsigned char *bitmap,long loc){
return ((*(bitmap+(loc>>3))) & (1<<(loc&MASK))) == (1<<(loc&MASK));
}
C++的STL中有一個資料結構bitset,操作位元影像很方便。
複製代碼 代碼如下:
#include <bitset>
#define MAX_NUM 4000000//最多的數,即需要的位元
using namespace std;
int main(){
FILE *readFp,*writeFp;
readFp = fopen("phoneNumber1.txt","r");
writeFp = fopen("bitsetSorted.txt","w");
bitset<MAX_NUM> bitmap;
for(long i=0;i<MAX_NUM;i++){//先將位元影像初試化為0
bitmap.set(i,0);
}
printf("Begin set bitmap...\n");
long number = 0;
while(fscanf(readFp,"%ld\n",&number) != EOF){
bitmap.set(number,1);//將number所在位設定為1
}
printf("Begin search bitmap...\n");
for(long i=0;i<MAX_NUM;i++){
if(bitmap[i] == 1)//將位1的位輸出到已排序檔案
fprintf(writeFp,"%ld\n",number);
}
fclose(writeFp);
fclose(readFp);
}
排序演算法很快就寫好了,就開始產生測試資料,想產生0—2^31的亂序資料集還真不容易,首先要保證不重複,第二要丟掉40%的數(無效手機號碼),第三要儘可能的亂序,搗了很久,最終還是找到了實現辦法,產生了12GB的資料集,關於產生這個資料集的辦法,歡迎一起討論,我將會在下一篇中總結一下我的方法。
完整的代碼可以參考github。