在之前的哈希表中,如果要在表中存放一个整数,此时就要申请一个整型的内存来存放它,一个整型数据在32位或64位平台下都占4个字节。如果现在需要存储的数据非常多,比如说40亿个不重复的数据,就需要160亿个字节来存储,1GB的内存表示的是10亿个字节,此时就需要16GB的内存来存放这些数据,而我们普通的电脑内存一般都是4G的内存,这显然是存放不下的。我们知道,内存中的最小单位是比特位。如果能用一个比特位来存放一个整型,只需要0.5GB的内存。
一个比特位可以表示一个0或1。如果要表示40亿个数据,可以申请0.5GB的内存。如果要存放的数据为10,就将第10个比特位设置为1。如果要查找的数据为100,就查看第100个比特位处的状态,如果为1说明,100存在于这堆数据中,如果为0说明不存在。
位图的概念
像上述所描述的,在一个结构中,用一个比特位来描述一个数据的状态,这种结构就称为位图。位图实际上是哈希表的一种变形。
1. 位图的结构定义
首先,该位图中最大能表示的比特位个数需要提前设定。
然后,根据最大比特位数来进行内存的申请。内存不能以比特位为单位进行申请,所以可以自己选用一种数据类型来申请内存,这里以64位8字节为一个数组元素长度进行内存的申请。
结构定义如下:
#include<stdint.h>//uint64_t需引用该头文件 //位图以uint64_t为单位进行内存的申请 typedef uint64_t BitMapType; //指定位图最大能表示的数字范围 #define MAXSIZE 1000 //位图的结构定义 typedef struct BitMap { BitMapType* data; uint64_t capacity;//位图中最大能表示的比特位个数 }BitMap;
2. 位图的初始化
初始化时要指定位图所能存储的最大比特位个数。然后根据该个数进行内存的申请。如果最大能表示的比特位为100,因为我们是以64位8字节(数组元素类型)为单位进行申请,此时需要2个数组元素的内存大小;如果最大比特位为300,此时需要5个数组元素的内存大小,所以,最大比特位capacity与64位的数组元素个数n之间满足:n = capacity/64 + 1;
在申请完内存之后,内存中存放的都是随机值,为方便后续的操作,将内存中的状态均置为0.
代码如下:
//位图的初始化 void BitMapInit(BitMap* bm,uint64_t capacity) { if(bm == NULL) { //非法输入 return; } bm->capacity = capacity; //如果位图最大表示100个数字,则需要2个64位的内存 //如果位图最大表示200个数字,则需要4个64位的内存 //如果位图最大表示300个数字,则需要5个64位的内存 //malloc以字节为单位进行内存申请 //需要申请多少的64位内存 uint64_t size = bm->capacity/(sizeof(BitMapType)*8) + 1; bm->data = (BitMapType*)malloc(size * sizeof(BitMapType)); //初始化时要将位图中的各个位均置为0,以便后续的操作 memset(bm->data,0,size*sizeof(BitMapType)); return; }
3. 判断位图中的某一位是否为1
思路如下:
(1)首先,如果该位的大小超过了位图最大能表示的范围,则判断失败
(2)如果不为(1)。则计算该位在位图结构中的哪个数组元素上,以及该数组元素的哪个比特位上。
如果要判断的位是100,一个数组元素的大小为64位,则n = 100/64 = 1,表示该位在数组下标为1的元素上;
一个数组元素有64位,100具体在哪个位上,还需要计算:offset = 100%64 = 36,表示100在下标为1的数组元素的第36位(从0开始计数)上。
(3)知道了指定位所在的位置,此时就要判断该位是否为1了。可以通过位运算来判断。因为指定位在下标为n的数组元素上的偏移量为offset,所以,先将1左移offset位之后,再与下标为n的数组元素进行按位与操作,如果指定位为1,则按位操作后的结果必不为0,如果按位操作后的结果为0,则按位操作后的结果必为0。此时,只需将按位操作后的结果与0进行比较即可。
根据上述思路,代码实现如下:
//测试某一位是否为1 int BitMapTest(BitMap* bm,uint64_t index) { if(bm == NULL || index >= bm->capacity) { //非法输入 return 0; } uint64_t n; uint64_t offset; //获取index所在的数组元素下标及偏移量 GetOffset(index,&n,&offset); //用1与之按位与,如果结果为0,则该位为0,否则为1 //如果该位为1时,按位与完的结果必定只有该位为1,其余位为0,所以对结果进行判断时 //只能跟0进行比较,不能跟1进行比较 uint64_t ret = bm->data[n] & (0x1ul << offset); if(ret == 0) { return 0; } else { return 1; } }
在上述代码中的语句:
uint64_t ret = bm->data[n] & (0x1ul << offset);
需要注意两点:
(1)在对0x1进行左移时,因为1是一个整型数据,最多有32位,如果offset的值大于32,就会发生错误,所以要将1强转为无符号长整型(在64位平台下为8字节)
(2)如果需判断的比特位的偏移量大于32,则按位与完后的结果就不能用一个整型数据来进行存放,所以结果ret要用uint64_t类型的数据来进行接收。
上述代码中的获取偏移量函数为:
//将获取指定位所在的数组下标及偏移量封装为函数 void GetOffset(uint64_t index,uint64_t* n,uint64_t* offset) { //首先计算该位在哪个数组元素内(数组元素以64为为一个单元) //计算完之后的n表示该位在下标为n的数组元素内(数组下标从0开始计数) *n = index/(sizeof(BitMapType)*8); //在计算该位在该元素的哪个位上 //计算完之后的offset表示index在下标为的数组元素的哪一位(从0开始计算) *offset = index % (sizeof(BitMapType)*8); return; }
4. 给某一位设置为1
(1)首先要判断该位有没有超过位图所能表示的最大范围,如果超过,则设置失败
(2)没有超过,则计算该位的所在的数组元素下标及偏移量。
(3)通过按位运算将该数组下标的偏移量处的比特位设置为1。
在对该位设置为1时,同时也要保证其他位不变,所以该位可以与1进行按位或运算,其他位与0进行按位或运算,即可达到目的。
根据上述思路,实现代码如下:
//给某一位设置为1 void BitMapSet(BitMap* bm,uint64_t index) { if(bm == NULL || index >= bm->capacity) { //非法输入 return; } //首先计算该位所在的数组下标及偏移量 uint64_t n; uint64_t offset; GetOffset(index,&n,&offset); //再将该位设置为1 //将该位置为1时要保证其他其他位不变,此时就要使其他位与0进行按位或 //因此,该位要与1进行按位或,才能保证将该位置为1,而其他位也能保持不变 //按位操作后的结果不会改变原来的值,所以要对其进行赋值 bm->data[n] = bm->data[n] | (0x1ul << offset); return; }
5. 给某一位设置为0
思路与上述相同:
(1)首先要判断该位有没有超过位图所能表示的最大范围,如果超过,则设置失败
(2)没有超过,则计算该位的所在的数组元素下标及偏移量。
(3)通过按位运算将该数组下标的偏移量处的比特位设置为0。
要将指定位设置为0,则需要将该位与0进行按位与操作,同时保证其他位不变,则其他位与1进行按位与操作。
代码实现如下:
//给某一位设置为0 void BitMapSet0(BitMap* bm,uint64_t index) { if(bm == NULL || index >= bm->capacity) { //非法输入 return; } //首先计算index所在的数组元素下标以及在在该下标处的偏移量 uint64_t n; uint64_t offset; GetOffset(index,&n,&offset); //在根据数组元素下标和偏移量将该位设置为0 //在将该位设置为0时,要保证其他位保持不变 //所以,其他位要与1进行按位与(其他位也可以与0进行按位或但是index所在的为要置为0,只能进行按位与运算) //该位要与0进行按位与 bm->data[n] = bm->data[n] & ~(0x1ul << offset); }
6. 将位图的所有位均置为1
(1)首先要计算该位图共占用了多大字节的内存
(2)利用memset函数将内存中的每个比特位均设置为1,因为该函数是以字节为单位进行设置的,所以要使一个字节的8个比特位均为1,则需要将一个字节设置为0xff。
代码实现如下:
//将位图的所有位均置为1 void BitMapFill(BitMap* bm) { if(bm == NULL) { //非法输入 return; } //将位图所占的内存区域中的所有位均置为1 //使用memset来进行置1,位图有多少字节,就置多少个0xff //首先计算位图中共有多少64为的内存单元 uint64_t size = bm->capacity/(sizeof(BitMapType)*8) + 1; memset(bm->data,0xff,size*sizeof(BitMapType)); return; }
7. 将位图的所有位均置为0
与上述思路相同,只需将0xff改为0即可。
//将位图的所有位均置为0 void BitMapClear(BitMap* bm) { if(bm == NULL) { //非法输入 return; } //将位图中的所有比特位均置为0 //首先,计算位图中共占用了多少个64位的内存单元 uint64_t size = bm->capacity/(sizeof(BitMapType)*8) + 1; //然后按字节将所有的比特位均置为0 memset(bm->data,0x0,size*sizeof(BitMapType)); return; }
8. 销毁位图
因为位图中的内存是动态申请而得,所以,销毁位图时要将其进行释放,同时要将位图所能表示的最大比特位数设置为0.
//销毁位图 void BitMapDestroy(BitMap* bm) { if(bm == NULL) { //非法输入 return; } bm->capacity = 0; free(bm->data); return; }