【数据结构:从0到1】-13-位运算与位数据结构

引言:为什么是“位”?

举个例子:你需要记录 1000 万个用户的在线状态(在线为1,离线为0)。如果用我们熟悉的 boolean 数组(在Java中,每个 boolean 约占1字节),那么需要的内存大约是:
10,000,000 * 1 byte ≈ 10 MB

但如果我们换一种思路,用一个比特(bit)来存储一个状态呢?
10,000,000 * 1 bit ≈ 1.25 MB

看到了吧?仅仅是换了一种存储方式,就大大节省了内存空间!这就是位数据结构的魅力。通过直接操作二进制位,将空间效率提升到了极致。

在高性能计算、海量数据处理、数据库索引等场景下,这种效率提升往往是至关重要的。而要掌握位数据结构,我们必须先熟悉它们的基础——位运算


一、 位运算

就是直接对整数在内存中的二进制位进行操作。它通常由CPU直接提供指令支持,因此速度极快,是许多底层优化的不二选择。

我们先来学习一下六种基本位运算符(以 Java/C 语法为例):

运算符 名称 描述 示例 (二进制)
& 两位都为1时,结果才为1 1010 & 1100 = 1000
| 两位有一位为1时,结果就为1 1010 | 1100 = 1110
^ 异或 两位相同为0,相异为1 1010 ^ 1100 = 0110
~ 取反 对每一位取反,0变1,1变0 ~1010 = 0101 (视位宽而定)
<< 左移 各二进位全部左移若干位,高位丢弃,低位补0 1010 << 2 = 101000
>> 右移 各二进位全部右移若干位,对无符号数,高位补0对有符号数,各编译器处理方法不一样 1010 >> 2 = 0010 (逻辑右移)

注意:在Java中,有明确的 >>> 运算符表示无符号右移(高位补0),而 >> 是带符号右移(高位补符号位)。

光知道定义可不行,下面这些“骚操作”才是位运算的精髓所在。

1. 判断奇偶性

技巧:使用 & 1

  • 偶数的最低位是0,奇数的最低位是1。
  • x & 1 的结果如果为 0,则 x 为偶数;如果为 1,则 x 为奇数。
public static boolean isEven(int x) {
   
   
    // 与 1 进行与运算,只看最后一位
    return (x & 1) == 0;
}

// 测试
System.out.println(isEven(4)); // true
System.out.println(isEven(7)); // false

这比 x % 2 == 0 在底层效率更高。

2. 交换两个数

技巧:使用 ^ 异或运算
异或运算有一个神奇的性质:a ^ b ^ b = a。我们可以利用这个性质,不需要临时变量就能交换两个数。

int a = 5; // 二进制 0101
int b = 3; // 二进制 0011

a = a ^ b; // a 现在为 0101 ^ 0011 = 0110 (6)
b = a ^ b; // b 现在为 0110 ^ 0011 = 0101 (5) -> 原来的a
a = a ^ b; // a 现在为 0110 ^ 0101 = 0011 (3) -> 原来的b

System.out.println("a=" + a + ", b=" + b); // a=3, b=5

原理流程图

第三步: a = a ^ b
第二步: b = a ^ b
第一步: a = a ^ b
初始状态
a: 3
6^5=3
即原b的值
b: 5
a: 6
b: 5
6^3=5
即原a的值
a: 6
5^3的结果
b: 3
a: 5
b: 3
3. 判断是否为2的幂次方

技巧:使用 & (x - 1)

  • 一个数如果是2的幂次方,它的二进制表示中一定有且仅有一个1。例如:1 (1), 2 (10), 4 (100), 8 (1000)
  • x - 1 会将最低位的1变成0,后面的所有0变成1。例如 8 (1000) - 1 = 7 (0111)
  • 那么 x & (x - 1) 的结果就会是0。如果结果是0,则 x 是2的幂次方(x > 0)。
public static boolean isPowerOfTwo(int x) {
   
   
    // 注意要排除x=0的情况,因为0 & -1 = 0,但0不是2的幂
    return x > 0 && (x & (x - 1)) == 0;
}

// 测试
System.out.println(isPowerOfTwo(8));  // true
System.out.println(isPowerOfTwo(10)); // false
System.out.println(isPowerOfTwo(0));  // false
4. 取最低位的1

技巧:使用 x & -x

  • -x 在计算机中是以补码形式存储的,它等于 ~x + 1
  • 这个操作会将 x 最低位的1保留下来,其他位全部置0。这个技巧是很多复杂位操作的基础。
int x = 12;                 // 二进制 1100
int lowBit = x & -x;        // 结果是 4 (二进制 0100)
System.out.println(lowBit); // 4
5. 移除最低位的1

技巧:使用 x & (x - 1)

  • 这个操作我们在判断2的幂时用过,它可以直接将数字二进制表示中最低位的1变成0。
int x = 12;            // 二进制 1100
x = x & (x - 1);       // x 现在为 8 (二进制 1000),最低位的1被移除了
System.out.println(x); // 8

这个操作常用于统计一个数的二进制表示中有多少个1。

小结一下
位运算虽然看起来简单,但组合起来能解决许多看似复杂的问题。熟练掌握它们是理解后续位数据结构的关键。


二、 位图(Bitmap)

掌握了位运算,我们就可以开始构建第一个位数据结构——位图(Bitmap),也叫位集合(Bitset)。

2.1 什么是位图?

核心思想:用一个比特位(bit)来标记某个元素对应的值或者状态。由于每个比特位只有0和1两种状态,所以它非常适合用来表示“存在”或“不存在”这种二值信息。

我们可以把位图想象成一个超长的、只有0和1的数组。比如,我们要表示数字 {2, 5, 9},就可以用一个长度为10的位图,在第2、5、9位设置为1,其他位为0:

索引: 0 1 2 3 4 5 6 7 8 9
值:   0 0 1 0 0 1 0 0 0 1
2.2 如何实现一个位图?

在编程语言中,我们通常不能直接申请一个bit数组,最小单位是字节(byte)。所以,我们需要用一个基本类型的数组(如 int[]long[])来“模拟”一个bit数组。

实现思路

  1. 确定容量:假设我们要存储 n 个元素的状态。
  2. 选择底层数组:一个 int 是32位。那么存储 n 个bit,需要 (n + 31) / 32int
  3. 定位:给定一个数字 x,我们需要找到它在数组中的哪个 int 的哪一位上。
    • 数组下标x / 32 (在代码中我们用位运算 x >> 5,因为 32 = 2^5)
    • 位偏移x % 32 (在代码中我们用 x & 31,因为 31 的二进制是 11111)

核心代码实现

public class MyBitSet {
   
   
    // 使用 int 数组存储位信息
    private int[] words;

    /**
     * 构造一个BitSet,至少能表示 [0, nBits-1] 的范围
     * @param nBits 位的数量
     */
    public MyBitSet(int nBits) {
   
   
        // (nBits + 31) / 32 是为了向上取整,确保位数足够
        this.words = new int[(nBits + 31) >> 5];
    }

    /**
     * 将第 bitIndex 位设置为1
     * @param bitIndex 位的索引
     */
    public void set(int bitIndex) {
   
   
        // 定位到 words 数组的哪个下标,以及在该下标的哪个位
        int wordIndex = bitIndex >> 5; // 相当于 bitIndex / 32
        int bitOffset = bitIndex & 31; // 相当于 bitIndex % 32
        // 使用位或操作 | 将指定位置1,其他位不变
        // 1 << bitOffset 创建了一个只有目标位是1的掩码
        words[wordIndex] |= (1 << bitOffset)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QuantumLeap丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值