判重问题
假设我们要写一个爬虫程序。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”,爬虫就会进入一个无限怪圈,找不到出路,程序出现崩溃。
所以为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL,也就是如何判重。
判重的方案
- 将访问过的URL保存到数据库,数据库管理系统可以为你去重。
- 用Set将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。
- URL经过MD5或SHA-1等单向哈希后再保存到Set或数据库。
- Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。
方案的缺陷
- 方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?
- 方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,至少需要5GB内存,还不包括Set数据结构中的内存浪费。
- 方法3的缺点:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。
- 方法4的缺点:消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。
hash函数
hash(散列、杂凑)函数,是将任意长度的数据映射到有限长度的域上。直观解释起来,就是对一串数据m进行杂糅,输出另一段固定长度的数据h,作为这段数据的特征(指纹)。也就是说,无论数据块m有多大,其输出值h为固定长度。到底是什么原理?将m分成固定长度(如128位),依次进行hash运算,然后用不同的方法迭代即可(如前一块的hash值与后一块的hash值进行异或)。如果不够128位怎么办?用0补全或者用1补全随意,算法中约定好就可以了。
BloomFilte的出现
单一哈希函数, 出现hash冲突的概率在数据量大的情况下, 很容易出现哈希冲突(即不同的URL, 通过哈希函数通过计算, 得到同样的结果).
BloomFilte就通过k个函数来降低哈希冲突的可能, 首先不同哈希函数计算出来的哈希值是不一样的, 假设1到k个哈希函数, 每个哈希函数出现哈希冲突的概率为p1, p2, p3....pk, 那么如果我采用这k个哈希函数的结果, 共同作为一个哈希值, 那么出现哈希冲突的概率就为 p1 * p2 * p3 * .... * pk. 显然冲突的概率大大降低了.
(1) BloomFilte就采用一个m大小的bit数组和k个不相同的哈希函数. (m的大小可以随意设定, 视情况而定, 这k个不同的哈希函数计算的结果限定为(0, m - 1))
(2) 第一个URL, 分别采用哈希函数计算一次,将计算结果映射到m位的某一位上, 这样一条URL就会在m数组中改变k位变成1(如果对应的某一位是1, 就不用改变了)
(3) 第二个URL, 重复步骤2, 如果发现计算出来的k个哈希函数的值, 在相应位置上都为1了, 就认为该URL重复, 否则就是不重复
(4) 显然第三步可能会出现误判断的情况, 因为随着URL的增多, m数组上肯定很多位置会变成1, 那么在比较的时候, 自己计算的k位全是1, 但是是多个URL共同贡献的结果. 这种情况是算法允许的.
BloomFilte的准确率
(1) 和哈希函数有关, 一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。
(2) 和参数有关
m: 数组的长度
n: 需要判断元素的个数
k: 哈希函数的个数
从实验结果来看 m/n越大越准,k越大越准
参考文章
算法应用
1000瓶毒药里面只有1瓶是有毒的,毒发时间为24个小时,问需要多少只老鼠才能在24小时后试出那瓶有毒。
1000只老鼠 < 1024, 那么10位二进制数是可以表示的.
对1000个瓶子进行编号, 编号的10进制数为0 - 999
- 十进制编号 - 二进制编号
- 0 - 0000000000
- 1 - 0000000001
- 2 - 0000000010
- ...
- 999 - 1111100111
既然10位二进制数能表示所有瓶子, 那么就需要10只老鼠, 为每只老鼠进行编号, a, b, c, d, e, f, g, h, i, j
每个老鼠会喝某一位为1的所有瓶子里的水, 代表老鼠和瓶子的映射
例如:
- a老鼠喝符合xxxxxxxxx1条件, 所有瓶子里的水
- b老鼠喝符合xxxxxxxx1x条件, 所有瓶子里的水
- ....
- j老鼠喝符合1xxxxxxxxx条件, 所有瓶子里的水
结果分析
24小时后
(1) 如果老鼠都没死, 就是第1瓶有毒
(2) 如果部分老鼠死了, 根据老鼠和瓶子的映射关系, 将死去老鼠的所有映射做个位与, 得到的结果再加1, 就是瓶子的编号了(因为瓶子是1-1000瓶, 编号是0-999).
- 例如a和b老鼠死了
- 那么位与就是 xxxxxxxxx1 和xxxxxxxx1x位与
- 得到的结果就是xxxxxxxx11 = 3, 就是第4个瓶子有毒