文章目录
引入
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
要判断一个数是否在某一堆数中,我们可能会想到如下方法:
- 将这一堆数进行排序,然后通过二分查找的方法判断该数是否在这一堆数中。
- 将这一堆数插入到unordered_set容器中,然后调用find函数判断该数是否在这一堆数中。
单从方法上来看,这两种方法都是可以,而且效率也不错,第一种方法的时间复杂度是O(NlogN),第二种方法的时间复杂度是O(N)。
但问题是这里有40亿个数,若是我们要将这些数全部加载到内存当中,那么将会占用16G的空间,空间消耗是很大的。因此从空间消耗来看,上面这两种方法实际都是不可行的。
1.位图的介绍
实际在这个问题当中,我们只需要判断一个数在或是不在,即只有两种状态,那么我们可以用一个比特位来表示数据是否存在,如果比特位为1则表示存在,比特位为0则表示不存在。
无符号整数总共有232个,因此记录这些数字就需要232个比特位,也就是512M的内存空间,内存消耗大大减少。
1.1位图的概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
1.2位图的应用
常见位图的应用如下:
1. 快速查找某个数据是否在一个集合中。
2. 排序。
3. 求两个集合的交集、并集等。
4. 操作系统中磁盘块标记。
5. 内核中信号标志位(信号屏蔽字和未决信号集)。
1.3bitset的基本使用
bitset的定义方式
方式一: 构造一个16位的位图,所有位都初始化为0。
bitset<16> bs1; //0000000000000000
方式二: 构造一个16位的位图,根据所给值初始化位图的前n位。
bitset<16> bs2(0xfa5); //0000111110100101
方式三: 构造一个16位的位图,根据字符串中的0/1序列初始化位图的前n位。
bitset<16> bs3(string("10111001")); //0000000010111001
bitset成员函数的使用
bitset中常用的成员函数如下:
成员函数 | 功能 |
---|---|
set | 设置指定位或所有位 |
reset | 清空指定位或所有位 |
flip | 反转指定位或所有位 |
test | 获取指定位的状态 |
count | 获取被设置位的个数 |
size | 获取可以容纳的位的个数 |
any | 如果有任何一个位被设置则返回true |
none | 如果没有位被设置则返回true |
all | 如果所有位都被设置则返回true |
具体用法可以用的时候再查询文档 bitset使用文档
2.位图的基本模拟实现
2.1基本结构
template<size_t N>
class bitset
{
public:
// 构造函数
bitset();
// x映射的位标记成1
void set(size_t x);
// x映射的位标记成0
void reset(size_t x);
// x映射的位是1返回真,0返回假
bool test(size_t x);
private:
vector<int> _bs;
};
2.2构造函数
在构造位图时,我们需要根据所给位数N,创建一个N位的位图,并且将该位图中的所有位都初始化为0。
一个整型有32个比特位,因此N个位的位图就需要用到N/32个整型,但是实际我们所需的整型个数是N/32+1,因为所给非类型模板参数N的值可能并不是32的整数倍。
例如,当N为40时,我们需要用到两个整型,即40/32+1=2。
bitset()
{
// 扩容,不管能否整除都多开一个空间
_bs.resize(N / 32 + 1);
}
2.3set函数
设置位图中指定的位的方法如下:
计算出该位位于第 i 个整数的第 j 个比特位。
将1左移 j 位后与第 i 个整数进行或运算即可。
// x映射的位标记成1
void set(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bs[i] |= (1 << j);
}
2.4reset
清空位图中指定的位的方法如下:
计算出该位位于第 i 个整数的第 j 个比特位。
将1左移 j 位再整体反转后与第 i 个整数进行与运算即可。
// x映射的位标记成0
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bs[i] &= (~(1 << j));
}
2.5test
获取位图中指定的位的状态的方法如下:
计算出该位位于第 i 个整数的第 j 个比特位。
将1左移 j 位后与第 i 个整数进行与运算得出结果。
若结果非0,则该位被设置,否则该位未被设置。
// x映射的位是1返回真,0返回假
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _bs[i] & (1 << j);
}
3.位图考察题目
- 给定100亿个整数,设计算法找到只出现⼀次的整数?
虽然是100亿个数,但是还是按范围开空间,所以还是开2^32个位,跟前面的题目是⼀样的。 - 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
把数据读出来,分别放到两个位图,依次遍历,同时在两个位图的值就是交集 - ⼀个文件有100亿个整数,1G内存,设计算法找到出现次数不超过2次的所有整数 ?
根据上面的题意,我们需要通过位图实现一个能表示出现0次,1次,2次,多次的位图,因此可以实现一个两个位图成员的类。
namespace twobt
{
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool bit1 = _bs1.test(x);
bool bit2 = _bs2.test(x);
if (!bit1 && !bit2) // 00->01
{
_bs2.set(x);
}
else if (!bit1 && bit2) // 01->10
{
_bs1.set(x);
_bs2.reset(x);
}
else if (bit1 && !bit2) // 10->11
{
_bs1.set(x);
_bs2.set(x);
}
}
// 返回0 出现0次数
// 返回1 出现1次数
// 返回2 出现2次数
// 返回3 出现2次及以上
int get_count(size_t x)
{
bool bit1 = _bs1.test(x);
bool bit2 = _bs2.test(x);
if (!bit1 && !bit2)
{
return 0;
}
else if (!bit1 && bit2)
{
return 1;
}
else if (bit1 && !bit2)
{
return 2;
}
else
{
return 3;
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
}
3.1只出现⼀次的整数?
代码演示:
void test_twobitset1()
{
lin::twobitset<100> tbs;
int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6,6,6,6,7,9 };
for (auto e : a)
{
tbs.set(e);
}
// 只出现一次的整数
for (size_t i = 0; i < 100; ++i)
{
if (tbs.get_count(i) == 1)
{
cout << i << endl;
}
}
}
3.2找到两个文件交集?
代码演示
void test_bitset()
{
int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
int a2[] = { 5,3,5,99,6,99,33,66 };
bitset<100> bs1;
bitset<100> bs2;
for (auto e : a1)
{
bs1.set(e);
}
for (auto e : a2)
{
bs2.set(e);
}
// 两个文件的交集
for (size_t i = 0; i < 100; i++)
{
// 第i的位置都是1就是交集
if (bs1.test(i) && bs2.test(i))
{
cout << i << endl;
}
}
}
3.3出现次数不超过2次的所有整数(出现1次和2次)?
代码演示
void test_twobitset2()
{
lin::twobitset<100> tbs;
int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6,6,6,6,7,9 };
for (auto e : a)
{
tbs.set(e);
}
// 出现次数不超过2次的所有整数
for (size_t i = 0; i < 100; ++i)
{
//cout << i << "->" << tbs.get_count(i) << endl;
if (tbs.get_count(i) == 1 || tbs.get_count(i) == 2)
{
cout << i << endl;
}
}
}
总结
位图的优缺点:
优点:增删查改快,节省空间
缺点:只适用于整形
本篇博客到此结束,欢迎各位评论区留言~