个人主页:Lei宝啊
愿所有美好如期而遇
位图
概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的
1. 面试题 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在 这40亿个数中。【腾讯】
- 遍历,时间复杂度O(N)
- 排序(O(NlogN)),利用二分查找: logN
- 位图解决 数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一 个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。
实现
template<size_t N>
class bitset
{
public:
bitset()
{
/*
N是整数个数,我们要将他当做一个bit位,而vecotr元素都是int
一个int32个比特位,如果我们有50个数,那么结果是1,却不够,
所以我们采取进一的操作
*/
v.resize(N / 32 + 1, 0);
}
void set(size_t num)
{
assert(num <= N);
size_t pos_vec = num / 32;
size_t pos_bit = num % 32;
v[pos_vec] |= 1 << pos_bit;
}
//num <= N
void reset(size_t num)
{
assert(num <= N);
size_t pos_vec = num / 32;
size_t pos_bit = num % 32;
v[pos_vec] &= ~(1 << pos_bit);
}
bool test(size_t num)
{
assert(num <= N);
size_t pos_vec = num / 32;
size_t pos_bit = num % 32;
return v[pos_vec] & (1 << pos_bit);
}
private:
vector<int> v;
};
应用
- 给定100亿个整数,设计算法找到只出现一次的整数?
- 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
- 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
应用一解决方法:
100亿整数,其中可能会有多个重复的整数,而我们需要找到只出现一次的整数,一个位图是不够的,所以我们使用两个位图:
template<size_t N>
class algorithm_bitmap_to_TheOnlyNum
{
public:
void set(size_t num)
{
//00->01
if (bt1.test(num) == false && bt2.test(num) == false)
bt2.set(num);
//01->11
else if (bt1.test(num) == false && bt2.test(num) == true)
bt1.set(num);
else
return;
}
bool test(size_t num)
{
if (bt1.test(num) == false && bt2.test(num) == true)
return true;
return false;
}
private:
bitset<N> bt1;
bitset<N> bt2;
};
当num在bt1中的比特位为0且在bt2中的比特位为0时表示该数字不存在,当在bt1中为0且在bt2中为1时表示出现1次,其余情况为2次及以上。
应用二解决方法:
首先100亿个整数,也就是说如果正常存储就是400亿字节,也就是需要大约40GB内存,两个文件就是80GB,即使我们分别将他们存储在位图中,一个位图也需要大约1GB内存,两个位图就是2GB,而我们只有1GB的内存,所以我们这样做:
我们的模板参数是常量N,这个N决定着有多少个bit位,整型值的范围在-2^31 ~ 2^31-1,但是比特位没有负数位,所以我们使用无符号整数来映射负数,所以比特位的范围就是0 ~ 2^32-1,我们可以先开一半的空间,这样两个位图加起来就只占一半内存,我们先映射0 ~ 2^31的值,找出交集后,再去映射另一半的值,映射另一半值的时候,值先减去2^31。
这样我们就实现了1GB内存,找到文件的两个交集。
template<size_t N>
class bitset
{
public:
bitset()
{
/*
N是整数个数,我们要将他当做一个bit位,而vecotr元素都是int
一个int32个比特位,如果我们有50个数,那么结果是1,却不够,
所以我们采取进一的操作
*/
v.resize(N / 32 + 10, 0);
}
//void set(size_t num)
//{
// if (num > N)
// return;
// size_t pos_vec = num / 32;
// size_t pos_bit = num % 32;
// v[pos_vec] |= 1 << pos_bit;
//}
num <= N
//void reset(size_t num)
//{
// if (num > N)
// return;
// size_t pos_vec = num / 32;
// size_t pos_bit = num % 32;
// v[pos_vec] &= ~(1 << pos_bit);
//}
//bool test(size_t num)
//{
// if (num > N)
// return;
// size_t pos_vec = num / 32;
// size_t pos_bit = num % 32;
// return v[pos_vec] & (1 << pos_bit);
//}
void set(size_t num)
{
if (num >= N)
num -= N;
size_t pos_vec = num / 32;
size_t pos_bit = num % 32;
v[pos_vec] |= 1 << pos_bit;
}
//num <= N
void reset(size_t num)
{
if (num >= N)
num -= N;
size_t pos_vec = num / 32;
size_t pos_bit = num % 32;
v[pos_vec] &= ~(1 << pos_bit);
}
bool test(size_t num)
{
if (num >= N)
num -= N;
size_t pos_vec = num / 32;
size_t pos_bit = num % 32;
return v[pos_vec] & (1 << pos_bit);
}
private:
vector<int> v;
};
int main()
{
FILE* fp1 = fopen("data1.txt", "r");
FILE* fp2 = fopen("data2.txt", "r");
//2,147,483,648
bitset<INT_MAX> bt1;
bitset<INT_MAX> bt2;
int num;
while (fscanf(fp1, "%d", &num) != EOF)
{
bt1.set(num);
}
while (fscanf(fp2, "%d", &num) != EOF)
{
bt2.set(num);
}
// i < INT_MAX
for (int i = -INT_MAX; i < 0; i++)
{
if (bt1.test(i) == bt2.test(i) && bt1.test(i) == true)
cout << i << endl;
}
fclose(fp1);
fclose(fp2);
return 0;
}
应用三解决方法:
就是应用一的一个变形,还是两个位图,只不过test方法返回条件可以变一变,这里不多做解释。
布隆过滤器
概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概 率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存 在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也 可以节省大量的内存空间。
其实和位图差不多,只是多了几个映射位置,并且使用不同哈希函数进行映射,由他们共同判断是否存在。
布隆过滤器在实际应用中主要是用于字符串,IP地址等的过滤。
布隆过滤器存在误判,即使我们使用多个hash函数映射不同位置,仍然可能存在这样一种情况,即某几个位置的bit位为1,我们去查询一个字符串是否存在,正好查到了这几个位置,然而这几个位置实际上映射的是其他字符串,由此也就产生了误判。
这种误判在布隆过滤器中是不可避免的,但是可以降低误判率,通过开辟更多的位图空间。
实际应用中,比如游戏昵称,通过布隆过滤器先过滤一次我们输入的昵称,如果没找到,那么一定就是未被使用,但是找到了,不一定是被使用了,所以还需要在数据库中进行一次查询。
实现
struct BKDRHash
{
size_t operator()(const string& s)
{
// BKDR
size_t value = 0;
for (auto ch : s)
{
value *= 31;
value += ch;
}
return value;
}
};
struct APHash
{
size_t operator()(const string & s)
{
size_t hash = 0;
for (long i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string & s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N,
class BKDRHash = BKDRHash,
class APHash = APHash,
class DJBHash = DJBHash>
class Bloom
{
public:
void set(const string& value)
{
size_t hash1 = BKDRHash()(value) % M;
size_t hash2 = APHash()(value) % M;
size_t hash3 = DJBHash()(value) % M;
bt.set(hash1);
bt.set(hash2);
bt.set(hash3);
}
bool test(const string& value)
{
size_t hash1 = BKDRHash()(value) % M;
if (!bt.test(hash1))
return false;
size_t hash2 = APHash()(value) % M;
if (!bt.test(hash2))
return false;
size_t hash3 = DJBHash()(value) % M;
if (!bt.test(hash2))
return false;
return true;
}
private:
static const int M = 5 * N;
bitset<M> bt;
};
应用
1.应用一
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
近似算法自然就是布隆过滤器
//假设文件1和文件2就是根据哈希切割后的相同编号的文件
//近似算法---布隆过滤器
int main()
{
FILE* fp1 = fopen("IP_one.txt", "r");
FILE* fp2 = fopen("IP_two.txt", "r");
Bloom<10000> bloom;
char ch[128];
while (fscanf(fp1, "%s\n", ch) != EOF)
{
string s(ch);
bloom.set(s);
memset(ch, 0, sizeof(ch));
}
while (fscanf(fp2, "%s\n", ch) != EOF)
{
string s(ch);
if (bloom.test(s))
cout << s << endl;
memset(ch, 0, sizeof(ch));
}
fclose(fp1);
fclose(fp2);
return 0;
}
精确算法就需要我们进行哈希切割,使用一个hash函数将query求hash值进入不同分割的文件,也就是说,进入同一个文件的query的hash值相同,要么就是相同的query,要么就是发生了冲突而进入。
两个文件都进行hash分割,并且使用同一个hash函数,那么;两个文件分割后形成的相同编号的文件,就是相同的query或冲突的,我们将他们加载进内存,保存在set中,然后对两个set取交集。
如果说有一个文件即使我们分割后仍然比1GB大,那么我们加载进set,如果没有异常,就说明重复的query很多,冲突的很少,如果出现异常,那么我们就换个hash函数再次对这个文件进行hash切割。
2.应用二
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
这里我们仍然使用hash切割,这个应用我们简单给出举例使用
//创建随机IP地址文件
int main()
{
srand((unsigned int)time(0));
FILE* fp = fopen("IP_two.txt", "w");
for (int i = 0; i < 10000; i++)
{
string s = "2001:0db8:85a3:0000:0000:8a2e:0370:";
int num = rand() % 10000;
s += to_string(num);
char ch[128];
strcpy(ch, s.c_str());
fprintf(fp, "%s\n", ch);
}
fclose(fp);
return 0;
}
创建文件进行hash分割
int main()
{
FILE* fp = fopen("IP.txt", "r");
for (int i = 1; i <= 10; i++)
{
string filename("IP");
filename += to_string(i);
filename += ".txt";
FILE* fp = fopen(filename.c_str(), "w");
fclose(fp);
}
int i = 0;
do{
char ch[128];
fscanf(fp, "%s\n", ch);
string s(ch);
int hash = BKDRHash()(s) % 10;
string filename("IP");
filename += to_string(hash);
filename += ".txt";
FILE* fp = fopen(filename.c_str(), "a");
fprintf(fp, "%s\n", ch);
fclose(fp);
i++;
} while (i < 10000);
fclose(fp);
}
找到出现次数最多的ip地址。
#include <map>
using namespace std;
int main()
{
pair<string, int> Max_Count_Ip(make_pair("", 0));
for (int i = 0; i <= 10; i++)
{
string filename("IP");
filename += to_string(i);
filename += ".txt";
FILE* fp = fopen(filename.c_str(), "r");
map<string, int> m;
char ch[128];
while (fscanf(fp, "%s\n", ch) != EOF)
{
string s(ch);
m[s]++;
memset(ch, 0, sizeof(ch));
}
for (auto& e : m)
{
if (e.second > Max_Count_Ip.second)
Max_Count_Ip = e;
}
}
cout << Max_Count_Ip.first << "------count: " << Max_Count_Ip.second << endl;
return 0;
}
3.应用三
如何扩展BloomFilter使得它支持删除元素的操作
在每个位上加一个计数引用,删除就--,直到为0时reset成0。