數據算法: 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;
    }
}

掃碼關注公衆號:冰山烈焰的黑板報
在這裏插入圖片描述

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