1. 定义:
从字面意思上理解,bitmap翻译为位图,更准确地说应该是位的映射。bitmap一般应用于海量数据的处理,如查找、去重、排序。
举个例子:40亿个int数字中,要找出只出现一次的数字集合。用普通的遍历查找的话,40亿int需要的内存空间是:40*10^8*4 = 16*10^9,即16GB的空间(ps:1GB大约是10亿字节)。对于一般计算机而言,内存大约2-8G,很明显无法存储16GB的数据。如果用存磁盘的方式分次加载,需要大量的I/O消耗,性能很差。这时候,就要使用bitmap了,其核心思想是:一个byte占8个bit,如果用一个bit表示一个int数字的值,即0表示这个数不存在,1表示整个数存在,那么一个byte就能表示8个int数字,一个int空间就能表示32个int数字。如下图所示:
这样的话,原本一个int数占32bit,现在只占1bit,即节省了32倍的空间。所以现在只需要16GB/32=512MB的内存空间,即只需要申请int bits[N/32 + 1]的空间就可存储数据,其中N表示这些数据中最大的数值数,此为2^32。此外,由于这些数字之间没有关联性,不需要同步处理,所以使用多线程的方式读取和加载数据可以实现更高的性能,时间复杂度大约是O(N/n),n表示线程数。
注意:要说明的是,当有N个int数字用bitmap的方式存储时,如果N个int数字的数值都在0-N的范围内,那么使用bitmap可以节省32倍的内存空间;如果N个int数字的数值是0-MAXINT(即2^32)的范围,那么使用bitmap需要512MB的内存来存放所有的数,这样的话如果N小于1.25亿使用bitmap反倒多消耗了内存,只有N大于1.25亿才会节省内存,节省的内存倍数是:(4*N)B/512MB,如N为10亿int时,节省内存(4*10*10^8)B/512MB = 8。
2. 具体方法:
接下来谈一下bitmap的实现方式。申请int bits[N/32+1]的空间后,一个int数如何定位到其索引位置及如何存放到bits数组中?如给定一个数33,我们知道应该将其放入bits[1]的第二个bit位置。
(1)确定数组索引:使用数字除以32,即:num/32,也可写:num>>5
(2)确定32位bit中的位置:使用数字对32取模,即:num%32,也可写为:num & 0x1F
(3)数字存入bits中:bits[num/32] |= (1<<(num%32)),即:bits[num>>5] |= (1<<(num&0x1F))
(4)数字从bits清除:bits[num/32] &= ~(1<<(num%32)),即:bits[num>>5] &= ~(1<<(num&0x1F))
(5)判断数字是否在bits中:return ( bits[num/32] &= (1<<(num%32)) ) != 0 )
3. 代码实现:
此处C++代码默认N个int数字的数值范围在0-N中,即bitmap可以节省32倍的内存空间。
#include <iostream> #include <vector> #include <cstring> using namespace std; class Bitmap { public: Bitmap(int N) :capacity(N) //使用bitmap节省空间 { bits = new int[(capacity>>5)+1]; for(int i=0; i<=(capacity>>5); ++i) bits[i] = 0; } ~Bitmap() { delete [] bits; } void Add(int num) //添加数字num到bits中 { bits[num>>5] |= (1<<(num&0x1F)); } void Clear(int num) //清除bits中的数字num { bits[num>>5] &= ~(1<<(num&0x1F)); } string IsContain(int num) //判断num是否在bits中 { return (( bits[num>>5] & (1<<(num&0x1F)) ) != 0) ? "YES" : "NO"; } void Sort() //对bits中的数排序,时间复杂度O(capacity) { int cnt = capacity>>5; //确定bits中的最大索引数 while(cnt >= 0) { for(int i=31; i>=0; --i) { if( (bits[cnt] & (1<<i)) != 0) sortRes.push_back(cnt*32+i); } --cnt; } } void PrintSortRes() //打印排序后的结果 { cout<<"Sort :"; for(auto i:sortRes) cout<<i<<" "; cout<<endl; } private: int capacity; //存储的int数据个数 int *bits; //指向存放数据的数组 vector<int> sortRes; //存放排序结果 }; int main() { Bitmap bm(100); bm.Add(4); bm.Add(37); bm.Add(99); cout<<"4 in bits? "<<bm.IsContain(4)<<endl; cout<<"37 in bits? "<<bm.IsContain(37)<<endl; cout<<"99 in bits? "<<bm.IsContain(99)<<endl; cout<<"89 in bits? "<<bm.IsContain(89)<<endl; bm.Clear(4); cout<<"4 in bits? "<<bm.IsContain(4)<<endl; vector<int> SortRes; bm.Sort(); bm.PrintSortRes(); return 0; }
4. 扩展:
如果要找出上亿整数中重复的数(多次添加的数)个数,可以用2-bitmap,即用2bit表示一个整数,00表示未出现,01表示出现一次,10表示出现多次。在遍历这些数时,如果对应位置是0则置为1,如果是1则置为2,如果是2则保持不变。遍历的同时统计对应位置为2的个数即为答案。