浅谈bitmap

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的个数即为答案。
原文地址:https://www.cnblogs.com/ladawn/p/8450235.html