BitMap是一种很经常使用的数据结构,它的思想的和原理是不少算法的基础,固然,而且在索引,数据压缩,海量数据处理等方面有普遍应用。java
BitMap 是一种很经常使用的数据结构,它的思想和原理是不少算法的基础,好比Bloom Filter 。算法
BitMap 的基本原理就是用一个 bit 位来存放某种状态(若是理解不了,看完下文再回头来看便可),适用于拥有大规模数据,但数据状态又不是不少的状况。一般是用来判断某个数据存不存在的。数组
它最大的一个特色就是对内存的占用极小,因此常常在大数据中被优化使用。数据结构
为何说占用内存小呢?其实从名字就能够看出端倪,直译过来叫位图,但不是图形学里面的位图哦,关键单词是Bit。好比经过某种方法用一个 bit 来表示一个 int,这样的话内存足足压缩至 1/32(1 int = 4 byte = 32 bit,PS:理论计算而已,实操时并不会有 1/32 这么夸张,下文会解释),因此原先须要8G内存的数据,如今只须要256M,岂不乐哉?固然了,其中算法的一些概念在下文会详解。函数
所谓的 BitMap 就是用一个 Bit 位来标记某个元素对应的 Value, 而 Key 便是该元素。因为采用了 Bit 为单位来存储数据,所以在存储空间方面,能够大大节省。性能
好比有个 int 数组 [2,6,1,7,3],内含5个元素,存储的空间大小为 5 * 32 = 160 bit,取的时候,使用元素的下标来获取对应位上的元素。大数据
可是若是换种思路,把元素的值做为下标,每一个下标位使用 bit 来标记,有值则为1,不然为0,此时咱们只须要在内存上开辟一个连续的二进制位空间,长度为8(由于上面数据最大的元素是7,可是须要考虑下标起点为0),则能够表示成:优化
说明:初始化一个长度是8的 BitMap,初始值均为0,而后将[2,6,1,7,3]填入对应的下标处,上图中蓝色域,即将这几个下标处的值设置为1,因此表示为:1 1 0 0 1 1 1 0。此时占用的内存空间为 8 bit,而原来是 160 bit(顺便解释下上文提到的 1/32,由于咱们开辟的是连续的内容空间,因此会有冗余)。spa
① 案例一:仍是上文的数组,需求是查询元素6是否在数组中。 原先咱们须要遍历整个数组,时间复杂度为 O(n); 而如今咱们只须要查验下标为6的字节是0仍是1便可,若是是1,则表明存在,时间复杂度直接降为 O(1)。 因此,**最直接的应用场景即是:**数据的查重。设计
② 案例二:有两个数组,判断这两个数组中的重复元素。 原先的最浅显的作法是双层for循环进行判断比较。 而如今,只须要将转换完成的两个BirMap进行与运算便可,如:11001110B & 10100000B = 10000000B,全部得出结果,只有元素 7 重复。 固然,最直接的应用场景是: 每一个客户都有不一样的标签,当须要查找同时符合标签a和标签b的客户的时候,只须要将标签a和标签b的客户查出来进行如上的与运算便可。
① 实际使用的时候,并不会向上面同样很随意地将长度设置为8,通常会设置为32(int型)或64(long型),理由见下文 BitSet 源码便可。
② 除了上文提到的与运算,固然了,逻辑或和逻辑异或操做都是OK的。
③ 每一个Bit位只能是0或1,因此只能表明true or false,当咱们要进行少许统计的时候,可使用2-BitMap,即每一个位上可使用 00、0一、十、11来分别表示数量为 0、一、2,此时的 11 通常无心义。
对于 BitMap 这种经典的数据结构,在 Java 语言里面,其实已经有对应实现的数据结构类 java.util.BitSet 了(***@since ***JDK1.0),而 BitSet 的底层原理,其实就是用 long 类型的数组来存储元素,因此回过头来看上文提到的为何实际使用的时候,长度通常会是有规则的,由于此处使用的是long类型的数组,而 1 long = 64 bit,因此数据大小会是64的整数倍。
/** * The internal field corresponding to the serialField "bits". */
private long[] words;
复制代码
至于 Java 中的 BitSet 为何使用 long 数组而不使用 int 数组,我以为应该是出于 Java 语言的性能考虑的,由于在进行逻辑与等一系列位运算的时候,是须要将两个数组中的元素一一进行位运算的,而使用 long 的一个好处是数组的长度减小了,从而遍历的次数也就减小了。
总之就是和场景有关系,抽象概念上就有点相似 Java 中字符串的匹配算法(indexOf)使用的是 BF(暴力检索)算法同样,为何不用更优解呢?还不是由于更优解在少许数据的状况下反而是拖后腿的那一位。
有参构造的参数表明的是元素的长度,不是数组的大小,好比传参1和64,数组的长度均为1,整个size均为64,可是传参65的时候,数组长度为2,size为128,由于数组是long类型,而一个long能够存储64个bit元素。
该函数只在两个构造方法中调用,做用是初始化数组,而数组的长度则会经过 workIndex(nbits-1) + 1 来获取。
这个方法很重要, 它是用来获取某个数在 words 数组中的索引的,采用的算法是将这个数右移6位,why?由于 bitIndex >> 6 == bitIndex / (2^6) == bitIndex /64,而long就是64个字节。
又是一个很重要的方法,做用是动态扩容,由于在初始化的时候,咱们并不知道未来会须要存储多大的数据。
size 方法很好理解,返回的其实就是数组的空间大小,即数组长度*64。 而 length 方法,看源码其实有点晦(qu)涩(qiao),简言之,返回的是 BitSet 的“逻辑大小”,即BitSet 中最高设置位的索引加 1 。
举个栗子,一个 BitSet 中存储了两个元素,10和50,那么,此时这个 BitMap 的:size = 64;length = 51。
其他的 set、get等方法暂不赘述,总之一句话,想要深入理解 BitSet 的源码,对于二进制的计算须要有必定的掌握水准。不得不认可,BitSet 的源码,不少细节的设计太精妙了。
如要论述拓展,要么就是论述场景的高层次应用,要么就是论述此算法的不足之处,此处各提一个点:
① 不足:数据稀疏问题,好比三个元素(1,100,10000000),则须要初始化的长度为 10000000,很不合理,此时可使用 Roaring BitMap 算法来解决,而 Java 程序可使用goolge的 **EWAHCompressedBitmap **来解决。
② 拓展:数据碰撞问题,好比上文提到的爬虫应用场景是将URL进行哈希运算,而后将hash值存入BitMap之中,可是不得不面临一个尴尬的状况,那就是哈希碰撞,而布隆算法(Bloom Filter)就能够解决这个问题,为何是拓展呢?由于它是以 BitMap 为基础的排重算法。