Java的BitSet原理及应用

原理

众所周知,Java的BitSet使用一个Long(一共64位)的数组中的没一位(bit)是否为1来表示当前Index的数存在不。但是BitSet又是如何实现的呢?其实只需要理解其中的两个方法:

set

get

就能够理解BitSet的实现原理是什么了。

set

先看源代码:

public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex);
words[wordIndex] |= (1L << bitIndex); // Restores invariants
checkInvariants();
}

除了判断给的值是否小于0的那两句,我们依次来看一下这个函数的每一句代码。

wordIndex

第一句就是计算wordIndex,通过wordIndex函数获取值。代码如下:

private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}

这里ADDRESS_BITS_PER_WORD的值是6,那么最先想到的问题就是:为什么是6呢?而不是其他值呢?

答案其实很简单,还记得在最开始提到的:BitSet里使用一个Long数组里的每一位来存放当前Index是否有数存在。

因为在Java里Long类型是64位,所以一个Long可以存储64个数,而要计算给定的参数bitIndex应该放在数组(在BitSet里存在word的实例变量里)的哪个long里,只需要计算:bitIndex / 64即可,这里正是使用>>来代替除法(因为位运算要比除法效率高)。而64正好是2的6次幂,所以ADDRESS_BITS_PER_WORD的值是6。

通过wordIndex函数就能计算出参数bitIndex应该存放在words数组里的哪一个long里。

expandTo
private void expandTo(int wordIndex) {
int wordsRequired = wordIndex+1;
if (wordsInUse < wordsRequired) {
ensureCapacity(wordsRequired);
wordsInUse = wordsRequired;
}
}

从上面已经知道在BitSet里是通过一个Long数组(words)来存放数据的,这里的expandTo方法就是用来判断words数组的长度是否大于当前所计算出来的wordIndex(简单的说,就是能不能存的下),如果超过当前words数组的长度(记录在实例变量wordsInUse里),也即是存不下,则新加一个long数到words里(ensureCapacity(wordsRequired)所实现的。)。

Restores invariants

words[wordIndex] |= (1L << bitIndex); // Restores invariants

这一行代码可以说是BitSet的精髓了,先不说什么意思,我们先看看下面代码的输出:

System.out.println(1<<0);
System.out.println(1<<1);
System.out.println(1<<2);
System.out.println(1<<3);
System.out.println(1<<4);
System.out.println(1<<5);
System.out.println(1<<6);
System.out.println(1<<7);

输出是:

1

2

4

8

16

32

64

128

这个输出看出规律没有?就是2的次幂,但是还是不太好理解,我们用下面的输出,效果会更好:

System.out.println(Integer.toBinaryString(1<<0));
System.out.println(Integer.toBinaryString(1<<1));
System.out.println(Integer.toBinaryString(1<<2));
System.out.println(Integer.toBinaryString(1<<3));
System.out.println(Integer.toBinaryString(1<<4));
System.out.println(Integer.toBinaryString(1<<5));
System.out.println(Integer.toBinaryString(1<<6));
System.out.println(Integer.toBinaryString(1<<7));

输出是:

1

10

100

1000

10000

100000

1000000

10000000

从而发现,上面所有的输出力,1 所在的位置,正好是第1,2,3,4,5,6,7,8(Java数组的Index从0开始)位。而BitSet正是通过这种方式,将所给的bitIndex所对应的位设置成1,表示这个数已经存在了。这也解释了(1L << bitIndex)的意思(注意:因为BitSet是使用的Long,所以要使用1L来进行位移)。

搞懂了(1L << bitIndex),剩下的就是用|来将当前算出来的和以前的值进行合并了words[wordIndex] |= (1L << bitIndex);。

剩下的checkInvariants就没什么好解释的了。

get

搞懂了set方法,那么get方法也就好懂了,整体意思就是算出来所给定的bitIndex所对应的位数是否为1即可。先看看代码:

public boolean get(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}

计算wordIndex在上面set方法里已经说明了,就不再细述。这个方法里,最重要的就只有:words[wordIndex] & (1L << bitIndex)。这里(1L << bitIndex)也已经做过说明,就是算出一个数,只有bitIndex位上为1,其他都为0,然后再和words[wordIndex]做&计算,如果words[wordIndex]数的bitIndex位是0,则结果就是0,以此来判断参数bitIndex存在不。

运用

搞清楚了原理,就可以很好的运用了,不过这里不讲怎么使用Java的BitSet,网上有很多。我讲一个我在刷Leetcode时碰见的一个题,就可以使用BitSet的原理来实现。

题目如下:

Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, find the one that is missing from the array.

For example, Given nums = [0, 1, 3] return 2.

Note: Your algorithm should run in linear runtime complexity. Could you implement it using only constant extra space complexity?

翻译过来就是:从0到n之间取出n个不同的数,找出漏掉的那个。 注意:你的算法应当具有线性的时间复杂度。你能实现只占用常数额外空间复杂度的算法吗?

这正好可以使用BitSet的思想来实现。

public int missingNumberInByBitSet(int[] array) {
int bitSet = 0;
for (int element : array) {
bitSet |= 1 << element;
}
for (int i = 0; i < array.length; i++) {
if ((bitSet & 1 << i) == 0) {
return i;
}
}
return 0;
}

这里是简单实现,使用int,而不是lang,也没有用一个数组,但是核心思想是这样的。