海量数据常用技巧之位图法

来源:互联网 发布:淘宝金丝楠木手串真假 编辑:程序博客网 时间:2024/04/30 01:58

介绍
位图的基本概念是用一个位(bit)来标记某个数据的存放状态,一个bit只能表示两种状态,所以不适合用来表示多状态(缺点1)。如果要表示多状态,需要用多个bit,但此时位图的性能会大打折扣,复杂度却增加了。由于采用了位为单位来存放数据,所以节省了大量的空间。一般把这种方法称为位图法,即Bitmap。
BitSet
正因为位图运算在空间方面(指的是海量数据的存储)的优越性,很多语言都有直接对它的支持。如在C++的STL库中就有一个bitset容器。而在Java中,在java.util包下也有一个BitSet类用来实现位图运算。此类实现了一个按需增长的位向量。BitSet的每一位都由一个boolean值来表示。用非负的整数将BitSet的位编入索引,可以对每个编入索引的位进行测试、设置或者清除。通过逻辑与、逻辑或和逻辑异或操作,可以使用一个BitSet修改另一个BitSet的内容。
需要注意的是BitSet底层实现是通过一个long数组来保存数据的,也就是说它增长的最小单位是一个long所占的逻辑位,即64位。但如果不是对存储区空间有极致的要求,而且对自己的基本功非常有信心,不建议自己去实现一个跟BitSet类似的类来实现相关的功能。因为jdk中的类都是极精简并做过合理优化的。

腾讯的一个面试题:
给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?

思路:首先是40亿个不重复的unsigned int的整数,8位的unsigned int的整数最多表示256个数,16位的unsigned int的整数最多表示65536个数,32位的unsigned int的整数最多表示约43亿个数。因此,这里的unsigned int的整数可以理解为至少是用32位表示的。如果说把这40亿个数全部放进内存,然后排序,至少需要40*10^8*4B的内存,即约为16GB内存(精确是14.9GB),这显然太吃内存了,而且如果内存有限制的话,这显然是做不到的。当然可以外排序解决内存不够的问题。不过这里我们考虑用位图来做,因为位图非常适合处理海量的数据。

40亿个数,每个数用1bit表示其是否存在,这样需要的内存大小应该为2^32bit,即512MB。这里不是40亿bit,因为这40亿个数是用32位表示的,其中可能存在32位能表示的最大数。这里同时也暴露出来的位图的一个缺点,即申请空间的时候要按照最大的数来决定申请空间的大小,而且有可能造成空间的利用率会非常低。

这里写图片描述

实际上位图是一种Hashtable,是一种映射关系,这里的数组每个元素占用一个字节的空间,数组元素的下标加上元素的每个位的下标和这个bit构成了一种映射关系。比如说15这个数,如果给的数中存在这个数,那么数组的第二个元素的最高位可以置为1表示存在15这个数。

所以如果用位图的话,需要的内存为2^32bit,即512MB。这是完全可以接受的。如果这里的40亿个unsigned int的整数是用64位表示的,那么这里需要的内存就为2^64bit,即2^31GB,这是万不可能接受的。而这里如果采用全部放进内存进行排序的话,需要32GB的内存,相比来说倒是可以接受的。

首先申请512MB的内存空间,数组的每个元素占用一个字节的空间,遍历一遍这40亿个数,将相应位置的bit位置1。然后根据要判断的数计算出数组元素的下标和bit的下标,看该bit位是否为1为0即可。判断的时间复杂度是O(1),而遍历的时间复杂度是O(n)。

举个例子,1234这个数,用数组哪个元素里面的哪个位表示? 1234 >> 3 = 154,表示用数组的下标为154的元素里面的位表示,而1234 & 0b111 = 2 表示用下标为2的bit位表示。这里使用右移代替除法和使用与操作代替取余是可以加快计算速度。因为除以8就等于右移3位,对8取余就相当于与0b111相与。

然后就是置1的操作可以用或操作,第0位bit置1,就和0x01做或,以此类推。0x02,0x04, 0x08, 0x10, 0x20, 0x40, 0x80。判断是否为1可以用与操作,第0位和0x01做与,以此类推。
这里考虑的是数组的每个元素是一个字节的大小,如果每个元素是4个字节的大小,上面的0x01,0x02,0x04, 0x08, 0x10, 0x20, 0x40, 0x80就需要增加至32个,很麻烦。
可以这样写1 << (1234 & 0b111),将1进行左移操作即可。

/*******************************************************************************************
这部分还未仔细考虑
这个问题在《编程珠玑》里有很好的描述,大家可以参考下面的思路,探讨一下:
又因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;
这里我们把40亿个数中的每一个用32位的二进制来表示
假设这40亿个数开始放在一个文件中。
然后将这40亿个数分成两类:
1.最高位为0
2.最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);
与要查找的数的最高位比较并接着进入相应的文件再查找
再然后把这个文件为又分成两类:
1.次最高位为0
2.次最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了);
与要查找的数的次最高位比较并接着进入相应的文件再查找。
…….
以此类推,就可以找到了,而且时间复杂度为O(logn)

/***********************************************************************************

海量数据的排序
第一遍遍历,进行置1,第二遍遍历,输出为1的位代表的数字,这就完成了排序。

海量数据去重
一个比较常见的面试题:在2.5亿个整数中找出不重复的整数,内存不足以放下算有的数。
(1)使用两位的位图,00表示没有出现,01表示出现一次,10表示出现多次,11没有意义。总共需要2^32*2bit内存,即1GB内存。(这里同样是考虑整数用32位的表示,如果用16位表示,就不需要那么多内存了。)初始时所有整数都是00,去遍历一遍整数,是00变为01,是01变为10,其他保持不变。遍历完后是01的数就是要找的数。
(2) 这里也可以使用两个一位的Bitmap,即第一个Bitmap存储的是整数是否出现,如果再次出现,则在第二个Bitmap中设置即可。这样的话,就可以使用简单的1- Bitmap了。第一个位图里面置1之前先判断是否为1,不为1说明是第一次出现,置1即可,为1说明已经出现过了,然后再去第二个位图里面进行置1。最后两个位图都为1的是重复出现的,第一个里面为1第二个里面为0的是出现一次的。

数据压缩 这部分还未仔细考虑
假设有这样一份数据,记录了全国1990-1999年出生的人的姓名和出生年月的键值对。假设正好有一千万人,那就要存储一千万个姓名和年份。如何运用Bitmap的思想来压缩数据呢。下面提供几种思路。
从人的角度来看,由于一共就只有10个年份,可以用4个bit将它们区分开。如0000表示1990年,1001表示1999年。那一个人的出生年份就可以用4个bit位来表示,进而一千万个年份就可以压缩为一千万个4位的bit组;从另一个角度来看这个问题,我们有10个年份,每个人要么是要么不是在这个年份出生。每个人对于年份来说就可以抽象为一个bit位,所以我们可以把一千万的年龄压缩为10个一千万位的bit组。这样压缩的力度不如按人的角度压缩的大,但从年份出发的问题会有一定的优势,如有哪些人是1990年出生的,只需遍历1990年对应的那个bit组就可以了。
可以看出来不管从哪个角度,bitmap的压缩都是建立在数据中存在大量的冗余数据的基础上的,如年份。而在上面的问题中,年份的分布是散乱的,那假如我们事先把数据进行了排序,把相同的出生年份的人排在一起,那数据就可以进一步压缩。这样一来就只要记录每个年份的人数,就可以根据下标来判断每个人的出生年份。

位图对有符号类型数据的存储,需要 2 位来表示一个有符号元素。对于有符号的数组还未考虑。

/**************************************这部分还未仔细考虑
使用位图法判断整形数组是否存在重复
遍历数组,一个一个放入bitmap,并且检查其是否在bitmap中出现过,如果没出现放入,否则即为重复的元素。
3、使用位图法进行整形数组排序
首先遍历数组,得到数组的最大最小值,然后根据这个最大最小值来缩小bitmap的范围。这里需要注意对于int的负数,都要转化为unsigned int来处理,而且取位的时候,数字要减去最小值。
/*****************************************************************

0 0