解决方案

容量

空间限制问题涉及的数据量通常非常大,一台机器无法容纳这些数据。在解决问题之前,要先了解一下单台机器的容量,从而规划需要拆分到多少台机器上。主要涉及以下两个容量:

  • 内存
  • 磁盘

常见的大小换算如下:

  • 1 byte = 8 bit
  • 1 KB = 210 byte = 1024 byte ≈ 103 byte
  • 1 MB = 220 byte ≈ 10 6 byte
  • 1 GB = 230 byte ≈ 10 9 byte
  • 1 亿 = 108

1 个整数占 4 byte,1 亿个整数占 4*108 byte ≈ 400 MB

拆分

空间限制问题解题思路是拆分,有以下策略进行拆分:

  • 按出现的顺序拆分:当有新数据到达时,先放进当前机器,填满之后再将数据放到新增的机器上。这种方法的优点是充分利用系统的资源,因为每台机器都会尽可能被填满。缺点是需要一个查找表来保存数据到机器的映射,查找表可能会非常复杂并且非常大。
  • 按数据的实际含义拆分:例如一个社交网站系统,来自同一个地区的用户更有可能成为朋友,如果让同一个地区的用户尽可能存储在同一个机器上,那么在查找一个用户的好友信息时,就可以避免到多台机器上查找,从而降低延迟。缺点同样是需要使用查找表。
  • 按散列值拆分:选取数据的主键 key,然后通过哈希取模 hash(key)%N 得到该数据应该拆分到的机器编号,其中 N 是机器总数。优点是不需要使用查找表,缺点是可能会导致一台机器存储的数据过多,甚至超出它的最大容量。

可以拆分到多台机器上和拆分到多个文件上:

  • 如果一个数据很大,无法放在一台机器上,就将数据拆分到多台机器上。这种方式可以让多台机器一起合作,从而使得问题的求解更加快速。但是也会导致系统更加复杂,需要考虑系统故障等问题;
  • 如果在程序运行时无法直接加载一个大文件到内存中,就将大文件拆分成小文件,分别对每个小文件进行求解。

整合

拆分之后的结果还只是局部结果,需要将局部结果汇总为整体的结果。

词汇的 TopK

题目描述

某搜索公司一天的用户搜索词汇是百亿量级的,请设计一种求出每天最热 TopK 词汇的可行办法。

解题思路

需要先对海量数据进行拆分,可以使用哈希取模的方法拆分到多台机器上。

在每台机器上使用 HashMap 统计词汇的词频,然后构建出一个大小为 K 的最大堆。

最后再用每台机器得到的最大堆的数据创建最终的大小为 K 的最大堆,这个堆中的词汇就是 TopK。

出现次数最多的数

题目描述

有一个包含 20 亿个全是 32 位整数的大文件,在其中找出出现次数最多的数。

空间限制

内存限制为 2 GB。

解题思路

使用 HashMap 来存储一个整数以及其对应的出现次数,一个哈希表项就需要 8 byte 的空间,20 亿个整数最多需要 16 GB 内存。

可以按照整数的散列值将 HashMap 拆分到多台机器上,由于只有 2 GB 的内存,因此可以拆分到 16 台机器上,然后分别求解出在每台机器上出现次数最多的整数,最后将这 16 台机器上得到的 16 个解进行比较取出次数最多的整数,这个整数就是最终解。

没出现的数

题目描述

32 位无符号整数的范围为 0~4294867295,现在有一个正好包含 40 亿个无符号整数的文件,所以在整个范围中必然有没出现过的数,找出所有没出现的数。

空间限制

内存限制为 1 GB;

进阶:内存限制为 10 MB,但是只用找到一个没出现过的数即可。

解题思路

可以使用长度为 4294867295 的 BitSet 来存储一个数是否出现过,大小为 4294867295 bit ≈ 5*108 byte ≈ 500 MB。

对于进阶问题,由于只能使用 10 MB 内存,因此可以先拆分成 64 份。拆分方法为区间拆分,也就是将整数范围划分成等大小的 64 份。求解需要进行两步,第一步使用大小为 64 的无符号整型数组,统计落在每个区间的数的个数,然后找出一个区间,其整数的个数小于区间大小;第二步也是使用 BitSet 来进行求解。

出现两次的数

题目描述

40 亿个无符号整数中找出出现两次的数。

空间限制

内存限制为 1 GB。

解题思路

和找到没出现过的数一样,使用 BitSet,但是需要两个,也就是使用两个 bit 来存储一个整数出现的次数。两个 bit 最大只能存储 3,我们只需要准确知道 0,1,2 这三个信息,对大于 2 的次数我们不用关心,因此两个 bit 完全足够。

具体的思路为,如果一个整数出现一次,就令存储这个整数出现次数的两个 bit 设置为 (0,1),如果出现两次,就为 (1,0),如果出现大于两次,就为 (1,1),那么结果为 (1,0) 的整数就是要找的数。

重复出现的元素

题目描述

给定一个数组,包含 1 到 N 的整数, N 最大为 32 000,数组可能含有重复的值,且 N 的取值不定。

空间限制

内存限制为 4 KB。

解题思路

4 KB = 4*210*8 bit = 32*1024 bit > 32000 bit,因此可以使用 BitSet 来存储每个数是否出现过。

本题的关键是写出 BitSet 的实现代码,如果允许的话可以直接使用 Java 内置的实现。

public class Question {
public static void checkDuplicates(int[] array) {
BitSet bs = new BitSet(32000);
for (int i = 0; i < array.length; i++) {
int num = array[i];
int num0 = num - 1; // bitset starts at 0, numbers start at 1
if (bs.get(num0)) {
System.out.println(num);
} else {
bs.set(num0);
}
}
}
}
class BitSet {
int[] bitset;
public BitSet(int size) {
bitset = new int[(size >> 5) + 1]; // divide by 32
}
boolean get(int pos) {
int wordNumber = (pos >> 5); // divide by 32
int bitNumber = (pos & 0x1F); // mod 32
return (bitset[wordNumber] & (1 << bitNumber)) != 0;
}
void set(int pos) {
int wordNumber = (pos >> 5); // divide by 32
int bitNumber = (pos & 0x1F); // mod 32
bitset[wordNumber] |= 1 << bitNumber;
}
}

所有数的中位数

题目描述

40 亿个无符号整数中找出所有数的中位数。

空间限制

只使用 10 MB 的内存。

解题思路

采用分区间的方法,如果 0~K-1 区间上的数的个数为 M 亿,0~K 区间上的数的个数为 N 亿,并且 M < 20,N > 20,那么可以知道中位数出现在第 K 个区间,并且在第 K 个区间的第 20 亿 - M 个数。

在找到包含中位数的第 K 个区间之后,需要对这个区间上的每个数出现的次数进行统计,也就需要 N*4 B 空间大小,其中 N 为区间的大小。因为限制条件为 10 MB,因此我们可以将 N 取为 2*106。那么区间的个数确定为 4*109 / 2*106 ≈ 2000。

重复的 URL

题目描述

有一个包含 100 亿个 URL 的大文件,假设每个 URL 占用 64 B,请找出其中所有重复的 URL。

解题思路

由于没有具体给出内存限制,因此需要先问清楚。

最直接的解题思路是使用 HashSet,但是如果加了内存限制,一般需要先哈希函数将大文件拆分成小文件,然后分别在小文件中进行求解,最后汇总出最后结果。

切分方式使用哈希取模,保证相同的 URL 被切分到同一个文件中。

布隆过滤器

题目

不安全网页的黑名单包含 100 亿个黑名单网页,每个网页的 URL 最多占用 64 B。现在想要实现一种网页过滤系统,可以根据网页的 URL 判断该网页是否在黑名单上,请设计该系统。

要求

  • 该系统允许有万分之一以下的判断失误率。
  • 使用的额外空间不要超过 30 GB。

解题

直接的解题思路是使用 HashSet 来存储 URL,但是需要的空间大约为 64 B * 1010 ≈ 600 GB,超出了题目要求。

布隆过滤器主要用在网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统。

基本思路是建一个 BitSet 来存储一个网页是否存储过,但是同一个 URL 需要经过多个独立的哈希函数映射。

查找所有包含某一组词的文件

给定数百万份文件,设计一个程序找出所有包含某一组词的所有文件,该程序会被多次调用。

在没有空间限制的情况下,这种问题的解决方案是建立一个词到文件集合的散列表,例如:

“books” -> {doc2, doc3, doc6, doc8}“many” -> {doc1, doc3, doc7, doc8, doc9}

在查找时,只需要对一组词中的每个单词对应的文件集合求交集即可。例如要查找“many books”,只需对“books”和“many”对应的文件集合求交集,得到结果 {doc3, doc8}。

现在考虑空间限制,因为有百万份文件,构造出来的散列表可能会非常大,一台机器放下完整的散列表,因此需要对散列表进行拆分。

可以使用散列值进行拆分,这样就不用再建议一张单词到机器的散列表。

在查找时,先找出每个单词的散列表所在的机器,然后在这台机器上找出单词所对应的文件集合,最后对每个单词所对应的文件集合求交集得到最终结果。