原理
BitSet是位操作的對象,值只有0或1即false和true,內部維護了一個long數組,初始只有一個long,所以BitSet最小的size是64,當隨着存儲的元素越來越多,BitSet內部會動態擴充,最終內部是由N個long來存儲,這些針對操作都是透明的。
用1位來表示一個數據是否出現過,0為沒有出現過,1表示出現過。使用用的時候既可根據某一個是否為0表示,此數是否出現過。
一個1G的空間,有 8102410241024=8.5810^9bit,也就是可以表示85億個不同的數。
注意:在沒有外部同步的情況下,多個線程操作一個BitSet是不安全的。
例子
比如有一堆數字,需要存儲,source=[3,5,6,9]
用int就需要4*4個字節。
java.util.BitSet可以存true/false。
如果用java.util.BitSet,則會少很多,其原理是:
1,先找出數據中最大值maxvalue=9
2,聲明一個BitSet bs,它的size是maxvalue+1=10
3,遍歷數據source,bs[source[i]]設置成true.
最后的值是:
(0為false;1為true)
bs [0,0,0,1,0,1,1,0,0,1]
3, 5,6, 9
這樣一個本來要int型需要占4字節共32位的數字現在只用了1位!
比例32:1
這樣就省下了很大空間
通常用在數據統計、分析的領域。
初始化邏輯
初始化大小 默認就一個long元素,邏輯如下:
private final static int ADDRESS_BITS_PER_WORD = 6;
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
//用來開辟bit位空間
private long[] words;
//當前long數組的大小
private transient int wordsInUse = 0;
public BitSet() {
initWords(BITS_PER_WORD);
sizeIsSticky = false;
}
private void initWords(int nbits) {
words = new long[wordIndex(nbits-1) + 1];
}
//bitIndex除去64(bitIndex >> 6 )得到會落到long數組的index;
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
BitSet的基本運算
public class BitUtils {
/**
* 獲取運算數指定位置的值<br>
* 例如: 0000 1011 獲取其第 0 位的值為 1, 第 2 位 的值為 0<br>
*
* @param source
* 需要運算的數
* @param pos
* 指定位置 (0<=pos<=7)
* @return 指定位置的值(0 or 1)
*/
public static byte getBitValue(byte source, int pos) {
return (byte) ((source >> pos) & 1);
}
/**
* 將運算數指定位置的值置為指定值<br>
* 例: 0000 1011 需要更新為 0000 1111, 即第 2 位的值需要置為 1<br>
*
* @param source
* 需要運算的數
* @param pos
* 指定位置 (0<=pos<=7)
* @param value
* 只能取值為 0, 或 1, 所有大於0的值作為1處理, 所有小於0的值作為0處理
*
* @return 運算后的結果數
*/
public static byte setBitValue(byte source, int pos, byte value) {
byte mask = (byte) (1 << pos);
if (value > 0) {
source |= mask;
} else {
source &= (~mask);
}
return source;
}
/**
* 將運算數指定位置取反值<br>
* 例: 0000 1011 指定第 3 位取反, 結果為 0000 0011; 指定第2位取反, 結果為 0000 1111<br>
*
* @param source
*
* @param pos
* 指定位置 (0<=pos<=7)
*
* @return 運算后的結果數
*/
public static byte reverseBitValue(byte source, int pos) {
byte mask = (byte) (1 << pos);
return (byte) (source ^ mask);
}
/**
* 檢查運算數的指定位置是否為1<br>
*
* @param source
* 需要運算的數
* @param pos
* 指定位置 (0<=pos<=7)
* @return true 表示指定位置值為1, false 表示指定位置值為 0
*/
public static boolean checkBitValue(byte source, int pos) {
source = (byte) (source >>> pos);
return (source & 1) == 1;
}
/**
* 入口函數做測試<br>
*
* @param args
*/
public static void main(String[] args) {
// 取十進制 11 (二級制 0000 1011) 為例子
byte source = 11;
// 取第2位值並輸出, 結果應為 0000 1011
for (byte i = 7; i >= 0; i--) {
System.out.printf("%d ", getBitValue(source, i));
}
// 將第6位置為1並輸出 , 結果為 75 (0100 1011)
System.out.println("\n" + setBitValue(source, 6, (byte) 1));
// 將第6位取反並輸出, 結果應為75(0100 1011)
System.out.println(reverseBitValue(source, 6));
// 檢查第6位是否為1,結果應為false
System.out.println(checkBitValue(source, 6));
// 輸出為1的位, 結果應為 0 1 3
for (byte i = 0; i < 8; i++) {
if (checkBitValue(source, i)) {
System.out.printf("%d ", i);
}
}
}
}
BitSet的應用一——排序
/**
* 問題重述:一個最多包含n個正整數的文件,每個數都小於n,其中n=107,並且沒有重復。
* 最多有1MB內存可用。要求用最快方式將它們排序並按升序輸出。
*/
import java.util.BitSet;
import java.util.Scanner;
/**
* 解決思路
* 將文件中的數讀入,把數字對應的bit位設置為1,最后,將bit位為1的按序輸出。
*/
public class SortByBit {
public static void main(String args[]) {
//輸入數字
int n;
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
BitSet bitSet = new BitSet();
for (int i = n; i>0;i--) {
bitSet.set(sc.nextInt());
}
//輸出
for (int i = bitSet.size(); i>0; i--) {
if (bitSet.get(i))
System.out.print(i + " ");
}
}
}
//輸出
3
1 20 2
20 2 1
應用二——字符串判重
BitSet bitSet = new BitSet(Integer.MAX_VALUE);//hashcode的值域
//0x7FFFFFFF (int類型的最大值,第一位是符號位,可用Integer.MAX_VALUE代替)
String url = "http://baidu.com/a";
int hashcode = url.hashCode() & 0x7FFFFFFF;
bitSet.set(hashcode);
System.out.println(bitSet.cardinality()); //狀態為true的個數
System.out.println(bitSet.get(hashcode)); //檢測存在性
bitSet.clear(hashcode); //清除狀態
為什么使用long,不用int?
JDK選擇long數組作為BitSet的內部存儲結構是出於性能的考慮,因為BitSet提供and和or這種操作,需要對兩個BitSet中的所有bit位做and或者or,實現的時候需要遍歷所有的數組元素。使用long能夠使得循環的次數降到最低,所以Java選擇使用long數組作為BitSet的內部存儲結構。
從數據在棧上的存儲來說,使用long和byte基本是沒有什么差別的,除了編譯器強制地址對齊的時候,使用byte最多會浪費7個字節(強制按照8的倍數做地址對其),另外從內存讀數組元素的時候,也是沒有什么區別的,因為匯編指令有對不同長度數據的mov指令。所以說,JDK選擇使用long數組作為BitSet的內部存儲結構的根本原因就是在and和or的時候減少循環次數,提高性能。