作者:Grey
原文地址:
本文內容使用的程式語言是 Java。其他語言有類似的資料結構。
在 Java 中,使用HashSet
可以實現如下操作:
add(T v)
加入一個元素到
HashSet
中,重複則覆蓋。
contains(T v)
判斷一個元素是否加入過
HashSet
。
remove(T v)
從
HashSet
中刪除一個元素。
如果資料範圍固定,使用點陣圖比使用HashSet
省空間。
在 Java 中,一個 int 型別的整數可以表示 32 個 bit,所以,如果資料範圍0 ~ 31
,可以直接用一個 int 型別的數來完成上述三個操作。
比如:
add(4)
這個操作,就是在如下 int 型別數(二進位制表示)中,第 4 號位置設定為 1。如下圖
繼續執行add(7)
這個操作,就是在如下 int 型別數(二進位制表示)中,第 7 號位置設定為 1。如下圖
contains(4)
這個操作,就是判斷 4 號位置是 0 還是 1,如果是 1, 就說明 4 存在,如果是 0 ,說明 4 不存在。
remove(7)
這個操作,就是把 7 號位置置為 0。如下效果
如果資料範圍是 0 ~ 1023
, 則可以用一個 int 型別陣列來表示,這個陣列只需要 32 個元素即可。因為 32 個 int 型別元素,可以表示 1024 位,正好可以覆蓋資料範圍中的所有數位。對於0 ~ 1023
中任意一個數 num,num在陣列中存在第num / 32
個元素中的第num % 32
位中。
舉例說明:
num = 37
,客觀上,num 應該在如下位置:
在 1 號(37 / 32)陣列元素的第 5 號( 37 % 32)位置。
為了擴大表示範圍,我們可以使用 long 型別來替代 int 型別,因為 long 型別可以表達 64 個 bit,思路還是和如上一樣。現在說明如何實現上述三個方法。
先把點陣圖的資料結構和相關方法定義好
public static class BitMap {
// 使用每個位置的資訊。
private long[] bits;
public BitMap(int max) {
// TODO
// 點陣圖初始化
}
public void add(int num) {
// TODO
// 新增一個元素
}
public void remove(int num) {
// TODO
// 刪除一個元素
}
public boolean contains(int num) {
// TODO
// 判斷一個元素是否在點陣圖中
}
}
注:我們這裡只考慮非負數,對於負數的情況,也可以轉換成正數來處理,比如:-3~6
,可以轉換成0~9
。
首先是點陣圖的初始化,即如何根據資料範圍確定點陣圖應該開闢多大的陣列?
由於是 long 型別,所以,對於 0 ~ x
區間來說,需要準備
(x + 64) / 64
這麼大的 long 型別陣列。
點陣圖中增加一個元素,比如我們要增加 53 這個元素,先定位它是陣列中的哪個元素,即53 / 64 = 0
,第 0 號位置的元素,再定位是這個元素中的第幾位,即:53 % 64 = 11
,即第 11 位,我們可以用 1L << 11
後的值| bit[0]
即可,程式碼實現如下
public void add(int num) {
bits[num / 64] |= (1L << (num % 64));
}
由於 num / 64
其實就是 num >> 6
,
num % 64
其實就是num & 63
,
由於位運算比算術運算效率要高,所以add
方法可以進一步寫成如下形式
public void add(int num) {
// bits[num / 64] |= (1L << (num % 64));
// num % 64 ---> num & 63
// 只適用於 2 的 n 次方
bits[num >> 6] |= (1L << (num & 63));
}
點陣圖中刪除一個元素,其實就是把對應位置的二進位制位置為 0,其他位置保持不變,通過
~((1L << (num & 63)))
可以預先得到一個除目標位置是 0,其他位置都是 1 的數。
然後通過這個數去 &
上陣列目標位置的元素,即可把對應位置的 1 改為 0,其他位置不變。
public void remove(int num) {
bits[num >> 6] &= ~(1L << (num & 63));
}
點陣圖中是否包含某個元素,其實就是判斷對應位置是 0 還是 1, 如果是 0 ,就說明存在,不是 0 , 則不存在。
public boolean contains(int num) {
return (bits[num >> 6] & (1L << (num & 63))) != 0;
}
點陣圖的完整程式碼見
public static class BitMap {
private long[] bits;
public BitMap(int max) {
// 準備多少個整數? 0 ~ 63 需要1個整數
// >> 6 就是 除以 64
bits = new long[(max + 64) >> 6];
}
public void add(int num) {
// bits[num / 64] |= (1L << (num % 64));
// num % 64 ---> num & 63
// 只適用於 2 的 n 次方
bits[num >> 6] |= (1L << (num & 63));
}
public void remove(int num) {
bits[num >> 6] &= ~(1L << (num & 63));
}
public boolean contains(int num) {
return (bits[num >> 6] & (1L << (num & 63))) != 0;
}
}
通過實現的點陣圖和 Java 自帶的 HashSet 進行對比測試,可以判斷我們寫的點陣圖是否正確,測試程式碼如下
package snippet;
import java.util.HashSet;
import java.util.Set;
public class Code_0009_BitMap {
public static void main(String[] args) {
System.out.println("測試開始!");
int max = 70000;
BitMap bitMap = new BitMap(max);
Set<Integer> set = new HashSet<>();
int testTime = 90000000;
for (int i = 0; i < testTime; i++) {
int num = (int) (Math.random() * (max + 1));
double decide = Math.random();
if (decide < 0.333) {
bitMap.add(num);
set.add(num);
} else if (decide < 0.666) {
bitMap.remove(num);
set.remove(num);
} else {
if (bitMap.contains(num) != set.contains(num)) {
System.out.println("Oops!");
break;
}
}
}
for (int num = 0; num <= max; num++) {
if (bitMap.contains(num) != set.contains(num)) {
System.out.println("Oops!");
}
}
System.out.println("測試結束!");
}
}
執行,未列印報錯資訊,說明我們的演演算法正確。
本文來自部落格園,作者:Grey Zeng,轉載請註明原文連結:https://www.cnblogs.com/greyzeng/p/16634282.html