喜欢的点赞,收藏,关注一下把!
问:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
可能你会想到下面这几种方式:
1.排序+二分查找
2.红黑树
3.哈希表
但是要关注到一点,这是40亿个无符号整数。请问占多大内存?可以放的下吗?
一个整型4个字节,40亿个整型占160亿个字节。
1G=1024MB
1MB=1024KB
1KB=1024Byte
也就是说1G=2^30次方Byte,也就是大约10亿字节。
所以160亿个字节大约占内存16G内存。
这么多数据内存根本存不下。
所以说
排序+二分查找 X
红黑树节点还要存3个指针和颜色,更大 X
哈希表链表节点也还要多存一个指针,并且还有头指针, 也大 X
那怎么解决这里的问题呢?
其实还是用哈希,它这里是K(在不在)模型,去标记一个值在不在最少用多少内存可以标记?
一个比特位,1表示在,0表示不在。
40亿个整数,开42亿9千万空间也就是2^32-1,注意要开范围,看最大最小值的范围。不能开个数。
把这40亿个整数假设从磁盘中读进来,我们用直接定址法,这个值是几我们就放在第几个位置并把这个比特位标记为1。
那现在开辟的空间占多少内存呢?
2^32byte=4G
2^32bite=?
1byte=8bite,所以2^32bite=0.5G=512M
不仅省空间,而且非常快!
而这个东西就是位图
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
下面就是如何实现位图
遇到第一个问题就是支不支持开辟2^32次方个比特位的数组?
答案是不支持的。
通常都是按照类型,最小是char,要不是int等等。
这里用char,int开都不重要。只是说用char开,一个位置就是8个比特位,如果是int一个位置就是32个比特位。
这里我们用char,0-7个比特位在第一个char上,8-15在第二个char上等等
那x映射的值,在第几个char对象上呢?
就比如说10在第几个char上?在第一个。20在第二个。从0开始数。
x/8
那x映射的值,在这个char对象第几个比特位呢?
x%8
如果是int就/32,%32。
接下来我们就实现位图
template<size_t N>//非类型模板参数
class bitset
{
public:
void set(size_t x)//把映射到的标记位,记为1
{}
void reset(size_t x)//清除标记位,记为0
{}
bool test(size_t x)//测试这个值在不在
{}
private:
vector<char> _bs;
};
set如何把这个标记位记为1呢?
肯定是先找到这个位置,这个我们已经知道怎么找了。
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i]//如何把第i个char第j个比特位记为1?
}
或等一下
这里1是向左移还是向右移j位?
并且这里说明一点不要和大小端扯上关系。
我们这里放的char,而不是int,如果是放int,也没什么关系。
就比说整型4个字节。
大端是指数据低地址保存至内存的高地址中,而数据的高地址,保存在内存中的低地址中。
小端是指数据的低地址保存在内存的低地址中,而数据的高地址,保存在内存中的高地址中。
我们的电脑是一个小端。
并且单个字节由低地址到高地址。左高右低。
即使你选择的是int涉及大小端,但是在我们与int做移位运算的时候,CPU会自己帮我们做移动。CPU会知道到底是大端还是小端。
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
reset如何把标记位记为0
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i]
}
1向左移动j位按位取反,然后与等
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= (~(1 << j));
}
test测试这个值映射到的比特位是1还是0
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
假设你的数字范围是0-99,你就要开100个空间,而不是100个数你开100个空间,一定要看你的范围。当然你也可以弄成相对映射,可以在外面就处理然后再传给位图,如范围1000-2000,你可以开1000个空间,但是1000要映射到0的位置,拿的时候要加1000这都需要再外面做修改。
void test_bitset()
{
bitset<100> bs;
}
下一个问题就是vector空间怎么开?开几个char合适?
是不是N/8个char?
如果N是20呢?20/8=2,还少了4bite没开。所以说是N/8+1;
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
位图主要功能我们已经写完了,请问写的有没有bug?
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_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:
vector<char> _bits;
};
其实并没有,但是有的人为了效率改了一下除就可能有了
如果是2的倍数就可以用移位
除向右移动>>
乘向左移动<<
template<size_t N>
class bitset
{
public:
bitset()
{
//注意这里有运算符优先级的问题
//+的优先级比>>高,这里是右移4位了
//_bits.resize(N >> 3 + 1, 0);
_bits.resize((N >> 3) + 1, 0);
}
void set(size_t x)
{
//size_t i = x / 8;
size_t i = x >> 3;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
void reset(size_t x)
{
//size_t i = x / 8;
size_t i = x >> 3;
size_t j = x % 8;
_bits[i] &= (~(1 << j));
}
bool test(size_t x)
{
//size_t i = x / 8;
size_t i = x >> 3;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
如果你想开辟42亿9千万个空间,你怎么开辟?
void test_bitset()
{
bitset<-1> bs1;
bitset<0xffffffff> bs2;
}
-1提升为无符号整型就是这个值的,当然也可以用16进制。
虽然位图很快很好用,但是它不是万能的,它只能针对整型。
我们的库也有一个位图
位图在线文档
接下来看看位图的应用
1.给定100亿个整数,设计算法找到只出现一次的整数?
怎么搞?
这个是位图的变形题,之前搞得是在不在两种状态,这里严格来说要三种状态。
0次
1次
1次以上
三种状态有两个比特位就可以表示。
可能大多数想的是把刚才的位图拿过来改一下。之前一个char里有8个bite表示8个值,现在两个比特位表示一个值,一个char里8个bite表示4个值。然后/4,%4,
00->01,01->10,10就是一次以上就不变了,最后在遍历一边找出所有01的。
这就要把我们写的代码都改一遍。
能不能不要重新造轮子。
之前2个比特位表示一个值。现在我开两个位图。
上面的位图和下面的位图进行组合,假设现在表示的是4种,5种,8种状态只要内存足够我们多开几张位图进行组合。这样就轻松很多。
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
if (!_bs1.test(x) && !_bs2.test(x)) //00
{
_bs2.set(x);//01
}
else if (!_bs1.test(x) && _bs2.test(x)) //01
{
_bs1.set(x);
_bs2.reset(x);//10
}
//超过10不变
}
void print()
{
for (size_t i = 0; i < N; ++i)
{
if (!_bs1.test(i) && _bs2.test(i))//找只出现1次的 01
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
2.给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
如何解决?
能不能把其中一个文件映射到一个位图,在把另外一个文件内的值拿过来在这个位图里找在不在,在就是交集,不在就不是交集?
可以是可以,但是这个方案有些缺陷,这样做的话,最后你的交集要去一下重。100亿里有重复的值,第一次找在进入交集,第二次找也在进入交集。因此交集里有很多重复的值。所有最后交集要去重。
还有一种方法,还是开两个位图,各自放一个位图。然后依次遍历为1的位。
3.位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
不超过2次的。就是1次和2次的。这里还是四种状态
把我们刚才写的代码稍微改一下。
00->01
01->10
11就不动
然后在遍历一下。把01,10都找出来。
4.给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
这里的IP地址可以当成字符串。
如何解决?
要找出现次数最多的IP地址,不就是统计次数吗。统计次数不就是用map吗,但是这里100G给限制了,不能直接用map。并且也不要和位图靠在一起。位图只能判断在不在,而且最多是几次以内的次数。不能用来找次数最多。也不要想哈希表,哈希表也有附带的节点指针的消耗。
那怎么办?
其实还是map,不能直接用,那就间接的来。
100G不能直接放进内存,那把100G分成1G,1G的放进内存。
对这些1G的文件一个一个来统计次数,依次读取每个小文件,依次统计次数,搞完一个,clear清掉map,在统计下一个。
这样平均切分的方式对吗?
并不对!
因为你统计的次数不准的!因为可能同一个IP地址,A0文件有10份,A1文件有10份等等。统计完1G的小文件可能并不是该IP真正出现的次数。但是统计完一个必须clear,不然放不下,而且clear之后也没有办法和下一个文件有联系了。
这时候就有了非常牛的一个东西:哈希切割
用一个HashFunc函数进行切割,把IP给这个函数,把字符串转成整型。然后取膜模上100,i是多少,ip就进入Ai号小文件。
可以理解每个小文件就是一个哈希桶。冲突的IP就进入变化相同的小文件,然后相同的值也一定进入相同的小文件。
但是这里还是有一些问题的。
Ai小文件超过1G怎么办?
这里有两种情况要分开看待。
如果是第一种情况怎么办呢?
换个字符串哈希函数,递归在切分。还像刚才哈希切割一样但是换一个哈希函数,可以切少一点文件。
第二次情况用第一种解决方法解决不了,因为大量ip都是相同的再去换函数切割也都是一样,最终造成死递归。map直接统计即可。
还有一个问题,如何区分第一种情况和第二种情况呢?
这两种情况的关键点是map统计不下,map可以统计。可以从这里下功夫区分两种情况。
方法是:直接用map统计
如果是第二种情况,可以统计出来的,不会报错。
如果是第一种情况,map的insert插入失败,那是没有内存了,相当于new节点失败,new失败会抛异常。
所有不管超不超过1个G,都直接统计,第二种情况直接统计,第一种情况抛异常,我们可以捕获异常再去换个哈希函数递归切割。
关于位图还有其他的应用,这里就不在一一叙述了
位图的优点缺点:
优点:
缺点:
现在有个问题如果给一堆字符串可不可以用位图存储呢?
其实也是可以的,整型可以直接映射,我们可以写一个哈希函数把字符串转成整型在映射,这样的话结构体也可以映射如日期类等等。
查找的时候也根据这个哈希函数把字符串转成整型然后去对应位置找,是1就在,0就不在。
这样位图只针对整型的问题就解决了。
但这种方法有没有什么缺陷呢?
字符串是无线的必须存在多个字符串映射到同一个位置的问题,进而导致一种误判!
1、在是准确的?
2、不在是准确的?
在是不准确的,因为可能本来不在,但是这个位置跟别人冲突,出现误判。
不在是准确的
还有一个缺陷就是假设100个字符,开多大空间,把整型都开出来吗?那还不如用哈希表。但是不把整型都开出来有些字符串转换成整型比较小,有些就比较大也不好把控的。
因此我们这里还是向哈希表一样模一下。
能对能优化一下,能否降低误判率?
find本来不在的,但是现在会误判认为它在。
布隆大佬想了一个办法,叫做布隆过滤器。
让一个值映射多个位置,多个映射都是1就认为在,而find一个位置是1一个位置是0就是不在了
布隆过滤器改进:映射多个位置,降低误判率,不能消除误判。
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
这个数据可以是一个字符串甚至是一个对象等。
布隆过滤器的应用场景
1、不需要一定准确的场景。
如:注册时候昵称判重
以前是把这些用户信息存到数据库,数据库在远端查的时候比较慢还要走网络。假设现在要求输入昵称之后快速判断。这个时候可以把所有昵称放到布隆过滤器里,查的时候不在确确实实就是不在的。当然这里还有其他的一些操作和布隆过滤器在一起。
如:提搞效率(用的更多)
客户端向服务端查找数据。
以前是向数据库里面查,数据库在磁盘。访问比较慢。
现在可以加一层布隆过滤器
不在就不用去数据库里找,直接返回。
在可能存在误判,然后去数据库里找。在不在都会返回。
假设误判率5%,100个人,5个人可能存在误判,但是事剩下95个人查找都可以提高。
如:黑名单等等。
下面就实现布隆过滤器
详解布隆过滤器的原理,使用场景和注意事项
布隆过滤器我们还是给一个非类型模板参数来确定它的大小,给一个K来确定传的是什么类型,给一些哈希函数把K转换成整型。虽然给的哈希函数越多误判率越低但是空间消耗越高。
如何选择哈希函数个数和布隆过滤器长度
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。
另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高
现在就可以根据这个公式计算布隆过滤器的长度。(n是插入的元素个数,k是哈希函数的个数)
假设k=3,m=4.3n,也就是说插入一个元素要开4.3个位置出来。
布隆过滤器底层是一个位图
namespace wdl
{
struct BKDRHash
{
size_t operator()(const string & key)
{
size_t hash = 0;
for (auto ch : key)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& key)
{
unsigned int hash = 0;
int i = 0;
for (auto ch : key)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
}
++i;
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& key)
{
unsigned int hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
//假设N是最多存储的数据个数
template<size_t N,
class K = string,//更多的是字符串
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash
>
class BloomFilter
{
public:
void set(const K& key)
{
size_t hash1 = HashFunc1()(key) % (5 * N);//模长度
size_t hash2 = HashFunc2()(key) % (5 * N);//模长度
size_t hash3 = HashFunc3()(key) % (5 * N);//模长度
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);//对应的位置set为1
}
bool test(const K& key)
{
size_t hash1 = HashFunc1()(key) % (5 * N);
if (!_bs.test(hash1))
return false;
size_t hash2 = HashFunc2()(key) % (5 * N);
if (!_bs.test(hash2))
return false;
size_t hash3 = HashFunc3()(key) % (5 * N);
if (!_bs.test(hash3))
return false;
//前面判断不在都是准确的,不存在误判
return true; //可能存在误判,映射几个位置都冲突,就会误判
}
private:
bitset<N*5> _bs;//先按5倍开辟,或者直接在外面算好开辟空间大小传给N里面就不要在乘了
};
}
测试误判率
void test_bloomfilter1()
{
srand(time(0));
const size_t N = 100000;
BloomFilter<N> bf;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(i));
}
for (auto& str : v1)
{
bf.set(str);
}
// v2跟v1是相似字符串集,但是不一样
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
url += std::to_string(999999 + i);
v2.push_back(url);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.test(str))
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
// 不相似字符串集
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
string url = "zhihu.com";
url += std::to_string(i + rand());
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
布隆过滤器能否支持reset?
不可以,一个位置有可能被多个值映射!
能不能强制支持一下reset?
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
但是这样有问题:
因为位置,不在使用一个标记位,而是需要多个位存储技术值,空间消耗成倍增加!
布隆过滤器优点:
布隆过滤器缺陷:
布隆过滤器的应用
1.给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
近似算法允许交集存在误差不是交集的也放进去了。
把其他一个文件放进布隆过滤器,再把另一个文件拿过来去里面找一下,在就是交集。是交集一定会进去但是可能存在误判不是交集也进去了。
精确算法
假设平均每个query是50byte,100亿个query合计多少内存?
1G=10亿byte,大约需要500G
怎么找?
因为外面只有1G内存,是不是还是要切割成小文件去找。一定统一用一个哈希函数切割两个文件。
这样Ai和Bi相同的字符串一定会进入编号相同的小文件。
找交集,文件较小这里可以考虑用哈希表,把一个文件放进哈希表另一个文件判断在不在当然最后还需要去重。或者把两个文件分别放进哈希表里然后在遍历找。
这个题的思路和前面哈希切割本质还是类似的,这里某个小文件超过1G放进内存还是用之前的方式,两种可能性,一个不同重复太多只能换个哈希函数在递归切割,一个相同重复太多直接用就好,前面说过了这里不再细说。