算法笔记-位图

网络爬虫的原理:通过解析已经爬取页面中的网页链接,然后再爬取这些链接对应的网页。

但是,一个页面的链接有可能被包含在多个页面之中,这就会导致爬虫爬取过程中,重复爬取相同的页面,这就需要我们过滤一下已经爬取过的页面。

最直接得方法,每次爬取新的链接的时候,都在已爬取链接的集合中查找,确认是否已经爬取过。思路简单,接下来总结用什么数据结构存储已爬取的链接。

算法分析

问题处理对象是网页链接,包含两个操作,一个是插入链接一个是查找链接。非功能方面,需要支持快速插入和快速查找。爬虫爬取的网页有上亿之多,内存消耗会很大,所有存储效率要高。

针对上述分析,功能上满足条件的数据结构有散列表,红黑树,跳表等动态数据结构,接下来分析内存方面是否可以接受。

举例散列表。假设爬取 10 亿网页,数据存储在散列表中。一个 URL 的平均长度为 64 字节,存 10 亿个 URL大约需要 60GB 的内存空间。由于散列表必须维持较小的装载因子,所以需要的存储空间要大于 60GB。如果用链表法解决散列冲突,还需要额外的空间存储指针,这样下来可能需要 100GB 以上的内存空间。

接下来分析如何优化内存空间。

布隆过滤器

布隆过滤器是基于位图的,对于位图的一种改进,所以先总结一下位图。

举例:如果我们有 1 千万个整数,范围在 1 到 1 亿之间,如何快速查找某个整数在这 1 千万的整数中呢?

我们使用一种特殊的散列表存储数据。首先,申请大小为 1 亿,数据类型为布尔类型的数组。将 1 千万的整数作为数组下标,对应的数组值设置为 true。

当查找某个整数 K 是否在 1 千万的整数中时,获取 array[K] 的值,如果为 true 说明 K 在这 1 千万个整数之中。大多数语言中布尔类型大小是 1 个字节,并不节省空间。那么我们用一个二进制位来表示 true 和 false。位图代码如下:

public class BitMap {
	private char[] bytes;
	private int nbits;

	public BitMap(int nbits) {
		this.nbits = nbits;
		this.bytes = new char[nbits / 8 + 1];
	}

	public void set(int k) {
		if (k > nbits)
			return;
		int byteIndex = k / 8;
		int bitIndex = k % 8;
		bytes[byteIndex] |= (1 << bitIndex);
	}

	public boolean get(int k) {
		if (k > nbits)
			return false;
		int byteIndex = k / 8;
		int bitIndex = k % 8;
		return (bytes[byteIndex] & (1 << bitIndex)) != 0;
	}
}

分析一下节省了多少空间。如果用散列表存储 1 千万的数据,数据类型是 32 位的整型,也就是需要 4 个字节的存储空间,一共需要 40MB 的存储空间。如果用位图的话,数字范围 1 到 1 亿之间,需要 1 亿个二进制位,也就是 12MB 的空间。

如果数字范围从 1 到 10 亿,那么存储空间就是 120MB 了。这个时候就用布隆过滤器来继续优化。

继续刚才的例子,1 千万个整数,1 到 10 亿的范围,仍然用 1 亿个二进制大小的位图表示,通过哈希函数,使得数据落在 1 到 1亿的范围。这样的设计必然存在哈希冲突,那么怎么解决哈希冲突呢?

如果只采用一个哈希函数,冲突的概率会很高,但是如果采用多个哈希函数的话,肯定就会降低冲突的概率。接下来总结布隆过滤器是如何解决冲突的。

使用 K 个哈希函数对同一个数字求哈希值,得到 K 个不同的哈希值,分别记做 X1,X2,X3,··· XK。把 K 个哈希值作为图中的下标,将对应的 BitMao[X1],BitMao[X2],BitMao[X3],···BitMao[XK]的值都设置为 true。也就是说,用 K 个二进制位表示一个数的存在。

当我们查找某个数字的时候,分别求出 K 个哈希值,然后找到这些哈希值对应位图中的值,如果都为 true 则说明该数字存在,如果有一个为 false 就说明不存在。对于两个不同的数字,同一个哈希函数处理可能得到相同的哈希值,如果是多个哈希值处理,最终哈希冲突的概率就小得多了。

也有一个新的问题,布隆过滤器的误判。它只会对存在的情况误判。如果一个数字经过布隆过滤器判断不存在,那它就是真的不存在。如果经过布隆过滤器判断存在,有可能是误判,其实并不存在。(如果数据将位图快要存满的时候,这种误判会更明显)只要我们调整哈希函数的个数,位图大小跟要存储数字个数的比例,就可以将这种误判降到最低。

经过布隆过滤器存在这种误判,但是并不影响我们使用。在实际场景中,爬虫爬取页面,如果一个页面被误判导致爬虫重新爬取一次,这样的情况我们是可以接受容忍的。如果统计一个网站的访问量,需要对同一个用户去重,那么用布隆过滤器去重,即便是有误判,也是我们可以接受容忍的。

布隆过滤器用多个哈希函数对同一个网页链接进行处理,CPU只需读取一次网页链接,然后进行多次哈希计算,理论上讲这组操作是 CPU 密集型的。如果用散列表的处理方式,需要读取散列表冲突链中多个网页链接,进行字符串匹配,这组操作设计内存数据的多次读取,所以是内存密集型操作。CPU处理内存访问要更快,所以理论上讲布隆过滤器的判重,更加快速。

布隆过滤器的应用很广泛,多数编程语言都已经实现了。比如 Java 中的 BitSet 类,Redis 也提供了 BitMap 位图类,Google 的 Guava 工具包也提供了 BloomFilter 布隆过滤器的实现。

总结

本文创作灵感来源于 极客时间 王争老师的《数据结构与算法之美》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

初入算法学习,必是步履蹒跚,一路磕磕绊绊跌跌撞撞。看不懂别慌,也别忙着总结,先读五遍文章先,无他,唯手熟尔~
与诸君共勉

关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值