转自: http://shmilyaw-hotmail-com.iteye.com/blog/1741608 

简介

    bitmap在很多海量数据处理的情况下会用到。一些典型的情况包括数据过滤,数据位设置和统计等。 它的引入和应用通常是考虑到海量数据的情况下,用普通的数组会超出数据保存的范围。使用这种位图的方式虽然不能在根本上解决海量数据处理的问题,但是在一定的数据范围内,它是一种有效的方法。bitmap在java的类库里有一个对应的实现:BitSet。我们会对bitmap的引入做一个介绍,然后详细分析一个bitvector的精妙实现,并在后面和java中的BitSet实现做一个对比。在本文中对bitmap, bitvector不做区分,他们表达的是同一个意思。

bitmap的引出

    假设我们有一个很大的数据集合,比如说是一组数字,它是保存在一个很大的文件中。它总体的个数为400个亿。里面有大量重复的数据,如果去除重复的元素之后,大概的数据有40个亿。那么,假定我们有一台内存为2GB的机器。我们该如何来消除其中重复的元素呢?再进一步考虑,如果我们消除了重复的元素之后,怎么统计里面元素的个数并将消重后的元素保存到另外的一个结果文件里呢?

    我们先来做一个大致的估计。假定数字的范围都是从0到Integer.MAX_VALUE。如果我们开一个数组来保存的话,是否可行呢?一个int数字4个字节,要保存0到Integer.MAX_VALUE个数字,那么就需要2的31次方个,也就是说2G个元素。这么一相乘,除非有8GB的内存,否则根本就保存不下来这么多数据。

bitmap分析和应用

    现在,如果我们换一种方式,用bitmap试试呢?bitmap它本质上也是一个数组,只是用数组中间对应的位来表示一个对应的数字。假设我们用byte数组。比如说数字1则对应数组第1个元素的第一位。数字9则超出了第一个元素的8位范围,它对应第二个元素的第一位。这样依次类推,我们可以将这40亿个元素映射到这个byte数组里。一个数字对应到数组中位的关系如下图所示:

    在上图中,假设i是数组中的一个字节,那么它将对应有下面的8个位。假设i是第一个字节,那么数字1就对应到第1位,后面的元素依次类推。

     通过这一番讨论,我们也可以很容易得到数字和保存在数组中元素具体位之间的关系。假设有一个数字i,它对应保存的元素位置为: i / 8。假设数组为a,那么则为a[i/8]。那么它对应到a[i/8]中间的哪个位呢?它对应这个元素中的第i % 8这一位。

    有了这些讨论,我们再来看bitmap的一个具体实现。

bitmap的一个实现

    针对前面讨论的部分,bitmap主要的功能包括有一下几个方面。1. 置位(set):将某一位置为1. 2. 清楚位(clear),清楚某一位,将其置为0. 3. 读取位(get),读取某一位的数据,看结果是1还是0. 4. 容器所能容纳的位个数(size),相当于返回容器的长度。5. 被置位的元素个数(count),返回所有被置为1的位的个数。我们就一个个来分析:

    首先,我们要定义一个byte数组,来保存这些数据。另外,我们也需要元素来保存里面所有位的个数和被置位的元素个数。因此,我们有如下的定义:

private byte[] bits;  

private int size;  

private int count = -1;

现在,假设我们要构造一个BitVector,我们就需要指定它的长度。它的一个构造函数可以构造成如下:

public BitVector(int n) {
size = n;
bits = new byte[(size >> 3) + 1];
}

这里,指定的参数n表示有多少个数字,相当于要置多少个位。由于我们要用byte来保存,所以能保存这么多数字的byte个数为n / 8 + 1。这种长度用移位的方式来表示则为(size >> 3) + 1。右移3位相当于除以8.

set

     前面已经提到过,set某个位的元素,需要找到元素所在的byte,然后再设置byte对应的位。而n / 8得到的就是对应byte的索引,而n % 8得到的是对应byte中的位。这部分的代码实现如下:

public final void set(int bit) {
bits[bit >> 3] |= 1 << (bit & 7);
count = -1;
}

和我前面讨论的类似,这里不过是利用移位的方式实现同样的效果。前面bit >> 3相当于bit / 8。而bit & 7则相当于bit % 8。为什么bit & 7会相当于这个效果呢?在前面有一篇分析HashMap实现的文章里也讨论过这种手法。因为这里一个byte是8位,而8对应的二进制表示形式为1000,那么比它小1的7的二进制形式为0111。在将bit和7进行与运算的时候,所有大于第3位的高位都被置为0,之保留最低的3位。这样,最低的3位数字最小是0,最大是7.就相当于对数字8求模的运算效果。

clear

   和前面的set方法相反,这里是需要将特定的位置为0。

public final void clear(int bit) {
bits[bit >> 3] &= ~(1 << (bit & 7));
count = -1;
}

get

    get这部分的代码主要是判断这一位是否被置为1。我们将这个byte和对应位为1的数字求与运算,如果结果不是0,则表示它被置为1.

public final boolean get(int bit) {
return (bits[bit >> 3] & (1 << (bit & 7))) != 0;
}

count

    count方法的实现是一个比较精妙的手法。按照我们原来的理解,如果要计算里面所有被置为1的位的个数,我们需要遍历每个byte,然后求每个byte里面1的个数。一种想当然的办法就是每次和数字1移位的数字进行与运算,如果结果为0表示该位没有被置为1,否则表示该位有被置位。这种办法没问题,不过对于每个字节,都要这么走一轮的话,相当于前面运算量的8倍。如果我们可以优化一下的话,对于大数据来说还是有一定价值的。下面是另一种高效方法的实现,采用空间换时间的办法:

public final int count() {
// if the vector has been modified
if (count == -1) {
int c = 0;
int end = bits.length;
for (int i = 0; i < end; i++)
c += BYTE_COUNTS[bits[i] & 0xFF]; // sum bits per byte
count = c;
}
return count;
} private static final byte[] BYTE_COUNTS = { // table of bits/byte
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
};

这里建立了一个BYTE_COUNTS的数组。里面记录了对应一个数字1的个数。我们在bit[i] && 0xff运算之后得到的是一个8位的数字,范围从0到255.那么,问题就归结到找到对应数字的二进制表示里1的个数。比如说数字0有0个1, 1有1个1, 2有1个1,3有2个1...。在一个byte里面,最多有256种,如果我们将这256个数字对应的1个数都事先编码保存好的话,后面求这个数字对应的1个数只要直接取就可以了。

和BitSet的比较

    前面我们讨论的bitmap的实现实际上是摘自开源软件lucene的代码片段。它采用byte数组来做为内部数据保存的方式。各种置位的操作和运算都采用二进制移位等运算方式来实现尽可能的高效率。在java内部的类库里,实际上也有一个类似的实现。那就是BitSet。

    BitSet的内部实现和BitVector的实现稍微有点不一样,它内部是采用long[]数组来保存元素。这样,每次的置位和清位操作方式就有差别。比如说置位,原来是对要置的数字除以8,现在则是除以64,相当于>> 6这中移位6次的操作。

    另外,在BigSet里并没有实现求所有被置为1的元素的个数,如果要求他们的话,因为要在64位的数字范围内来找,不可能再用前面数字列表的方法来加快其统计速度,只能一位一位的运算和比较统计了。这是这种实现一个不足的地方。

    BitSet的内部代码实现还有一个比较有意思的地方,我们先看这一段代码:

public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex); int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex); words[wordIndex] |= (1L << bitIndex); // Restores invariants checkInvariants();
} private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}

这是java里对应的置位实现方法。按照我们的理解,它应该是找到对应的long元素,然后再将对64取模后对应的位设置为1.可是这代码里的设置部分却如下: words[wordIndex] |= (1L << bitIndex); // Restores invariants. 这里用到了移位,但是没有对64求模。为什么呢?这样不会出错吗?在我们的理解里,如果对数字向左移位,如果超出了数字的表示范围,潜意识里就会认为那些部分被忽略掉了。这样想的话,那么这么一通移位下来不就得到个0了吗?我们后面针对这一点继续分析。

一个有意思的地方

    这个问题的答案并不复杂。如果我们去察看书上的定义,仔细看才发现。<< >>等这样的移位运算,实际上是循环移位效果的。也就是说,如果我一个数字向左移位到溢出了,它不是被忽略掉,而是后续会在低位继续补进。比如说我们看下面一个最简单的代码:

class test
{
public static void main(String[] args)
{
for(int i = 0; i < 100; i++)
System.out.println(1 << i);
}
}

如果我们执行上面这一段代码,会发现实际的结果是当溢出之后又开始重新从头来显示,部分的输出结果如下所示:

1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
2097152
4194304
8388608
16777216
33554432
67108864
134217728
268435456
536870912
1073741824
-2147483648
1
2
4
8

现在,我们也就理解了为什么前面直接用一个左移位的运算来表示。因为这是循环的移位,相当于已经实现了求模的运算效果了。老实说,这种方式可行,不过个人觉得不太直观,还是用一个类似于求模运算的方式来表示好一些。

总结

    bitmap通过充分利用数组里面每一位的置位来表示数据的存在与否。比如说某一位设置为1,表示数据存在,否则表示不存在。通过充分利用数据的空间,它比直接利用一个数组,然后数组里面的每一个元素来表示一个数组的空间利用率高。比如说有一个同等长度的int数组,原来一个int元素用来表示一个数据,现在利用int元素的每一位,它可以表示32个元素。所以说,在一定程度上,某些数据映射、过滤等问题通过bitmap它可以处理的范围更大。当然,bitmap也受到计算机本身数据表示范围的限制,在超出一定的范围之后,我们还是需要考虑结合数据划分等手段。另外,在考虑这些数据结构的详细实现时,有很多细节的东西也会加深我们的认识,也许很多就是我们平时忽略的地方。

参考资料

http://alvinalexander.com/java/jwarehouse/lucene-1.3-final/src/java/org/apache/lucene/util/BitVector.java.shtml

http://docs.oracle.com/javase/7/docs/api/java/util/BitSet.html

java bitmap/bitvector的分析和应用的更多相关文章

  1. Java 类反射机制分析

    Java 类反射机制分析 一.反射的概念及在Java中的类反射 反射主要是指程序可以访问.检测和修改它本身状态或行为的一种能力.在计算机科学领域,反射是一类应用,它们能够自描述和自控制.这类应用通过某 ...

  2. Java 动态代理机制分析及扩展

    Java 动态代理机制分析及扩展,第 1 部分 王 忠平, 软件工程师, IBM 何 平, 软件工程师, IBM 简介: 本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟 ...

  3. JAVA 从GC日志分析堆内存 第七节

    JAVA 从GC日志分析堆内存 第七节   在上一章中,我们只设置了整个堆的内存大小.但是我们知道,堆又分为了新生代,年老代.他们之间的内存怎么分配呢?新生代又分为Eden和Survivor,他们的比 ...

  4. Java Reference 源码分析

    @(Java)[Reference] Java Reference 源码分析 Reference对象封装了其它对象的引用,可以和普通的对象一样操作,在一定的限制条件下,支持和垃圾收集器的交互.即可以使 ...

  5. Java 线程池原理分析

    1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...

  6. Java应用常用性能分析工具

    Java应用常用性能分析工具 好的工具有能有效改善和提高工作效率或加速分析问题的进度,笔者将从事Java工作中常用的性能工具和大家分享下,如果感觉有用记得投一票哦,如果你有好的工具也可以分享给我 工具 ...

  7. 三个实例演示 Java Thread Dump 日志分析

    原文地址: http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html jstack Dump 日志文件中的线程 ...

  8. JAVA CAS原理深度分析 volatile,偏向锁,轻量级锁

    JAVA CAS原理深度分析 http://blog.csdn.net/hsuxu/article/details/9467651 偏向锁,轻量级锁 https://blog.csdn.net/zqz ...

  9. Java NIO原理 图文分析及代码实现

    Java NIO原理图文分析及代码实现 前言:  最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请 ...

随机推荐

  1. EUI组件之CheckBox

    一.CheckBox常规使用 拖动一个checkBox到exml即可 点击效果 二.代码中监听事件 /** * 主页场景 * @author chenkai 2018/5/26 */ class Ho ...

  2. 【BZOJ1529】[POI2005]ska Piggy banks Tarjan

    [BZOJ1529][POI2005]ska Piggy banks Description Byteazar 有 N 个小猪存钱罐. 每个存钱罐只能用钥匙打开或者砸开. Byteazar 已经把每个 ...

  3. [UML]UML 教程

    统一建模语言(UML)已经迅速变成建立面向对象软件的事实标准.本教程提供了Enterprise Architect支持的13种UML图的技术概览.UML 2 详细的语义解释请看新的UML 2 教程. ...

  4. c# Winform间的页面传值

    Form2 public partial class Form2 : Form { public string str; public Form2() { InitializeComponent(); ...

  5. Centos设置SSH限制登录用户及IP

    1,系统版本查看 2,编辑ssh配置文件 vim /etc/ssh/sshd_config 在尾部加一行 允许sysman用户从ip1.1.1.*登录 3,重启sshd即可 /etc/init.d/s ...

  6. poj3764 The XOR Longest Path【dfs】【Trie树】

    The xor-longest Path Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 10038   Accepted:  ...

  7. php中 const 与define()的区别 ,选择

    来自: http://stackoverflow.com/questions/2447791/define-vs-const 相同点: 两者都可以定义常量 const FOO = 'BAR'; def ...

  8. MySQL :: MySQL 8.0 Reference Manual :: B.6.4.3 Problems with NULL Values https://dev.mysql.com/doc/refman/8.0/en/problems-with-null.html

    MySQL :: MySQL 8.0 Reference Manual :: B.6.4.3 Problems with NULL Values https://dev.mysql.com/doc/r ...

  9. 新安装和已安装nginx如何添加未编译安装模块/补丁

    新安装和已安装nginx如何添加未编译安装模块/补丁 --http://www.apelearn.com/bbs/forum.php?mod=viewthread&tid=10485& ...

  10. 203-ReactDOM

    一.概述 加载方式: <script> ES6:import ReactDOM from 'react-dom' ES5:var ReactDOM = require('react-dom ...