数据算法: Bitmap

1. 初识 Bitmap

Bitmap 也被称为位图。Bitmap 既是一种数据结构,又是一种图片类型。从数据结构的角度讲,Bitmap 适用于以下场景,后文会逐一进行阐述:

  1. 判重
  2. 定基
  3. 排序
  4. 压缩

2. 数据结构

Bitmap 是指由多个二进制位组成的数组,数组中的每个二进制位都有与其对应的索引。可以使用索引对二进制位进行操作。如下图表示 16 位的 Bitmap:
图1 16 位 Bitmap
数据 {0, 4, 9, 10, 13} 存入 Bitmap 如图2 所示:
图2 存入数据后的 Bitmap

判重

判重是指一个元素是否在一个数据集中是否重复出现或存在。在数据处理领域,判重是个很常见的需求。搬个网上的栗子:给一台普通 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,这样就可以放到内存里进行计算了。这里用两种方法可以处理:

  1. 很多语言如 Java、C++ 都有现成的 Bitmap 数据结构,索引即 int 整数,索引对应的值即该是否存在,即是否重复。
  2. 用 int[] 数组,每个索引元素表示 4Byte * 8 = 32bit,int 整数除以 32 的结果表示 int[] 数组的索引 Index,int 整数对 32 取模的结果表示 int[] 数组在索引 Index 上所在的偏移量。

定基

定基是指一个数据集中存在多少不同的元素,即数据集的基数。举个栗子:某网站有 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 时,输入索引即可。

3. 压缩

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种),其压缩级别也是很高的。

4. 位图图像

每张图片按大小来存储,即图像的长宽像素大小。如果一张图片的像素是 100×100100 \times 100,则此图像在内存的存放是一个 100×100100 \times 100 的数组,每个数组的元素是 int 整型(整数占用 4 个 byte )。

数组中每个元素中整型数字含四位信息:RGBA。RGB 就是自然界三原色,通过 RGB 的组合可以将任何色彩表示出来。

  1. R:Red 红色通道(占一个 byte 取值 0~255)
  2. G:Green 绿色通道色(占一个 byte 取值 0~255 )
  3. B:Blue 蓝色通道(占一个 byte 取值 0~255 )
  4. A:Alpha 通道值,即该位置像素点的透明值(占一个 byte 取值 0~255)

举个栗子,下面的数组表示这是一张 4×44 \times 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。

5. 扩展:数码相机的图片

我们通常说的图片分辨率其实是指像素数,表示长度方向的像素点数乘以宽度方向的像素点数。由于数码图片没有物理上的长宽概念,而数码图片的长宽也并非物理的长度单位,是指各自方向上的像素点数。

比如,数码相机支持 500 万像素,一般是指 25921944 或 25601920,其中第一个数字表示图片长度方向上所包含的像素点数,第二个数字表示其宽度方向上所包含的像素点数。二者的乘积 25921944 = 5038848,25601920 = 4915200,都约等于 500 万(像素)。500 万像素代表它能处理多大的图形色彩信息的能力,像素越高,需要处理时间越长,因为数组很大。

500 万像素,就是由 500 万个这样的方块或者点组成,而且像素点的尺寸是不一定的。

数码图片的计算大小和实际大小

一台 500 万像素的数码相机拍摄的图片,这张图片的实际容量是 500万 X 3= 1500万 = 15MB ,乘以 3 是因为数码相机中的感光 CCD 是通过红、绿、蓝三色通道,所以最终图像容量就要乘以 3。

但是数码图片的实际大小会和内存大小不同,实际大小与图片采用的存储文件格式、文件头和附加信息有关。

6. Bitmap 的实现

JDK 源码中 Bitmap 是用 long[] 实现的,为了和第 3 节相对应,我们采用 int[] 实现。一个 int 整型占 4byte、32bit:

bitmap[0]  00000000000000000000000000000000		bit位区间:[0, 31]
bitmap[1]  00000000000000000000000000000000		bit位区间:[31, 63]	
......

对于第 N (从 0 开始)个 bit 位在 int[] 中的计算方法如下:

  1. N/32:表示 int[] 的下标索引 IDX;
  2. N%32:表示 int[IDX] 中的偏移量。

在实现的时候,有人给出了位运算的方案,实现比较优雅,指定的 Bitmap 的 bit 位 N(从 0 开始):

  1. N >> 5:表示 int[] 的下标索引 IDX;
  2. N & 31:表示 int[IDX] 中的偏移量。
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;
    }
}

扫码关注公众号:冰山烈焰的黑板报
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章