文章目录
一、面试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】
- 遍历,时间复杂度O(N)
- 排序(O(NlogN)),利用二分查找: logN (230 =30亿,大概找32次即可找到)
1G空间=1024* 1024 * 1024Byte=10亿Byte
40亿个整数=40亿个int = 160亿Byte=16G内存
这在内存中一次能够放的下吗?
答案显而易见
如果使用哈希表或者红黑树来存储这些数字,那么附带的消耗也是巨大的
- 位图解决
位图是一种直接定址法的哈希,因此效率很高,用O(1)就可以探测到对应位是0还是1,效率非常高,因此可以快速判断
数据是否在给定的整形数据中,结果是在(1)或者不在(0),刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。
40亿个整数=我们要开40亿个比特位
40亿bit = 40亿b / 8 /10亿 = 0.5G=500M 空间
可想而知,这是多么的省空间
但是我们这里要开42亿个比特位!这是为啥呢?
因为我们采用的直接定址法,要是我开40亿个位置,注意unsigned int范围:0-42亿,那么万一有个数是MAX,岂不是没有办法映射了。
咱们C++开空间是怎么开的?
是不是要先确定一个类型,才能开,那么最小的类型就是char 了
我们可以控制这个char,来实现控制bit位
std::vector<char> _bits;
二、位图核心函数的实现
set 设置指定位或所有位
reset 清空指定位或所有位
test 获取指定位的状态
简单的使用:
#include <iostream>
#include <bitset>
using namespace std;
int main()
{
bitset<8> bs;
cout << bs << endl; // 00000000
bs.set(1); // 设置第1位
bs.set(7); // 设置第7位
cout << bs << endl; // 10000010
cout << bs.test(1) << endl; // 看看第1位是啥 1
cout<< bs.test(2) << endl; // 看看第2位是啥 0
cout << bs.test(7) << endl; // 看看第7位是啥 1
cout << bs << endl; // 10000010
bs.reset(1); // 清空第1位
cout << bs << endl; // 10000000
return 0;
}
如何把第j位置为1?其他位都是0
测试:
所以:
第一个char 0010 0000
第二个char 0000 0100
第三个char 0001 0000
如何把第j位 置为0?其他位都不变
最后只剩下2了
如何检测第j位 为0还是1?
模拟实现
namespace sjj
{
template<size_t N>
class bit_set
{
public:
bit_set()
{
_bits.resize(N / 8 + 1);// 比如我要开10个比特位 N=10 , 10/8=1 那么很明显不够,那么我们就多加1,现在相当于我们有2个char,16比特位
}
//
/// 三个核心函数接口
//
void set(size_t x)
{
size_t i = x / 8; // 确定它在哪个char上
size_t j = x % 8; // 确定它在该char的具体第几个位置
_bits[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= (~(1 << j));
}
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
std::vector<char> _bits;
};
void test_bit_set()
{
bit_set<100> bs;
bs.set(5);
bs.set(10);
bs.set(20);
bs.set(1);
cout << bs.test(5) << endl;
cout << bs.test(20) << endl;
bs.reset(20);
bs.reset(10);
bs.reset(5);
cout << bs.test(5) << endl;
cout << bs.test(20) << endl;
}
}
解决开头的面试题
首先文件读取,将这40亿个数分批次读取到内存(fstream),然后设置到我们的位图结构中
这样开空间:
bit_set<0xffffffff> bs;
bit_set<-1> bs; // -1存的是补码,原码是全1,就是最大值
三、位图的应用
1、给定100亿个整数,设计算法找到只出现一次的整数?
位图的变形题
100亿个整数=100亿个int=40G内存
注意题目是找只出现一次的数字,那么难免会有的数字出现多次,所以的数字共有42亿个
所以我们要分为三种状态:
- 出现0次
- 出现1次
- 出现2次及以上
一个位可以表示两个(0 1)状态,三种状态我们需要2个比特位,意思就是说,我们需要开两个位图,这两个位图的对应位置分别表示该位置整数的第一个位和第二个位。
三种状态定义为:00 01 10,当我们读取到重复的整数时,就可以让其对应的两个位按照00→01→10的顺序进行递增变化,最后状态是01的整数就是只出现一次的整数。
template <size_t N>
class TwoBitSet
{
public:
void Set(size_t x)
{
if (!bs1.test(x) && !bs2.test(x)) // 00 ->01
{
bs2.set(x);
}
else if (!bs1.test(x) && bs2.test(x)) // 01 ->11
{
bs1.set(x);
bs2.reset(x);
}
// 表示x已经出现过2次以上的了,不用处理了
}
void PrintfOnceNum()
{
for (size_t i = 0; i < N; i++)
{
if (!bs1.test(i) && bs2.test(i))
{
cout << i << endl;
}
}
}
private:
bitset<N> bs1;
bitset<N> bs2;
};
void testTwoBitSet()
{
int a[] = { 1,1,2,2,99,55,55 };
TwoBitSet<100> tbs;
for (auto e: a)
{
tbs.Set(e);
}
tbs.PrintfOnceNum();
}
2、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
思路一:一个文件中整数,set到一个位图,读取第二个文件中的整数判断在不在位图,在就是交集,不在就不是交集
1G内存够吗?
够的,我们bitset开辟的时候,是按照范围来开辟的,只需要开辟500M空间,即可包含所有的整数。
这种方法的缺陷?
缺陷:交集会把重复值找出来,多次出现
思路二:第一个文件中整数,set到一个位图bs1,另一个文件中整数,set到一个位图bs2
子思路a:遍历bs2中值,看在不在bs1,在就是交集。
子思路b:bs1中的值依次跟bs2中的值按位与一下,再去看与完是1的位置值就是交集。
与完:遍历,结果,是全0,我们就知道,第一个字节,没有交集
3、位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
这其实是第一个题的变形,我们可以分为4种状态:
- 出现0次(意思就是没出现过的)
- 出现1次
- 出现2次
- 出现3次及以上的
根据题意,我们只需要找出出现1次和2次的即可
分别对应四种状态
00
01
10
11
变式问题:找出不超过5次的所有整数,需要几个位图?
一个值用三个位统计次数,共3个位图即可