时隔多年又再次见到这个问题,不得不说是一个经久不衰的问题。与友人讨论时,感叹这个问题反复在不同场合被不同人提起,但每次探索答案时,又总不能获得一个“标准答案”。最早我第一次遇到这个问题时,我给出的令我自己可以满意的答案是:大量数据去重:Bitmap和布隆过滤器(Bloom Filter)。在一定程度上,它确实大大缓解了“有限空间”对hash实现的限制。但是回头一看,布隆过滤器作为工程实现(带有一定错误率),它不应该是一道算法题的解决思路——即最优解。所以,今次再次谈及此题,又有了一些别的思考。时移事易,今日的题设有了一些变化:
有A、B两份大文件,按行分隔,放置了数以亿记的URL。请你给出一个算法,找出所有两份文件同时出现的URL。内存空间有限,你只能适用512MB内存。
换汤不换药,单纯的逐行读取文件并不需要一次性加载整份文件。因此我们以B文件为逐行读取的对象,那么每一个URL,我们需要判断它是否存在于A中,这个操作越快越好。如果空间不限,我们可以把A加载到内存,创建一个哈希表——单纯的位图并不适用与这种场景,因为URL即便映射到某个位上,为了解决碰撞问题,我们仍然需要存储URL本身,以便碰撞发生时进行开放地址(Open addressing)或链地址(Separate chaining)1。这种做法,平均上仍然可以在O(1)时间内查到一个URL存在与否。但是,空间上所有字符都需要进入内存,空间复杂度为 O ( N L ) O(NL) O(NL), N N N为A文件的行数, L L L为URL的平均长度。RFC72302中提及实践中建议客户端和服务器支持8000字节的URL,而大多数浏览器支持2000字节的URL3。
文章目录
空间压缩
和位图类似,遇到这类空间有限的问题,我们总是会先想到如何压缩空间到极致、而不是优先考虑牺牲一部分东西——布隆过滤器牺牲了正确率,以及我们即将提到的外排序牺牲了时间。位图相当于为每个整型值找了一个单独的比特来表示其存在性,而对于URL,我们能做些什么呢?
编码
如上图所示,URL是一种高度结构化的字符串4,从某种角度上可以进行编码压缩。
- 对于scheme部分,有限的几种协议如http、https、ftp等,可以用一个字节进行表示;
- 对于host部分,以ip形式出现的host实际是“点分十进制”表示的四个字节,“127.0.0.1”可以直接被压缩成十六进制数0x0F000001;
- 对于port部分,可能的端口号为0~65535,可以用两个字节表示;
- 从定长码的角度,URL可能字符集为84个字符5:
{ c h ∣ c h i n A t o Z , a t o z , 0 t o 9 a n d − . : / ? # [ ] @ ! $ & ′ ( ) ∗ + , ; = } \{\ ch\ |\ ch\ in\ A\ to\ Z,a\ to\ z,0\ to\ 9\ and\ -._~:/?\#[]@!\$\&'()*+,;=\} { ch ∣ ch in A to Z,a to z,0 to 9 and −. :/?#[]@!$&′()∗+,;=}
严格上来说,并不需要一个字节来表示一个字符,可以将每个字符映射到一个7比特码本(码空间大小为128),这样可以将空间消耗缩小为原来的7/8; - 从变长码的角度,URL也是一种类字词拼接的文本形式,霍夫曼编码可以对URL进行无损压缩。
压缩之后的字节流可以成倍缩短URL的长度,但是仍然需要成百字节来构成一个URL。如果我们用一个很不保守的长度50字节来估算一个URL,1亿条URL(假设各不相同、没有重复)需要空间:
50 B y t e s ∗ 100000000 ≈ 5 ∗ 2 30 B y t e s = 5 G B 50 Bytes * 100000000 ≈ 5 * 2^{30} Bytes = 5 GB 50Bytes∗100000000≈5∗230Bytes=5GB
这看起来并不是很大,但是仍然没法在512MB如此严酷的内存条件下完成任务。
字典树(Trie)
字典树(Trie)也叫前缀树,它是我这次看到这题时,第一个想到的解决办法。首先,URL这种高度结构化的、自带前缀叠合属性的文本串,用字典树这种前缀结构时再合适不过了。本身来说,每个网站在有限的URI内,实际上是把自身有限的资源按目录结构有序放置在一个多叉树下,就如文件系统一样,所以经常会出现前缀相同的资源:如www.baidu.com/abc/1.txt和www.baidu.com/abc/2.txt,他们实际的差异就是最后的资源文件名,前缀则完全相同。当然,这个假设是建立在输入URL都是有效的URL的前提下——如来源于爬虫爬取等,当然这也是最常见的实际应用。如果是随机暴力拼装的URL,则不会具备任何前缀特性,访问之后对端服务器大概率会丢给你一个404。假定,A文件有大量来自于http://www.google.com/mail、http://www.google.com/document以及http://www.facebook.com/的链接,以字典树的形式会被压缩为:
这种形式的压缩就不再局限于URL本身了,而是一种