之前写过一篇《BitSet 源码解析》,
由于篇幅所限,没有给出使用的例子,
简单找个例子说明下。
一、ArrayList 中 BItSet的使用
ArrayList
的 removeIf
方法的实现中,
很巧妙的用了 BitSet
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
这段代码的大概逻辑如下:
遍历数组,标记出要删除的下标
数组数据原地迁移
空位设置为null,方便后续GC回收
BitSet removeSet = new BitSet(size);
if (filter.test(element)) {
removeSet.set(i); // 标记需要删除的下标,
removeCount++;
}
数组原地迁移数据,空间复杂度就是 O(1),厉害
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
假设原数组长度是10,其中 下标 3,4,7 是要删除的位置。
i = removeSet.nextClearBit(i);
这个方法,
从入参下标开始,找到第1个,值为0的下标,将其返回。
结合上图 nextClearBit(2)
返回 2,nextClearBit(3)
返回5
如果不明白,看下 《BitSet 源码解析》
第1次循环,i = 0, j=0,elementData[0] = elementData[0];
第2次循环,i = 1, j=1,elementData[1] = elementData[1];
第3次循环,i = 2, j=2,elementData[2] = elementData[2];
第4次循环,i = 5, j=3,elementData[3] = elementData[5];
第5次循环,i = 6, j=4,elementData[4] = elementData[6];
第6次循环,i = 8, j=5,elementData[5] = elementData[8];
第6次循环,i = 9, j=6,elementData[6] = elementData[9];
它就是这么实现的原地复制。
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
这段代码比较简单,略!
从时间复杂度来讲,第1次循环,标记删除元素,
第2次循环,迁移数据,时间复杂度应该是 O(n)。
数组本身就是这个特点,也不大可能再被优化了。
从空间复杂度来说,借助 BitSet 实现原地复制,
空间复杂度,大约是 O(1) 了,这是很棒的实现。
二、列举出 200000 以内所有质数。
方法有很多,说一个暴力求解的,比如说 100 以内的质数
2 * 3
2 * 4
…
2 * 50
3 * 4
3 * 5
…
3 * 34
4 * 5
4 * 6
…
4 * 25
…
9 * 10
9 * 11
10 * 10
把 100 以内,所有的数相乘,得到所有的 和数,其它的就是质数
public static void main(String[] args) {
BitSet bitSet = new BitSet(n);
int n = 100;
double sqrt = Math.sqrt(n); // 求出平方根
int i = 2;
for(; i <= sqrt; i++){
for(int j = i; i * j <= n; j++){
bitSet.set(i*j);
}
}
for(int k = 3; k <= n; k = bitSet.nextClearBit(++k)){
System.out.println(k);
}
}
这只是个大概的思路,有兴趣可以优化下。
三、布隆过滤器
面试中如果问到缓存穿透问题,多多少少会提下布隆过滤器。
布隆过滤器,可以减少绝大多数缓存穿透问题。
相关的文章,网上很多,有兴趣可以自己找下。
大概的场景就是,对于某个 key 值,缓存中没有,
就查数据库,数据库也没有。本来这个没什么问题。
如果这种查询特别多,还是很消耗数据库的性能。
把数据库存在的值,做一个布隆过滤器,
如果一个key 值,布隆过滤器里没有,数据库里一定没有。
反之不亦然,这也是布隆过滤器的缺点。你用它的优点就好了。
总结
BitSet 做存储时,可大大降低内存的使用,在特定的场景中,很有用。
本质是位运算,没有特别难的地方,
它不是银弹,还是用在合适的地方好。