5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。若是这些数据是一些32bit大小的数据该如何解决?若是是64bit的呢?java
在面试时遇到的问题,问题的解决方案十分典型,但对于海量数据处理接触少的同窗可能一时也想不到什么好方案。介绍两个算法,对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。面试
在网上并无找到Bitmap算法的中文翻译,在《编程珠玑》中有说起。与其说是算法,不如说是一种紧凑的数据存储结构。其实若是并不是如此大量的数据,有不少排重方案可使用,典型的就是哈希表。算法
public int[] removeDuplicates(int[] array) { int index = 0; Map<Integer, Boolean> maps = new LinkedHashMap<Integer, Boolean>(); for(int num : array) { if(!maps.contains(num)) { array[index] = num; index++; maps.put(num, true); } } return newArray; }
实际上,哈希表实际上为每个可能出现的数字提供了一个一一映射的关系,每一个元素都至关于有了本身的独享的一份空间,这个映射由散列函数来提供(这里咱们先不考虑碰撞)。实际上哈希表甚至还能记录每一个元素出现的次数,这样的数据结构完成这个任务有点“大材小用”了。编程
咱们拆解一下咱们的需求:数组
int
)有一个独享的空间这个空间要多大?对于咱们的问题来讲,一个boolean就够了,或者说,1个bit就够了,咱们只想知道某个元素出现过没有。若是为每一个全部可能的值分配1个bit,32bit的int
全部可能取值须要内存空间为:数据结构
2 32 bit=2 29 Byte=512MB 232bit=229Byte=512MBdom
那怎么样完成这个映射呢?其实就是Bitmap所要完成的工做了。若是咱们把整型0x0一、0x0二、…、0x08的空间依次映射到一个Byte上,每一个bit就表明这个int
值是否出现过,初值为0(false)。ide
若扩展到整个int
取值域,申请一个byte[]
便可,示例代码以下:函数
public static final int _1MB = 1024 * 1024; public static byte[] flags = new byte[ 512 * _1MB ]; public static void main(String[] args) { int[] array = {255, 1024, 0, 65536} int index = 0; for(int num : array) { if(!getFlags(num)) { //未出现的元素 array[index] = num; index = index + 1; //设置标志位 setFlags(num); } } } public static void setFlags(int num) { flags[num >> 3] |= 0x01 << (num & (0x07)); } public static boolean getFlags(int num) { return flags[num >> 3] >> (num & (0x07)) & 0x01; }
其实,就是按int
从小到大的顺序依次摆放到byte[]
中,仅涉及到一些除以2的整次幂和对2的整次幂取余的位操做小技巧。很显然,对于小数据量、数据取值很稀疏,上面的方法并无什么优点,但对于海量的、取值分布很均匀的集合进行去重,Bitmap极大地压缩了所须要的内存空间。于此同时,还额外地完成了对原始数组的排序工做。缺点是,Bitmap对于每一个元素只能记录1bit信息,若是还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。性能
然而Bitmap不是万能的,若是数据量大到必定程度,如开头写的64bit类型的数据,还能不能用Bitmap?咱们来算一算:
2 64 bit=2 61 Byte=2048PB=2EB 264bit=261Byte=2048PB=2EB
EB(Exabyte,艾字节)这个计算机科学中统计数据量的单位有多大,有兴趣的小伙伴能够查阅下资料。这个量级的Bitmap,已经不是人类硬件所能承担的了。我相信谁也不会想用集群去计算这么一个问题吧?因此Bitmap的好处在于空间复杂度不随原始集合内元素的个数增长而增长,而它的坏处也源于这一点——空间复杂度随集合内最大元素增大而线性增大。
因此接下来,咱们要引入另外一个著名的工业实现——布隆过滤器(Bloom Filter)。若是说Bitmap对于每个可能的整型值,经过直接寻址的方式进行映射,至关于使用了一个哈希函数,那布隆过滤器就是引入了k(k>1) k(k>1) 个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。下图中是k=3 k=3 时的布隆过滤器。
x,y,z x,y,z 经由哈希函数映射将各自在Bitmap中的3个位置置为1,当w w 出现时,仅当3个标志位都为1时,才表示w w 在集合中。图中所示的状况,布隆过滤器将断定w w 不在集合中。
那么布隆过滤器的偏差有多少?咱们假设全部哈希函数散列足够均匀,散列后落到Bitmap每一个位置的几率均等。Bitmap的大小为m m 、原始数集大小为n n 、哈希函数个数为k k :
1−1m 1−1m
(1−1m ) k (1−1m)k
(1−1m ) nk (1−1m)nk
1−(1−1m ) nk 1−(1−1m)nk
ε≈[1−(1−1m ) nk ] k ε≈[1−(1−1m)nk]k
ε≈[1−e −nkm ] k ε≈[1−e−nkm]k
k=mn ln2≈0.7mn k=mnln2≈0.7mn
ε≈(1−e −ln2 ) ln2mn =0.5 ln2mn =0.5 k ε≈(1−e−ln2)ln2mn=0.5ln2mn=0.5k
回到咱们的问题中,有趣的是因为硬盘空间是限制死的,集合元素个数n n 的大小反而与单个数据的比特数成反比,数据长度为64bit时,
n=5TB64bit =5×2 40 Byte8Byte ≈2 34 n=5TB64bit=5×240Byte8Byte≈234
若以m=16n m=16n 计算,Bitmap集合的大小为2 38 bit=2 35 Byte=32GB 238bit=235Byte=32GB ,此时的ε≈0.0005 ε≈0.0005 。而且要知道,以上计算的都是偏差的上限。
布隆过滤器经过引入必定错误率,使得海量数据判重在能够接受的内存代价中得以实现。从上面的公式能够看出,随着集合中的元素不断输入过滤器中(n n 增大),偏差将愈来愈大。可是,当Bitmap的大小m m (指bit数)足够大时,好比比全部可能出现的不重复元素个数还要大10倍以上时,错误几率是能够接受的。
最后咱们所要作的,就是实现一个布隆过滤器,而后利用它对硬盘上的5TB数据一一判重,并写回硬盘中。这其中可能涉及到利用读写的buffer,待有时间补上。
这里有一个google实现的布隆过滤器,咱们来看看它的误判率:
import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.util.HashSet; import java.util.Random; public class testBloomFilter { static int sizeOfNumberSet = Integer.MAX_VALUE >> 4; static Random generator = new Random(); public static void main(String[] args) { int error = 0; HashSet<Integer> hashSet = new HashSet<Integer>(); BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), sizeOfNumberSet); for(int i = 0; i < sizeOfNumberSet; i++) { int number = generator.nextInt(); if(filter.mightContain(number) != hashSet.contains(number)) { error++; } filter.put(number); hashSet.add(number); } System.out.println("Error count: " + error + ", error rate = " + String.format("%f", (float)error/(float)sizeOfNumberSet)); } }
在这个实现中,Bitmap的集合m m 、输入的原始数集合n n 、哈希函数k k 的取值都是按照上面最优的方案选取的,默认状况下保证误判率ε=0.5 k <0.03≈0.5 5 ε=0.5k<0.03≈0.55 ,于是此时k=5 k=5 。
/** * Creates a {@link BloomFilter BloomFilter<T>} with the expected number of * insertions and a default expected false positive probability of 3%. */ public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) { return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions }
而还有一个颇有趣的地方是,实际使用的却并非5个哈希函数。实际进行映射时,而是分别使用了一个64bit哈希函数的高、低32bit进行循环移位。注释中包含着这个算法的论文“Less Hashing, Same Performance: Building a Better Bloom Filter”,论文中指明其对过滤器性能没有明显影响。很明显这个实现对于m>2 32 m>232 时的支持并很差,由于当大于2 31 −1 231−1 的下标在算法中并不能被映射到。
enum BloomFilterStrategies implements BloomFilter.Strategy { /** * See "Less Hashing, Same Performance: Building a Better Bloom Filter" by Adam Kirsch and * Michael Mitzenmacher. The paper argues that this trick doesn't significantly deteriorate the * performance of a Bloom filter (yet only needs two 32bit hash functions). */ MURMUR128_MITZ_32() { @Override public <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) { long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong(); int hash1 = (int) hash64; int hash2 = (int) (hash64 >>> 32); boolean bitsChanged = false; for (int i = 1; i <= numHashFunctions; i++) { int nextHash = hash1 + i * hash2; if (nextHash < 0) { nextHash = ~nextHash; } bitsChanged |= bits.set(nextHash % bits.bitSize()); } return bitsChanged; } @Override public <T> boolean mightContain(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) { long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong(); int hash1 = (int) hash64; int hash2 = (int) (hash64 >>> 32); for (int i = 1; i <= numHashFunctions; i++) { int nextHash = hash1 + i * hash2; if (nextHash < 0) { nextHash = ~nextHash; } if (!bits.get(nextHash % bits.bitSize())) { return false; } } return true; } }; ... }