BitSet数据结构以及jdk中实现源码分析

本文介绍了BitSet数据结构的基础知识,包括其在算法设计中的作用和效率。接着详细探讨了Java中BitSet的实现,包括初始化、清空、反转、设置、获取状态等操作。最后,讨论了BitSet的实际应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

                                        

一. Bitset 基础

Bitset,也就是位图,由于可以用非常紧凑的格式来表示给定范围的连续数据而经常出现在各种算法设计中。上面的图来自c++库中bitset的一张图

基本原理是,用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用用的时候既可根据某一个是否为0表示此数是否出现过。

一个1G的空间,有 8*1024*1024*1024=8.58*10^9bit,也就是可以表示85亿个不同的数。


常见的应用是那些需要对海量数据进行一些统计工作的时候,比如日志分析等。

面试题中也常出现,比如:统计40亿个数据中没有出现的数据,将40亿个不同数据进行排序等。

又如:现在有1千万个随机数,随机数的范围在1到1亿之间。现在要求写出一种算法,将1到1亿之间没有在随机数中的数求出来(百度)。

programming pearls上也有一个关于使用bitset来查找电话号码的题目。


Bitmap的常见扩展,是用2位或者更多为来表示此数字的更多信息,比如出现了多少次等。


二. java中bitset的实现

Bitset这种结构虽然简单,实现的时候也有一些细节需要主要。其中的关键是一些位操作,比如如何将指定位进行反转、设置、查询指定位的状态(0或者1)等。
本文,分析一下java中bitset的实现,抛砖引玉,希望给那些需要自己设计位图结构的需要的程序员有所启发。

Bitmap的基本操作有:
  1. 初始化一个bitset,指定大小。
  2. 清空bitset。
  3. 反转某一指定位。
  4. 设置某一指定位。
  5. 获取某一位的状态。
  6. 当前bitset的bit总位数。

1. 声明

在java中,bitset的实现,位于java.util这个包中,从jdk 1.0就引入了这个数据结构。在多个jdk的演变中,bitset也不断演变。本文参照的是jdk 7.0 源代码中的实现。
声明如下:
package java.util;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.LongBuffer;

public class BitSet implements Cloneable, java.io.Serializable {、
  private long[] words;
....
....

同时我们也看到使用long数组来作为内部存储结构。这个决定了,Bitset至少为一个long的大小。下面的构造函数中也会有所体现。

2. 初始化函数

 public BitSet() {
        initWords(BITS_PER_WORD);
        sizeIsSticky = false;
    }

    public BitSet(int nbits) {
        // nbits can't be negative; size 0 is OK
        if (nbits < 0)
            throw new NegativeArraySizeException("nbits < 0: " + nbits);

        initWords(nbits);
    private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }
    private static int wordIndex(int bitIndex) {
        return bitIndex >> ADDRESS_BITS_PER_WORD;
    }
    private final static int ADDRESS_BITS_PER_WORD = 6;
    private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;

两个构造函数,分别是一个指定了初始大小,一个没指定。如果没指定,我们可以看到默认的初始大小为, 2^6 = 64-1=63 bit. 我们知道java中long的大小就是8个字节,也就是8*8=64bit。也就是说,bitset默认的是一个long整形的大小。初始化函数指定了必要的大小。
注意:如果指定了bitset的初始化大小,那么会把他规整到一个大于或者等于这个数字的64的整倍数。比如64位,bitset的大小是1个long,而65位时,bitset大小是2个long,即128位。做这么一个规定,主要是为了内存对齐,同时避免考虑到不要处理特殊情况,简化程序。




3. 清空bitset

<think>好的,我需要解析JDK8中ArrayList的removeIf方法的源码实现细节,包括每一步的功能,同时涉及Predicate过滤器和并发修改异常。首先,我应该回忆一下ArrayList的结构和常用方法。ArrayList内部使用数组存储元素,动态扩容,支持快速随机访问。 removeIf方法是Java8引入的,用于根据Predicate条件批量删除元素。用户可能想了解其内部如何高效实现过滤删除,以及如何避免或处理并发修改异常。根据引用[4],提到了retainAll和removeAll方法使用batchRemove方法,而removeIf可能也类似。 首先,我需要查看JDK8中ArrayList的removeIf源码。该方法可能遍历元素,应用Predicate,删除符合条件的元素。但具体实现可能涉及到数组元素的移动,以及modCount的处理,防止并发修改异常。 步骤一:定位到removeIf方法的源码。在JDK8的ArrayList中,removeIf方法可能使用迭代器或者内部循环,逐个检查元素是否符合Predicate条件,然后删除。但需要考虑效率,比如批量删除时如何减少数组复制的次数。 步骤二:分析源码结构。可能使用batchRemove方法,类似removeAll或retainAll,传入Predicate作为过滤条件。batchRemove方法可能有两个参数:集合和补集标志。在removeIf中,可能将Predicate转换为条件,并调用batchRemove。 步骤三:具体到batchRemove的实现。根据引用[4],retainAll和removeAll调用batchRemove,第二个参数控制保留还是删除。对于removeIf,可能类似,遍历数组,将保留的元素前移,最后截断数组并更新size。 同时需要注意并发修改的问题。ArrayList的迭代器有modCount机制,如果在迭代过程中结构被修改(如直接调用remove方法),会抛出ConcurrentModificationException。但removeIf是原子操作,内部会处理modCount的变化,所以不会触发这个异常,前提是在使用迭代器时没有其他线程或代码段同时修改。 接下来需要详细分析源码的每一步,比如如何遍历元素,应用Predicate,移动元素,以及修改modCount和size。同时,解释Predicate的作用,以及为何使用它来过滤元素。 可能遇到的难点是理解batchRemove中的具体逻辑,比如如何确定哪些元素需要保留,如何高效移动元素,以及如何处理结构变化后的数组。此外,需要解释为什么removeIf不会引发并发修改异常,而手动在迭代中删除可能会。 总结,removeIf的实现步骤大致为:检查Predicate是否为null,遍历内部数组,应用Predicate测试每个元素,将需要保留的元素复制到数组前部,最后截断数组并更新size和modCount。整个过程是同步的,避免了并发修改的问题,但如果在外部有其他修改,比如在遍历时调用removeIf,仍可能引发异常。</think>### JDK8 ArrayList removeIf方法源码解析 #### 一、方法入口与参数 `removeIf(Predicate<? super E> filter)`是`ArrayList`中实现批量删除的核心方法: ```java @Override public boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); return batchRemove(filter, false); } ``` - **功能**:接受一个`Predicate`(断言条件),删除所有满足条件的元素[^3]。 - **关键步骤**:调用内部方法`batchRemove`,第二个参数`false`表示删除满足条件的元素[^4]。 #### 二、batchRemove方法实现 ```java private boolean batchRemove(Predicate<? super E> filter, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (filter.test((E) elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { if (r != size) { // 发生异常时恢复数据 System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // 清理多余元素并更新状态 for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; } ``` #### 三、逐步骤解析 1. **初始化指针**: - `r`:读取指针(遍历原数组)。 - `w`:写入指针(记录保留元素的当前位置)。 2. **遍历与过滤**: - 循环检查每个元素`elementData[r]`是否满足条件。 - 如果`filter.test(elementData[r]) == complement`,则保留该元素(拷贝到`w`位置,`w++`)。 - **示例**:当`complement=false`时,满足`filter.test(element)=true`的元素会被跳过(即删除)。 3. **异常处理**: - 如果遍历过程中发生异常(如`Predicate`抛出错误),通过`finally`块将剩余未处理的元素复制到数组前部,确保数据完整性。 4. **清理与更新**: - 如果`w < size`,说明有元素被删除,将`w`之后的位置置`null`(帮助GC回收)。 - 更新`modCount`(结构修改次数)和`size`(实际元素数量)。 #### 四、并发修改异常(ConcurrentModificationException) - **触发条件**:在迭代过程中(如使用`for-each`或`Iterator`),若直接调用`ArrayList`的`add/remove`方法修改结构,会导致`modCount`变化,迭代器检测到后会抛出异常。 - **removeIf的规避**: - `removeIf`是原子操作,内部统一修改`modCount`,不会导致迭代器中的`expectedModCount`不一致。 - 但如果外部在迭代过程中调用`removeIf`,仍会触发异常,例如: ```java ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b")); for (String s : list) { list.removeIf(e -> e.equals("a")); // 抛出ConcurrentModificationException } ``` #### 五、Predicate过滤器的作用 - `Predicate`是一个函数式接口,定义条件判断逻辑(如`e -> e.startsWith("a")`)。 - 在`removeIf`中,每个元素会经过`Predicate.test()`判断,返回`true`则被删除。 #### 六、性能特点 - **时间复杂度**:$O(n)$,单次遍历完成过滤和元素移动。 - **空间复杂度**:$O(1)$,原地操作数组,无需额外空间。 ### 示例代码 ```java ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c")); list.removeIf(s -> s.equals("b")); // 删除所有"b" System.out.println(list); // 输出 [a, c] ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值