目录
概念引入
给40亿个不重复的无符号整数,且为乱序,给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
常规的想法是将40亿个数存入数组中,快速排序然后采用二分法查找,时间复杂度O(NlogN).或者是将一堆数存入unordered_set容器中,使用find进行查找,时间复杂度为O(N)。
但是内存空间存不下这么多的数字:
40亿≈2
32=2
2 * 2^10 * 2^10 * 2^10=4 * 1024 * 1024 * 1024 = 4G
每个整数占4字节,所以40亿个整数占用16G字节的空间
而一台32位机的内存地址有32位,每个地址指向1字节(8bits)的数据,其内存地址空间的大小只有2^32=4G字节的空间大小,由于系统本身要占据一部分内存,我们可用的内存只有3.2G左右。
如果我们直接按整型的方式40亿个整数存入内存显然是不可能的事。
于是我们需要使用位图(bitset)来解决:
这个问题只需要考虑数字在或者不在,那么我们仅需要0,1来表示其存在性(0表示不在,1表示存在),而使用二进制比特位来标记该数字的位置:
那么每个数的是否存在仅用一个bit位就能表示了。
40亿个数的状态占用2^32个bit=4 * 1024 * 1024 * 1024 bit=0.5G ,显然内存占用变小了很多。
所谓位图,就是用比特位来存放数据的状态,适用于海量数据,数据无重复的场景,通常用于判断数据存不存在。
bitset类
标准库提供了位图:
该类模拟了bool类型元素的数组,但是每个元素只占用一个bit位,一个char可以存放8个元素。
每一个位的位置可以单独访问,例如一个bitset对象foo,可以类似于数组使用下标访问的方式:foo[ 3 ],来访问第四个元素。
模板参数N表示元素的数量。
bitset的构造
-
(1)默认构造函数
所有元素都初始化为0 -
(2)使用整型初始化
元素按照给定整数的二进制位进行初始化:0xff ——> 1111 1111 -
(3)字符串初始化
使用01的string进行初始化:std::string(“01101001”) ——> 01101001使用01的字符串进行初始化:(“01101001”) ——> 01101001
-
案例
:
#include <iostream> // std::cout
#include <string> // std::string
#include <bitset> // std::bitset
int main ()
{
std::bitset<16> foo;
std::bitset<16> bar (0xfa2);
std::bitset<16> baz (std::string("01101001"));
std::bitset<16> baz2("01101001");
std::cout << "foo: " << foo << '\n';
std::cout << "bar: " << bar << '\n';
std::cout << "baz: " << baz << '\n';
std::cout << "baz2: " << baz2 << '\n';
return 0;
}
下标访问 operator[]
返回pos位的元素值或者引用,但是不执行范围检查,建议使用bitset::test ,其会检查边界。
-
案例
:
int main ()
{
std::bitset<4> foo;
foo[1]=1; // 0010
foo[2]=foo[1]; // 0110
std::cout << "foo: " << foo << '\n';
return 0;
}
返回bit位值 test
int main()
{
std::bitset<5> foo(std::string("01011"));
std::cout << "foo contains:\n";
//运行下面的语句,将bool值从0,1转化为false,true
std::cout << std::boolalpha;
for (std::size_t i = 0; i < foo.size(); ++i)
std::cout << foo.test(i) << '\n';
}
当使用boolalpha后,以后的bool类型结果都将以true或false形式输出,除非使用noboolalpha取消 boolalpha流的格式标志。
设置比特位 set
-
(1)把所有比特位都设置为1
-
(2)把pos处的比特位设置为1(默认true),或者第二个参数给为0(或者false),则可将pos位置为0.
-
案例
:
int main()
{
std::bitset<4> foo;
std::cout << foo.set() << '\n'; // 1111
std::cout << foo.set(0, 0) << '\n'; // 1110
std::cout << foo.set(2, false) << '\n'; // 1010
std::cout << foo.set(0,1) << '\n'; // 1011
std::cout << foo.set(2,true) << '\n'; // 1111
return 0;
}
重置比特位 reset
-
(1)将所有bits重置为0
-
(2)pos位置为0
-
案例
int main()
{
std::bitset<4> foo(std::string("1011"));
std::cout << foo.reset(1) << '\n'; // 1001
std::cout << foo.reset() << '\n'; // 0000
return 0;
}
计算元素数量 size/count
size返回元素的总数量,即模板参数中我们给的N。
count 返回元素中被set的元素数量。
-
案例
:
int main()
{
std::bitset<8> foo("01101001");
std::cout << foo << " has ";
std::cout << foo.count() << " ones and ";
std::cout << (foo.size() - foo.count()) << " zeros.\n";
return 0;
}
翻转位 flip
- (1)翻转所有位
- (2)翻转pos位
int main ()
{
std::bitset<4> foo (std::string("0001"));
std::cout << foo.flip(2) << '\n'; // 0101
std::cout << foo.flip() << '\n'; // 1010
return 0;
}
测试bitset对象中是否已有bite位被set —— any,none,all
如果至少有一个bit位已设置,any返回true,否则none返回真.如果所有bit位都已set,则all返回true。
int main()
{
std::bitset<16> foo;
std::cout << "输入二进制数:";
std::cin >> foo;
if (foo.any())
std::cout << foo << " has " << foo.count() << " bits set." << std::endl;
else
std::cout << foo << " has no bits set." << std::endl;
foo.reset();
if (foo.none())
std::cout << foo << " has no bits set." << std::endl;
foo.set();
if(foo.all())
std::cout << foo << " all bits set." << std::endl;
return 0;
}
bitset的运算符重载
int main ()
{
std::bitset<4> foo (std::string("1001"));
std::bitset<4> bar (std::string("0011"));
std::cout << (foo^=bar) << '\n'; // 1010 (XOR,assign)
std::cout << (foo&=bar) << '\n'; // 0010 (AND,assign)
std::cout << (foo|=bar) << '\n'; // 0011 (OR,assign)
std::cout << (foo<<=2) << '\n'; // 1100 (SHL,assign)
std::cout << (foo>>=1) << '\n'; // 0110 (SHR,assign)
std::cout << (~bar) << '\n'; // 1100 (NOT)
std::cout << (bar<<1) << '\n'; // 0110 (SHL)
std::cout << (bar>>1) << '\n'; // 0001 (SHR)
std::cout << (foo==bar) << '\n'; // false (0110==0011)
std::cout << (foo!=bar) << '\n'; // true (0110!=0011)
std::cout << (foo&bar) << '\n'; // 0010
std::cout << (foo|bar) << '\n'; // 0111
std::cout << (foo^bar) << '\n'; // 0101
return 0;
}
自己实现一个bitset
如果我们以一个整型作为比特位的容器,那么如果要求0~N范围的比特位,就需要有
N/32+1
个整型来容纳这些比特位,同理如果以char为容器,则需要
N/8+1
个char来容纳N个比特位。这里我们用char数组作为底层容纳比特位的容器。
namespace mystd
{
template<size_t NUM>
class bitset
{
public:
bitset()
{
_bits.resize(NUM/8+1, 0);
}
void set(size_t pos)//设置指定位或者所有位
{}
void reset(size_t pos)//清空指定位或者所有位
{}
bool test(size_t pos)//获取指定位状态
{}
void flip(size_t pos)//翻转指定pos
{}
size_t count()//统计set的位数
{}
bool none()//没有bit为被set返回true
{}
bool any()//至少一个bit位set返回true
{}
bool all()//全部NUM个bit位被set返回true
{}
private:
vector<char> _bits;//位图
};
}
set reset test flip
- set是将pos位的0置为1。
首先通过 i=pos/8 找到bit位在第i个char中,再通过 j=pos%8 找到在这个char的第j位bit位上。将1左移j位后于这个char
按位或运算
即可。
void set(size_t pos)//设置指定位或者所有位
{
size_t i = pos / 8;
size_t j = pos % 8;
_bits[i] |= (1 << j);
}
-
reset 将pos置0也是同理,将1左移j位,按位取反,再和这个char
按位与运算
即可:
void reset(size_t pos)//清空指定位或者所有位
{
size_t i = pos / 8;
size_t j = pos % 8;
_bits[i] &= (~(1 << j));
}
-
test获取位的状态,将1左移j位,再和这个char
按位与运算
即可:
bool test(size_t pos)//获取指定位状态
{
size_t i = pos / 8;
size_t j = pos % 8;
return _bits[i] & (1 << j);
}
- flip翻转位
- 计算出该位位于第 i 个char的第 j 个比特位。
-
将1左移 j 位后与第 i 个char进行
按位异或运算
即可。
void flip(size_t pos)
{
size_t i=pos/8;
size_t j=pos%8;
_bits[i]^=(1<<j);
}
count
count 获取位图中被设置的位的个数,遍历每一个char,统计每个char中的二进制位1的个数
统计二进制中1的个数的方法:
- n = n & (n-1) => 消去n的二进制数中最右边的1;
- n不为零继续执行第一步;
- 执行了几次说明n中有多少个1;
size_t count()
{
size_t count;
for(auto e:_bits)
{
int n=e;
while(n)
{
n=n&(n-1);
count++;
}
}
return count;
}
none any all
-
none
遍历每一个char 如果全为0,则none返回true
bool none()
{
//遍历每个char
for (auto e : _bits)
{
if (e != 0) //该整数中有位被设置
return false;
}
return true; //全部整数都是0,则没有位被设置过
}
-
any
是none的逆操作
bool any()
{
return !none();
}
-
all
每一个char中的二进制位全为1,除了最后一个char的剩余的比特位。所以需要分情况:
bool all()
{
size_t size = _bits.size();
for(size_t i = 0;i < size-1;++i)
{
if(~_bits[i]!=0)//取反应该为0,否则取反之前不全为1
return false;
}
for(size_t j = 0;j < NUM % 8 ;++j)
{
if((_bits[size-1] & (1<<j))==0)
{
return false;
}
}
return true;
}
应用
100亿个整数
给定100亿个整数,设计算法找到只出现1次的整数?
100亿个整数,占用40GB内存,如使用位图将占用512MB内存(因为整型为4字节,不会超过32位,2^32bits=512MB)。整数的出现次数有三种状态
- 出现0次
- 出现1次
- 出现2次及以上
由于一个bit位只能记录两种状态,所以这里需要两个bit位来记录3种状态。我们设计一个类其中封装两个bitset
template<size_t N>
class TwoBitset
{
public:
void set(size_t pos)
{
if (!_bs1.test(pos) && !_bs2.test(pos))//00 -> 01
{
_bs1.set(pos);
}
else if (_bs1.test(pos) && !_bs2.test(pos))//01->10
{
//出现次数置为2
_bs1.reset(pos);
_bs2.set(pos);
}
//10 表示已经出现两次或以上,不用处理
}
//找出 出现次数为1(01)的整数,即只出现一次
void PrintOnceNum()
{
for (size_t i = 0; i < N; ++i)
{
if (_bs1.test(i))
{
cout << i << ' ';
}
}
}
private:
std::bitset<N> _bs1;
std::bitset<N> _bs2;
};
文件交集
给你两个文件,每个文件分别有100亿个整数,1G可用内存,如何找到两个文件的交集?
答1
:使用哈希函数将第一个文件中的所有整数映射到1000个文件中,如何哈希函数分配合理,那么每个文件几乎可以均匀的分到1000万个整数(4Bytes * 10 * 1000 * 1000 =40MB),将这1000个文件记为a1,…,a1000。同理将第二个文件也利用哈希函数均分1000个文件记为b1,…,b1000。由于是使用同一个哈希函数,两个文件一样的整数会被分到文件下标一致的文件中,那么分别对a1和b1求交集,…,a1000和b1000求交集,每次比较只会占用80MB内存,最后将交集的结果汇总即可。
答2
:还是使用位图的方法,分别使用两个位图记录整数是否出现。最后将遍历两个位图,每位按位与运算,如果结果为true则说明为交集整数。
100亿个整数 1G可用内存
1个文件有100亿个int,1G可用内存,设计算法找到出现次数不超过两次的所有整数
两个位图
- 出现0次 :00
- 出现1次 :01
- 出现2次 :10
- 出现3次及以上 :11
这里出现了四种状态,需要两个位图即可解决;
以此类推如果题目要求为找出不超过5次的整数,那么3个位图可描述8个状态,可以应付这种要求。
— end —
青山不改 绿水长流