《编程珠玑》-- 开篇:BitMap算法用于磁盘文件排序的原理与实现

一、问题提出与描述:

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

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

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

需要另想他法,比如遍历未排序的文件多次,第一次将从固定范围的一部分数据(比如:032767大小范围)的最小值开始读取进内存,排序完毕之后输出到排序文件中去,下一次依旧遍历整个文件读取下一区间(3276865535),以此类推(需要读取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%8=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 8

int 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能产生的最大随机数是32767

void 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空间压缩和快速排序去重

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值