BitMap算法用于磁盘文件排序的原理与实现

来源:互联网 发布:陕西省国家税务局网络 编辑:程序博客网 时间:2024/06/04 07:24

一、问题提出与描述:

我们经常会遇到一类问题,这类问题是:有一个大的文件,该文件中存有大量的未经排序的数字,给定限制大小的内存,设计算法实现对该文件中的所有数字的排序,并将排序结果放到新文件中(经常会以时间日期等命名)。

比如:有一个文件,该文件中有0~4294967295区间内的2147483648个数字,并且是乱序的,给定最大内存为1GB,对该文件中的数字进行排序。

一般情况下,这类问题有个特点,就是给定的内存用一般的冒泡、选择、快速排序等等的方法是无法解决的。因为这些算法中,作为一个数字元素,一般占据内存至少为32位(32位机为例对应int型或unsigned int型),假设均为unsigned int型数据,则从0~4294967295的2147483648个无符号整型数据。需要的内存大小为:2147483648*4Byte = 8192MB = 8G,但是只给定了1G的空间,就不能将文件中的数据全部一次性读取到内存数组中,排序完毕后再写入内存中去。

需要另想他法,比如遍历未排序的文件多次,第一次将从固定范围的一部分数据(比如:0~32767大小范围)的最小值开始读取进内存,排序完毕之后输出到排序文件中去,下一次依旧遍历整个文件读取下一区间(32768~65535),以此类推(需要读取4294967295/32768=131072次),当2147483648个数据读取完毕之后排序也就完成了。修改每次的区间大小可以减少读取文件的次数,只要一次读取的文件数据在内存中占用空间小于给定内存大小即可。但是这种方式多次遍历文件数据的次数依旧比较多。(该思路可详细参考《编程珠玑》第2版)

今天我们用更简单(高效率、低消耗)、更理想的一种方法,即BitMap(位图)来实现大文件数据的内存排序。为什么说高效率,因为bitmap的操作基本都是对bit位进行位操作,效率更高,而低消耗是因为bitmap的一个bit位就能代表一个32bit、64bit…的数字(即理论上任意大小的数字),比如映射2147483648个32bit的数字消耗内存为:4294967295/8(bit)/1024(KB)/1024(MB) +1=512MB。(该内存量与最大的值有关,也就是说需要提前知道最大的值,或者预先分配可能存在的最大值的内存空间。上面所说的多次分区间读取也是需要知道最大值的)。至于为什么会是512MB,后面会具体分析。

二、BitMap的原理分析:

我们以0、1、3、4、7、8、10、12、13、14在BitMap中的存放形式为例说明:
由于一个字节有8bit,14是最大的值,则需要2个字节来存储(2*8=16bit>14)。存放位置如下:

这里写图片描述

arr是一个char类型数组,一个数组元素占8bit。单个字节的元素是没有意义的,而每个字节的具体一个bit位代表一个数字,这个位置是如何对应的?是根据该数字val对8取整、取余算出来的,比如:14存放在对应的位置为arr[1]的从右往左数的第6位(8是第0位)。因为:
14/8=1(arr的第一个元素arr[1])
14%=6(第6位)
所以说一个数字对8取整和取余就可以定位其所在字节与所在字节的第几位。但是该数字实质并不是这样存放的,因为这里只有一位,而一个14(1110)至少占四位,上面只是一个映射。实际存储方式为下图所示:

这里写图片描述
某一位为1则代表该bit位对应的数字(映射的数字)是存在的。比如:arr[0]的从右往左第7位为1(0*8+7=7),则该位置映射的值为7,若该位置为0,则7不存在。上面两张图第一张是映射关系图,第二张是实际内存中的存储形式。

那么原理搞清楚了,算法如何实现?我们需要用为操作来实现:

1、已知value值,确定数组中的存储位置:
arr[value/8] |= 0x01 << (avlue%8);
2、从数组中取出指定位置的bit位:
value = (arr[i]>>j) & 0x01;
//第i个元素的第j个bit位(从右往左并且j取值范围为0~7)

我们以一个简单的排序程序为例,看看代码以及内存中的数据存放:

#include <stdio.h>//最大值1012,则只需要1024个比特位即可:1024/8=128Byte//亦即1012/8+1,或者是:((1012%8) ? (1012/8+1) : (1012/8));#define MAXBIT 128#define NUM 20#define LEN 8int main(void){    int arr[NUM] = {709,1000,670,778,30,1012,70,506,175,8,911,43,910,178,1011,137,1,103,120,374};    char bitMap[MAXBIT] = {0};//定义并初始化建立bitMap的数组    int i = 0, j = 0, k = 0;    int val = 0;    //第一步:将数组中无序的元素读进内存中的对应bit位    for(i=0; i<NUM; i++){        val = arr[i];        bitMap[val/LEN] |= 0X01<<(val%LEN);//给对应bit位置为1    }    //第二步:将读取进内存的值从小到大判断,并依次输出到数组中    for(i=0,k=0;i<MAXBIT;i++){//读取每一个字节        for(j=0; j<LEN; j++){//读取一个字节的每一位            //如果当前位为1,则该为对应的数字存在,计算后输出            if((bitMap[i]>>j) & 0X01){  //要对bitMap中的每个位进行判断是否为1                //printf("%d\t",i*LEN+j);                arr[k++] = i*LEN+j;            }        }    }    //遍历一遍,检验排序正确性    for(i=0; i<NUM; i++){        printf("%d\t",arr[i]);    }    return 0;}

测试如下(因为是从小到大读取的,读出来的数据写会数组自然是满足有序的):
这里写图片描述

接着我们对内存中的每一个bit位进行输出来观察这20个数字的映射情况,读取的代码如下:

void SetColor(unsigned short ForeColor){    HANDLE dev = GetStdHandle(STD_OUTPUT_HANDLE);    SetConsoleTextAttribute(dev,ForeColor);//需要windows.h头文件}int main(void){    //第一步    /*按上面的来,不改变*/    //第二步,对每一位进行输出    for(i=0,k=0;i<MAXBIT;i++){        SetColor(4);        //显示红色的数组字节单元(也是元素下标)编号。从0~127        printf("%-4d",i);        for(j=0; j<LEN; j++){            if((bitMap[i]>>j) & 0X01){                arr[k++] = i*LEN+j;                SetColor(7);    //显示白色的1,即存在的值                printf("1");            }            else{                SetColor(2);                printf("0");    //显示绿色的0,即不存在的值            }        }        putchar('\t');        if((i+1)%8 == 0){            putchar('\n');        }    }    putchar('\n');    for(i=0; i<NUM; i++){        SetColor(7);        //遍历的排序后的数组值(显示白色)        printf("%d\t",arr[i]);    }    return 0;}

测试结果如下:
这里写图片描述

我们可以清晰地看到每一个数字在bitMap中的映射关系。

三、文件排序简单测试:

文件排序就是要将文件中的每个数字取出来,取出来的是字符串,先转换成数字,再计算其在bitMap的数组中的位置,通过位运算放进去。之后从小到大读取每一位,把为1的位通过映射关系计算出数字值,然后写到输出文件中去,最终输出文件中的数字就是有序的。(这里有一个需要注意的点:就是这种简单的bitMap只能用来排序无重复值的大量数据,或者在排序时剔除重复值时适用(亦即去重复值的好方法)),bitMap的变化形式有多种,但万变不离其宗:取整运算、取余运算、移位运算的完美结合。

测试文件排序的代码:

#include <stdio.h>#include <stdlib.h>#include <time.h>#define NUMBER 65536 //产生的数据个数#define MAXNUM 32767#define BITMAP ((MAXNUM%8) ? (MAXNUM/8+1) : (MAXNUM/8)) //分配字节数(由最大的数字决定)//注意:rand能产生的最大随机数是32767void create(void);void sort(void);int main(void){    create();//构建一个乱序文件    sort();//对乱序的文件进行排序    return 0;}void create(void){    char bitmap[BITMAP] = {0};//初始化用于建立映射的数组    int i=0,val = 0;    FILE * fp = fopen("../FileSort_BitMap/noSort.txt","w+");    if(NULL == fp){        perror("Open error");        exit(EXIT_FAILURE);    }    srand(time(NULL));    for(i=0;i<NUMBER;i++){        val = rand();        #if 0        //剔除重复值        if((bitmap[val/8] >> (val%8)) & 0x01){            continue;//如果已经存在就不向文件中写入了        }        #endif    //如果构建时不需要剔除重复值,则不需要建立bitMap映射        bitmap[val/8] |= 0x01 << (val%8);        fprintf(fp,"%d\n",val);    }    if(!fclose(fp)){        printf("write success\n");    }}void sort(void){    FILE * fpr = fopen("../FileSort_BitMap/noSort.txt","r+");//打开未排序的文件    if(NULL == fpr){        perror("ReadFile Open error");        exit(EXIT_FAILURE);    }    FILE * fpw = fopen("../FileSort_BitMap/Sort.txt","w+");//新建并打开作为输出的排序后的文件    if(NULL == fpw){        perror("WriteFile Open error");        exit(EXIT_FAILURE);    }    int val=10,i=0,j=0;    char buf[8] = {0};    char bitmap[BITMAP] = {0};    while(fgets(buf,8,fpr) != NULL){        //fscanf遇到空格和换行时结束,fgets遇到空格不结束        val = atoi(buf);        bitmap[val/8] |= 0x01 << (val%8);    }    for(i=0;i<BITMAP;i++){  //判断每一个字节        for(j=0;j<8;j++){   //判断一个字节的每一位            if((bitmap[i]>>j) & 0x01){//如果该位为1则输出bitmap对应的数字到排序后的文件中                fprintf(fpw,"%d\n",i*8+j);            }        }    }    if(!fclose(fpr) && !fclose(fpw)){        printf("close success\n");    }}

关于上例测试,构建时去除重复值的结果为:

这里写图片描述

构建时不去除重复值的结果为:

这里写图片描述

但是无论构建时去除重复值与否(noSort.txt的创建结果),经过本次bitMap的操作,其重复值都被去除掉了(Sort.txt的排序结果)。

参考资料:
参考书籍:《编程珠玑》,Author:Jon Bentley
参考博客:[Data Structure] Bit-map空间压缩和快速排序去重