位图的引入
1.对于下面的题
- 给40亿个不重复的无符号整数,没排过序。任给一个无符号整数,如何快速判断一个数是否在这40亿个数中。 【腾讯】
2.分析
(1)我们首先可以想到将这40亿个整数先进行排序,然后再进行二分查找;
(2)但是40亿个整数在内存中大约占16G的内存;
(3)我们一般的计算机的内存很难达到16G,也就是说如果没有特殊的方式,我们无法在计算机的内存里存放这么多的数据;
(4)所以对于上面的想法很难实现,我们需要思考其它的方式;
(5)由题目,我们只用知道一个数是否存在这40亿个数,在或者不在可以用1个比特位来表示(0表示不在,1表示在)。所以可以将一个数用一个位来进行表示,这样一个数以前用32位表示,现在就缩短到用一个位来表示,这样的话,对于40亿个整数现在只用500M就可以存下,大大缩短了空间。
(6)这种用位来表示一个数在或是不在的数据结构称为位图,位图利用的是哈希直接定址法的思想。
自己模拟实现一个位图(BitSet)
1.位图的数据结构可以用数组来实现,STL里面有一个容器是vector就是动态增长的数组,所以位图的底层就用一个vector。
class BitSet
{
public:
private:
vector<size_t > _bits;
};
2.构造函数
- 构造函数需要为BitSet开空间,并且将里面的数据都清零(也就是将位图的每一位都置0)
- 我们知道vector里面用两个函数可以用来开辟空间,一个是reserve ,一个是resize。(reserve只改变容量不改变size,一般和push_back搭配使用;而resize不仅改变容量,也会改变size),所以这里用resize。
- 如果想要开辟range个位,因为当前位图用vector实现,而vector里面的数据为size_t类型,也就是1个size_t类型可以存放32个位,range个比特位就需要开range/32个size_t的空间,为了防止当range<32时,range/32=0的情形,所以需要多开一个size_t的空间
综上,构造函数如下:
BitSet(size_t range)
{
_bits.resize(range / 32 + 1, 0); //resize开range/32+1的空间,并且将所有的数据初始化为0
}
3.Set(将位图的某一位设置为1,也就是将给定的数x对应的位设为1)
- 将某一位设置成1,首先要知道该位 位于位图的哪个位置上(即该位对应的数组的下标);(因为该数组一个位置对应32个位,所以用该数除32可以算出该位对应数组的下标是哪一个);
- 然后还要知道该位对应数组那个位置的哪一位上;
- 如果要将某一位设置为1,由逻辑运算符知道,1|x=1(1与任意数按位或都是1)0|x=x(0与任意数按位或都是任意数),则我们可以将该位与1进行按位或,但是又不能影响别的位(即别的位可以或上0);
- 如果将pos位变成1其它位不变,则将1左移pos位,再或等上数组_bits对应位置上的数。(对应位置:该位所在的数组的下标)。
void Set(size_t x)
{
size_t num = x / 32; //计算x位于_bits的哪个下标处
size_t pos = x % 32; //计算x位于对应下标的哪一位上
_bits[num] |= (1 << pos);
}
4.Reset(将位图的某一位设置为0,也就是说将该位对应的数删除)
- 仍然需要先算出该位对应数组的哪个位置;
- 如果要将某一位变为0,则可以将该位 按位与上0,但是其它位不变,所以其它位应与上1;
- 如何将pos位变为0,其它位变为1呢?(可以将1左移pos位,就会使得pos位为1,其它位为0,然后再将左移后的数按位取反,则pos位为0,其它位为1.
void ReSet(size_t x)
{
size_t num = x / 32;
size_t pos = x % 32;
_bits[num] &= ~(1 << pos);
}
5.Test(测试某个数是否存在,也就是测试该数对应的位是0还是1)
- 如果该数对应的位为1说明该数存在,为0说明不存在在
- 因为 1&1=1,1&0=0,所以可以将该数对应的位与上1,将其它位与上0,则结果为0说明当前位为0即表示该数不在,不为0说明该数存在
bool Test(size_t x)
{
size_t num = x / 32;
size_t pos = x % 32;
return _bits[num] & (1 << pos);
}
6.总的代码如下:
#include<vector> //注意用到vector,就必须包含vector的头文件
class BitSet
{
public:
BitSet(size_t range)
{
_bits.resize(range / 32 + 1, 0);
}
void Set(size_t x)
{
size_t num = x / 32; //计算x位于_bits的哪个下标处
size_t pos = x % 32; //计算x位于对应下标的哪一位上
_bits[num] |= (1 << pos);
}
void ReSet(size_t x)
{
size_t num = x / 32;
size_t pos = x % 32;
_bits[num] &= ~(1 << pos);
}
bool Test(size_t x)
{
size_t num = x / 32;
size_t pos = x % 32;
return _bits[num] & (1 << pos);
}
private:
vector<size_t > _bits;
};
void TestBitSet()
{
BitSet s(1000);
s.Set(100);
s.Set(15);
s.Set(34);
s.Set(815);
cout << s.Test(815) << endl;
cout << s.Test(34) << endl;
cout << s.Test(69) << endl;
cout << s.Test(125) << endl;
cout << s.Test(150) << endl;
}
C++标准库中位图的使用(bitset)
1.在C++标准库里位图(bitset)是一个容器,采用模板实现,有一个非类型的模板参数,这个非类型的模板参数用来表示我们需要的比特位的位数;
2.使用:
(1)bitset的定义及初始化
bitset<4> bs1; //位图bs1包含4个位,每个位初始值为0
bitset<4> bs2(11); //bs2包含4个位,由于11的二进制为1011,该二进制的低位对应位图的低位置处,高位对应位图的高位置处,即位图的第0位为1,第一位为1,第2位为0,第3位为1
bitset<4> bs3("0011"); //可以将一个string对象初始化一个位图,string对象的值与位图位置相反,string的最右边的字符对应位图的最低位,最左边的字符对应位图最高位
string s = "1111";
bitset<4> bs4(s);
bitset<17> bs5(0xffff); //在32位机器上,0xffff为16进制数,表示16个1,所以会将bs5里0~15位都置为1,第16位为0
string s2 = "00110011110";
bitset<32> bs6(s2, 3, 4); //bs6有32个位,将s2从第3位(从0开始)开始包含4个字符用来初始化bs6的0~3位
(2)将某位设置为1 (set( ))
bitset<8> bs1; //bs1包含8个位,第0~第7位,初始时每位都为0
bs1.set(); //set里面不带任何参数,则会将所有位都设置位1
bitset<8> bs2;
bs2.set(5); //将bs2的第5位设置为1
(3)将某位设置为0 (reset( ))
bitset<8> bs1("00011111");
bs1.reset(3); //将bs1的第3位设置为0
(4)测试某一位是1还是0 (test())
bitset<8> bs1("00011111");
bs1.reset(3);
cout << bs1.test(3) << endl; //输出0
cout << bs1.test(2) << endl; //输出1
(5)统计位图里面为1的个数
bitset<8> bs1("00011111");
cout << bs1.count() << endl; //输出5
(6)统计位图的size
bitset<8> bs1("00011111");
cout << bs1.size() << endl; //输出8
(7)访问位图的某一位(通过[ ])
bitset<8> bs1("00011111");
cout << bs1[3] << endl;
(8)按位取反 (flip( ))
bitset<8> bs1("00011111");
bs1.flip(); //不带参数,表示将所有位都按位取反
bs1.flip(3); //表示将bs1的第三位按位取反
(9)输出该位图