简介
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://docs.oracle.com/javase/7/docs/api/java/util/BitSet.html
相关推荐
以下是一个简单的示例,展示如何从Bitmap中裁剪出指定大小和位置的子Bitmap: ```java // 原始Bitmap Bitmap originalBitmap = ...; // 裁剪的起始坐标(x, y) int left = 100; int top = 50; // 裁剪的宽度和...
Bitmap位图是Android系统中广泛使用的图像处理类,主要用于显示和操作像素级别的图像数据。在Android开发中,我们经常会遇到需要对Bitmap进行各种操作的情况,其中旋转Bitmap就是一种常见的需求,比如用户拍摄照片后...
本文将基于给定的代码片段,深入探讨“二维码生成BitMap图片”的相关知识点,包括二维码的基本概念、二维码的生成原理、以及如何利用Java语言实现二维码到BitMap图片的转换。 ### 二维码的基本概念 二维码(Quick ...
Bitmap-->List
总的来说,Activity间的Bitmap传递需要考虑内存管理和效率,合理选择传递方式,并结合优化策略,确保应用的稳定性和性能。通过理解这些知识点,开发者能更好地处理Android应用中的图像数据传输。
在Android应用开发中,有效地处理和保存Bitmap是常见的需求。理解Bitmap的保存机制,以及如何根据需求进行缩放和异步处理,能帮助我们编写出更高效、更稳定的应用。同时,合理地管理内存和磁盘资源,可以提升用户...
它提供了多种语言的API,包括Java,使得开发者可以方便地在Java应用中集成二维码的生成和解析功能。 **二、生成二维码** 1. **引入ZXing依赖** 在Java项目中使用ZXing,首先需要在项目的构建文件(如Maven的pom....
Bitmap图片压缩是一个常见的需求,尤其是在处理大量图片或者需要优化应用性能时。本示例将详细探讨Bitmap的两种主要压缩方法:按比例压缩和按质量压缩。 首先,我们来理解Bitmap的基本概念。Bitmap是一个位图图像的...
当我们需要对Bitmap应用Matrix变换时,可以使用Bitmap.createBitmap方法,它接受原始Bitmap、新的宽度和高度以及Matrix作为参数。Matrix会根据给定的变换规则对图像进行处理,生成一个新的Bitmap。例如,以下代码...
在本节中,我们将详细介绍Bitmap类和Graphics类的概念、方法和应用场景。 一、Bitmap类 Bitmap类是C#中用于图像处理的基本类,它提供了多种方法来操作图像,包括图像的创建、编辑、显示和保存。Bitmap类的实例可以...
1:介绍图片耗用内存资源原理,以及计算图片内存方法 2:Bitmap压缩图片基本手段和思路
- 生成带倒影的Bitmap:`createReflectionImageWithOrigin`方法通过复制和翻转Bitmap并添加渐变效果实现。 以上就是Android中Bitmap的一些常见用法,包括创建、转换、保存、缩放以及与ImageView的配合使用。在处理...
RoaringBitmap, 在Java中,一个更好的压缩 bitset RoaringBitmap Bitsets,也称为位图,通常用作快速数据结构。 不幸的是,他们可以使用太多的内存。 为了补偿,我们经常使用压缩位图。咆哮位图是压缩位图,它比传统...
Bitmap位图缩放是图像处理中的常见操作,广泛应用于各种应用程序和系统中,例如手机壁纸适配、游戏画面渲染、图像编辑软件等。在Android开发中,Bitmap对象是用于存储和处理像素数据的核心类,而缩放Bitmap是优化...
Bitmap工具类是Android开发中非常关键的一个部分,主要用于处理图像数据,尤其是在...在实际开发中,结合使用第三方库和自定义的Bitmap工具类,可以更好地应对Android平台上的图像处理挑战,确保应用的稳定性和性能。
当应用程序尝试加载或操作一张超出虚拟机内存预算的`Bitmap`时,系统会抛出`java.lang.OutOfMemoryError: bitmap size exceeds VM budget`异常,导致应用崩溃。为了解决这个问题,开发者需要采取一些策略来优化图片...
Palette是Android SDK中的一个工具类,它主要用于分析和提取Bitmap图像中的颜色信息。通过Palette,我们可以轻松地获取到图片的暗色、亮色、主色、互补色等各种色彩值,这在UI设计和动态主题应用中非常有用。 要...
Redis中的Bitmap是一个非常高效的数据结构,它用于存储和操作位序列。在社交网络或内容分享平台上,点赞功能是非常常见的,用户可以对喜欢的内容进行点赞,系统需要记录这些点赞行为。在大数据量的情况下,传统的...
然而,在处理大量或高分辨率图像时,不当的`Bitmap`管理常常会导致内存溢出问题,这严重影响了应用程序的性能和用户体验。接下来,我们将深入探讨如何有效地管理和优化`Bitmap`的使用,从而避免内存溢出。 #### ...
java实现的Bitmap类 用于自动生成Bitmap文件头和填充调色板等,目前只支持8位256色的Bitmap