哈希即散列是一种数据结构的思想。而哈希表是这种思想的一种应用,除了哈希表,本文要介绍的位图以及布隆过滤器也是哈希思想的体现。
C++的STL已经实现了位图这一种数据结构:
目录
一、概念
如果遇到下面这样的要求:
现有40亿个不重复的无符号整数,没有排序。给一个无符号整数,如何快速判断这个数是否在这40亿个数中。
很多人可能会想用map、set、哈希这样的结构存储这些数据,然后查找。或者用排序+二分查找等等思路。
然而,真正面临的问题是:
无论如何这都是不符合常理的要求。
而应用哈希即散列的思想,我们的目的是判断“在/不在” 两种状态,而一个bit位的0/1值完全可以满足要求,使用bit位来存储状态,用于判断在与不在的数据结构就是位图。通常用于海量数据,数据无重复的场景。
二、位图的实现
位图中比较重要的成员函数
set 将存在的数据对应位设置为1 reset 将不存在的数据对应位设置为0 test 判断是否存在,返回值为bool类型
1.框架
template<size_t N> class bitset { public: bitset() { _bits.resize(N / 32 + 1, 0); } private: vector<int> _bits; };
成员变量是一个vector<int>,我们打算用整型的每一位来达到需求。
对于这个数组开多大的空间,如果有64个数据,则用64个bit位即两个字节即可。如果有70个数据则需要多开一个字节的空间。
2.set
//将x对应vector中的bit位改为1 void set(size_t x) { assert(x <= N) { size_t i = x / 32; size_t j = x % 32; _bits[i] |= (1 << j); } }
代码中:
除32对应到第i个字节,模32对应到第i个字节的第j个比特位。而不影响其他位的情况下,要将对应比特位修改为1,需要将1左移(考虑到大小端存储,我们平常所说的左移本质是向高位移动),或运算1将比特位修改为1,对应的,与运算0将比特位修改为0。
3.reset
//将对应位修改为0 void reset(size_t x) { assert(x <= N); size_t i = x / 32; size_t j = x % 32; _bits[i] &= ~(1 << j); }
4.test
bool test(size_t x) { assert(x <= N); size_t i = x / 32; size_t j = x % 32; return _bits[i] & (1 << j); }
_bits[i]是整个字节,而我们只需要一个bit位,将一个bit位转换为一个类型只需要按位与1即可。
三、位图的应用
现有100亿个整数,设计算法找到只出现一次的整数。
不难知道,size_t 的最大值范围是42亿左右,100亿个整数必然有重复值。利用位图的特点,为了达到需求,某一个数,可能的状态是出现0次、出现1次、出现2次及以上,而我们关注的是它出现1次的情况,因此只需要3个bit状态即可达到需求,使用两个bit位即可。
00 ------- 出现0次
01 ------- 出现1次
10 ------- 出现2次不止
template<size_t N> class two_bit_set { public: void set(size_t x) { if (_bs1.test(x) == false && _bs2.test(x) == false) { _bs2.set(x); } else if (_bs1.test(x) == false && _bs2.test(x) == true) { _bs1.set(x); _bs2.reset(x); } else { } } bool test(size_t x) { if (_bs1.test(x) == false && _bs2.test(x) == true) { return true; } return false; } private: bitset<N> _bs1; bitset<N> _bs2; };
现有两个文件,分别有100亿个整数,只有1G内存,如何找到它们的交集
存放100亿个整型,大约需要40G。将这些存储下来是不现实的,一般对于大量数据的处理,用位图来处理。实际的整型数据量在42亿左右,而42亿bit的大小为512MB,因此1GB的空间用来开两个大小为512MB的位图,依次遍历两个位图,比特位同时为1的时候说明这个数据属于交集。