位图的概念
所谓位图,就是使用比特位表示数据存在与否的一种数据结构,1个比特位表示一个数据的状态(存在/不存在)。而一个字节有8个比特这就意味着一个字节就可以表示8个数据的状态,这将大大节省保存数据的空间。
例如,我要保存1-16这16的数字,通常的做法是申请16个整型空间,将其存储起来,这需要16*4=64个字节的空间。而如果使用位图,则只需要2的字节即可:2个字节有16个比特,每个比特有两种状态(0/1),则16个比特就可以表示这16个整型数字。
但很明显,你若是给两个数:1与10000000,想将他们存储起来,用整型数据只需2*4=8个字节即可,用位图则需要就不合适了,由上面的例子可以轻易看出位图需要10000/8=1250个字节了,这就不合适了。由上面的例子可以轻易看出位图适用于那些密度较大的数据,即适用数据断点不是特别大的情况。
一般来说,位图适用于海量数据,数据无重复的场景。通常是用来判断某个数据存在不存在的。
若现有40000000个不重复的数据,我们使用位图,仅需要5000000个字节即可表示所有数据的存在状态,使用其他数据结构需要的空间比位图需要的空间往往是8倍往上。
位图的简单使用示意
例,现有一组数据A{1,3,4,5,7,8,9,10,13,17,22} 需要表示存在状态,A中的最大数据为22,那么我们需要22/8=2余6,共计3个字节。
所属字节 | 0 | 1 | 2 |
---|---|---|---|
表示范围 | (0,7) | (8,15) | (16,23) |
比特状态 | 0101111 | 11100100 | 01000010 |
从左至右对应 | {0,1,2,3,4,5,6,7} | {8,9,10,11,12,13,14,15} | {16,17,18,19,20,21,22,23} |
位图的应用
- 快速查找某数据是否存在于一个集合中(查探目标数据在bitset中对应比特的状态)
- 排序(因bitset自身表示范围有序,因此将数据映射向bitset后,即完成排序)
- 求两个集合的交集,并集等(两个集合对应两个bitset,交集则是两个比特bitset按位与的结果,并集则是二者按位或的结果)
- 操作系统中磁盘块标记(已占用为1,未占用为0)
位图的实现
位图由于是以比特为单位实现目标,因此位图的所有方法几乎均是位操作。
以下代码是从STL中的bitset
抽出的部分方法,可以基本完成位图的基本功能,已添上注释
#include <iostream>
#include <vector>
using namespace std;
namespace BL {
template <size_t _N>
class bitset {
public:
typedef unsigned long _Ty;//无符号长整型
public:
bitset() { _Tidy(); }
bitset<_N>& set(){
_Tidy(~(_Ty)0);//按位全1
return (*this);
}
bitset<_N>& set(size_t _P, bool _X = true){
if (_X)//设置_P为比特为1
_A[_P / _Nb] |= (_Ty)1 << _P % _Nb;
else//设置_P为比特为0
_A[_P / _Nb] &= ~((_Ty)1 << _P % _Nb);
return (*this);
}
bitset& flip(){ // 所有比特翻转
for (size_t _I = 0; _I <= _Nw; ++_I) {
_A[_I] = ~_A[_I];//按位取反
}
_Trim();
return *this;
}
size_t size() const { return (_N); }
bool test(size_t _P) const{
//第_P位是否为1,是则true
return ((_A[_P / _Nb] & ((_Ty)1 << _P % _Nb)) != 0);
}
bool any() const{
//是否存在非0位
for (int _I = _Nw; 0 <= _I; --_I)
if (_A[_I] != 0)
return (true);
return (false);
}
bool none() const { return (!any());}
size_t count() const{
//计算1的个数
size_t _V = 0;
for (int _I = _Nw; 0 <= _I; --_I)
for (_Ty _X = _A[_I]; _X != 0; _X >>= 4)
_V += "\0\1\1\2\1\2\2\3"
"\1\2\2\3\2\3\3\4"[_X & 0xF];
//"\0\1\1\2\1\2\2\3"
//"\1\2\2\3\2\3\3\4"[_X & 0xF];[]之前的字符串其实就是一个常字符数组
//其具体意义为十进制数_X&0xF对应的二进制中1的个数
return (_V);
}
class reference{
//之所以有reference类是因为bitset会将[]进行重载,以实现修改存在状态等操作
friend class bitset<_N>;
public:
reference& operator=(bool _X){
//第_P位改变状态
_Pbs->set(_Off, _X);
return (*this);
}
reference& operator=(const reference& _Bs){
_Pbs->set(_Off, bool(_Bs));
return (*this);
}
reference& flip(){
//取反
_Pbs->flip(_Off);
return (*this);
}
bool operator~() const{
//是否为0
return (!_Pbs->test(_Off));
}
operator bool() const{
//是否为1
return (_Pbs->test(_Off));
}
private:
reference(bitset<_N>& _X, size_t _P)
: _Pbs(&_X), _Off(_P) {}
bitset<_N>* _Pbs;
size_t _Off;
};
reference operator[](size_t _P) { return (reference(*this, _P)); }
private:
void _Tidy(_Ty _X = 0) {//重置数据为全0
for (int _i = _Nw; _i >= 0; --_i)//置为_X
_A[_i] = _X;
if (_X != 0)//多出的字节置0(大于_N的字节)
_Trim();
}
void _Trim() {
if (_N % _Nb != 0) //仅保留合理范围内的1
_A[_Nw] &= ((_Ty)1 << _N % _Nb) - 1;
}
private:
enum {
_Nb = CHAR_BIT * sizeof(_Ty),//数组单元素所占比特数:32
_Nw = _N == 0 ? 0 : (_N -1)/ _Nb
//计算需要几个4字节空间,之所以_N-1的原因在于:
//_Nw代表可以完全占据一个_Ty空间的数目
//eg.127,(127-1)/32=3余30,_Nw=1
//eg.128,(128-1)/32=3余31,_Nw=1
//之所以使用(_N-1)/_Nb,其思路绝对保证完整_Ty数目,减1之后,
//剩下的数目必然只有两种可能,刚好一个完整Ty/不足一个完整_Ty
//将剩下的数目交给申请数组时处理,有归一化处理的涵义
};
_Ty _A[_Nw+ 1];//加1补不足
private:
friend ostream& operator<<(ostream& _O, const bitset<_N>& _R){
//重载输出符
for (size_t _P = _N; 0 < _P;)
_O << (_R.test(--_P) ? '1' : '0');
return (_O);
}
};
};