Bitmap 也被称为位图。Bitmap 既是一种数据结构,又是一种图片类型。从数据结构的角度讲,Bitmap 适用于以下场景,后文会逐一进行阐述:
Bitmap 是指由多个二进制位组成的数组,数组中的每个二进制位都有与其对应的索引。可以使用索引对二进制位进行操作。如下图表示 16 位的 Bitmap:
数据 {0, 4, 9, 10, 13} 存入 Bitmap 如图2 所示:
判重是指一个元素是否在一个数据集中是否重复出现或存在。在数据处理领域,判重是个很常见的需求。搬个网上的栗子:给一台普通 PC,2G 内存,要求处理一个包含 40 亿个不重复并且没有排过序的无符号的 int 整数,给出一个整数,问如何快速地判断这个整数是否在文件 40 亿个数据当中?
分析:如果我们用 Java 的整型来存储,一个整型是 4Byte,那么 40 亿个 int 需要 40亿 * 4 / 1024 / 1024 / 1024 = 14.9GB。这谁受得了,2GB 内存显然放不下啊。如果采用 Bitmap 存储,那么 40 亿个 int 需要 40 / 1024 / 1024 = 476.84MB,这样就可以放到内存里进行计算了。这里用两种方法可以处理:
定基是指一个数据集中存在多少不同的元素,即数据集的基数。举个栗子:某网站有 15 亿用户,用户 ID 在 1,000,000,000~2,999,999,999 之间,统计每天登陆了多少个用户,最多有 256MB 的内存空间可用。
分析:采用 Bitmap,首先将用户 ID 减去 10^10 ,用 1,999,999,999 个 bit 位存储需要 20亿 / 8 / 1024 / 1024 = 238.42MB 小于 256MB。然后将 Bitmap 的二进制索引一一映射(出现过即设置为 1),最后遍历计算出 Bitmap 中 1 的个数即可。
排序就不做赘述了。直接上栗子:一个最多包含 n 个正整数的文件,每个数都小于 n,其中 n = 10^7,且所有正整数都不重复。最多有 2MB 的内存空间可用,求如何将这 n 个正整数升序排列。
分析:采用 Bitmap,10,000,000 / 1024 / 1024 = 1.19MB,2MB 绰绰有余了。存到 Bitmap 里之后(正整数出现过设置为 1),则遍历 Bitmap 遇到 bit 位是 1 时,输入索引即可。
Bitmap 可以压缩数组,对象或任何类型的数据。我们现在使用 JSON 将大型数组从服务器传输到客户端(浏览器)。假设现在我们有一个数据集,包含了一组不同的年份,并且以不同的方式分散。
data = {
0 => 1991,
1 => 1992,
2 => 1993,
3 => 1994,
4 => 1991,
5 => 1992,
6 => 1993,
7 => 1992,
8 => 1991,
9 => 1991,
10 => 1991,
11 => 1992,
12 => 1992,
13 => 1991,
14 => 1991,
15 => 1992,
...
}
这个 JSON 将编码的信息如下:
[1991,1992,1993,1994,1991,1992,1993,1992,1991,1991,1991,1992,1992,1991,1991,1992, ...]
如果我们采用 Bitmap 去编码,会得到一个很短的数组:
data = (
0 => array(1991, '1000100011100110'),
1 => array(1992, '0100010100011001'),
2 => array(1993, '0010001000000000'),
3 => array(1994, '0001000000000000'),
)
最后,JSON 压缩之后的结果如下:
[
[1991,"1000100011100110"],
[1992,"0100010100011001"],
[1993,"0010001000000000"],
[1994,"0001000000000000"]
]
显而易见,压缩之后的效果会比未压缩要好很多。事实上,我们大多数人都知道图像的位图压缩,因为该算法主要用于图像压缩。 我们可以想象压缩黑白图像时会多么成功(因为黑白可以表示为 0 和 1)。 实际上,Bitmap 用于两种以上的颜色(例如256种),其压缩级别也是很高的。
每张图片按大小来存储,即图像的长宽像素大小。如果一张图片的像素是 100 × 100 100 \times 100 100×100,则此图像在内存的存放是一个 100 × 100 100 \times 100 100×100 的数组,每个数组的元素是 int 整型(整数占用 4 个 byte )。
数组中每个元素中整型数字含四位信息:RGBA。RGB 就是自然界三原色,通过 RGB 的组合可以将任何色彩表示出来。
举个栗子,下面的数组表示这是一张 4 × 4 4 \times 4 4×4 像素大小的全红色的图。一个像素在屏幕上显示出来非常小,当多个不同的像素按规律摆放在一起形成有行有列的数组的时候,我们就看到了图像。
{
{0xffff0000,0xffff0000,0xffff0000,0xffff0000},
{0xffff0000,0xffff0000,0xffff0000,0xffff0000},
{0xffff0000,0xffff0000,0xffff0000,0xffff0000},
{0xffff0000,0xffff0000,0xffff0000,0xffff0000}
}
在掘金上面看到了这样一个面试题:100*100 的 canvas 占多少内存?作者的解释如下:
我们在定义颜色的时候就是使用 rgba(r,g,b,a) 四个维度来表示,而且每个像素值就是用十六位 00-ff 表示,即每个维度的范围是 0~255,即 2^8 位,即 1 byte, 也就是 Uint8 能表示的范围。所以 100 * 100 canvas 占的内存是 100 * 100 * 4 bytes = 40,000 bytes。
我们通常说的图片分辨率其实是指像素数,表示长度方向的像素点数乘以宽度方向的像素点数。由于数码图片没有物理上的长宽概念,而数码图片的长宽也并非物理的长度单位,是指各自方向上的像素点数。
比如,数码相机支持 500 万像素,一般是指 25921944 或 25601920,其中第一个数字表示图片长度方向上所包含的像素点数,第二个数字表示其宽度方向上所包含的像素点数。二者的乘积 25921944 = 5038848,25601920 = 4915200,都约等于 500 万(像素)。500 万像素代表它能处理多大的图形色彩信息的能力,像素越高,需要处理时间越长,因为数组很大。
500 万像素,就是由 500 万个这样的方块或者点组成,而且像素点的尺寸是不一定的。
一台 500 万像素的数码相机拍摄的图片,这张图片的实际容量是 500万 X 3= 1500万 = 15MB ,乘以 3 是因为数码相机中的感光 CCD 是通过红、绿、蓝三色通道,所以最终图像容量就要乘以 3。
但是数码图片的实际大小会和内存大小不同,实际大小与图片采用的存储文件格式、文件头和附加信息有关。
JDK 源码中 Bitmap 是用 long[] 实现的,为了和第 3 节相对应,我们采用 int[] 实现。一个 int 整型占 4byte、32bit:
bitmap[0] 00000000000000000000000000000000 bit位区间:[0, 31]
bitmap[1] 00000000000000000000000000000000 bit位区间:[31, 63]
......
对于第 N (从 0 开始)个 bit 位在 int[] 中的计算方法如下:
在实现的时候,有人给出了位运算的方案,实现比较优雅,指定的 Bitmap 的 bit 位 N(从 0 开始):
public class BitMap {
private int[] words;
public BitMap(long capacity) {
// 计算words的下标索引
int arrayIndex = (int) (capacity >> 5);
// 计算words[arrayIndex]的偏移量
int offset = (capacity & 31) > 0 ? 1 : 0;
this.words = new int[arrayIndex + offset];
}
/**
* @param index ∈ [0, capacity)
*/
public void set(long index) {
int arrayIndex = (int) (index >> 5);
int offset = (int) (index & 31);
words[arrayIndex] |= (0x01 << offset);
}
/**
* @param index ∈ [0, capacity)
*/
public int get(long index) {
int arrayIndex = (int) (index >> 5);
int offset = (int) (index & 31);
return words[arrayIndex] >> offset & 0x01;
}
}