位图思想详解:用一个小小的比特征服整个世界

一、什么是位图?

位图是一种超级节省空间的数据结构,他利用二进制位(0/1)来表示某个元素是否存在或某种状态是否为真。想象一下,用一个小小的比特位就能记录一个信息,这简直是数据存储界的"极简主义"!

二、位图的形象理解

想象你是一个小学班主任,班上有40个学生。周一早上,你需要记录谁到校了,谁没到。

  • 传统方法:创建一个布尔数组boolean[] attended = new boolean[40],这要占用40个布尔值的内存。
  • 位图方法:只需要申请一个64位的long类型变量,每一个对应一个学生的出勤率。这就像是一个微型的"打卡机",每个学生占用一个小格子,来了就打个勾(1),没来就空着(0)。

三、位图的 Java 实现

public class Bitmap {
    private long[] bits;
    
    // 假设我们要表示n个元素
    public Bitmap(int n) {
        // 每个long有64位,所以需要 (n+63)/64 个long
        this.bits = new long[(n + 63) / 64];
    }
    
    // 设置第i位为1(表示存在)
    public void set(int i) {
        bits[i / 64] |= 1L << (i % 64);
    }
    
    // 设置第i位为0(表示不存在)
    public void clear(int i) {
        bits[i / 64] &= ~(1L << (i % 64));
    }
    
    // 检查第i位是否为1
    public boolean get(int i) {
        return (bits[i / 64] & (1L << (i % 64))) != 0;
    }
}

🏗️ 构造函数:为派对准备场地

public Bitmap(int n) {
    // 每个long有64位,所以需要 (n+63)/64 个long
    this.bits = new long[(n + 63) / 64];
}

想象你要举办一个有n位客人的派对,但每张桌子只能坐64人。你需要多少张桌子?当然是 ⌈n÷64⌉(向上取整)。这就是 (n + 63) / 64 的由来!

这里用了一个小技巧:整数除法会自动向下取整,所以 (n + 63) / 64 等价于 ⌈n÷64⌉。就像买桌子,9人需要2张桌子,不能买1.5张对吧?

n = 150 的情况

需要的long数组大小:(150 + 63) / 64 = 213 / 64 = 3.328125 ≈ 3

创建数组:bits = new long[3], 64 * 3 = 192

bits[0]:表示第0-63位
bits[1]:表示第64-127位  
bits[2]:表示第128-191位

🔨 set() 方法:点亮你的专属小灯泡

public void set(int i) {
    bits[i / 64] |= 1L << (i % 64);
}

这个方法就像一个漆黑的体育馆里,你要点亮 i 座位的灯。问题:这个座位在哪个区域?在哪个位置?

  1. i / 64:确定在哪个long中(哪个区域)
  2. i % 64:确定哪个long中哪个bit位上(哪个位置)
  3. 1L << (i % 64):创建一个只有那个位置是1的掩码(准备一个只照亮那个座位的聚光灯)
  4. |=:使用OR操作将那个位置修改成1(打开闪光灯)
假设i = 73,我们要点亮第73个灯

i / 64 = 73 / 64 = 1(在第1个long里)
i % 64 = 73 % 64 = 9(在这个long的第9位)

1L << 9 = 000...000100000000(二进制,只有第9位是1)

bits[1] |= 000...000100000000

🧹 clear() 方法:熄灭特定的灯

public void clear(int i) {
    bits[i / 64] &= ~(1L << (i % 64));
}

这个方法就像是派对结束后,你要关掉特定的一盏灯。操作流程是:

  1. i / 64:确定在哪个long(哪个区域)
  2. i % 64:确定哪个long中哪个bit位上(区域内的灯号)
  3. 1L << (i % 64):创建一个只有那个位置是1的掩码
  4. ~(1L << (i % 64)):反转掩码,现在除了那个位置都是1(准备一个"除了那盏灯以外都亮着"的状态)
  5. &=:使用AND操作将那一位设置为0,其他位保持不变(只关那一盏灯)
假设i = 73,我们要熄灭第73个灯

i / 64 = 73 / 64 = 1(在第1个long里)
i % 64 = 73 % 64 = 9(在这个long的第9位)

1L << 9 = 000...000100000000(二进制,只有第9位是1)
~(1L << 9) = 111...111011111111(二进制,只有第9位是0)

bits[1] &= 111...111011111111

🔍 get() 方法:查看灯是否亮着

public boolean get(int i) {
    return (bits[i / 64] & (1L << (i % 64))) != 0;
}

这个方法就像是派对组织者在检查:第i号座位有人坐着吗?

  1. i / 64:确定在哪个long(哪个区域)
  2. i % 64:确定在这个long的哪一位(区域内的座位号)
  3. 1L << (i % 64):创建一个只有那个位置是1的掩码(准备一个只照亮那个座位的手电筒)
  4. bits[i / 64] & (1L << (i % 64)):使用AND操作检查那一位是否为1(用手电筒照,看看有没有人)
  5. != 0:如果结果不是0,说明那一位是1(座位上有人)
假设我想要检查第73个灯是否被设置

i / 64 = 73 / 64 = 1(在第1个long里)
i % 64 = 73 % 64 = 9(在这个long的第9位)

1L << 9 = 000...000100000000(二进制,只有第9位是1)

假设 bits[1] = 000...000100010000(第9位和第4位是1)

bits[1] & (1L << 9) = 000...000100010000 & 000...000100000000 = 000...000100000000 ≠ 0

所以返回 true

四、位图的算法原理剖析

位图的核心原理其实也很简单:利用二进制位的0和1来记录信息

  1. 索引映射:每一个需要记录的元素映射到位图中的一个特定的位置
  2. 状态表示:使用0表示不存在/否,1表示存在/是
  3. 位操作:通过位运算可以高效的设置和获取状态

位图的数学原理可以表示为

  • 元素 i 映射到数组索引 i / 64(假设用long数组)
  • 在该数组元素中的位偏移量为 i % 64
  • 设置该位:array[i / 64] |= (1L << (i % 64))
  • 清除该位:array[i / 64] &= ~(1L << (i % 64))
  • 检查该位:(array[i / 64] & (1L << (i % 64))) != 0

五、实际应用案例:网站用户活跃度统计

假设你在追踪一个拥有1000万用户的网站,需要记录每个用户在过去30天是否登录过。

  • 传统方法:使用布尔数组,需要约300MB内存。
  • 位图方法:只需约36MB!
public class UserActivityTracker {
    private Bitmap[] dailyActivity; // 30天的活跃记录
    // 创建3个位图,每个表示一天的用户活跃度
    public UserActivityTracker() {
        dailyActivity = new Bitmap[30];
        for (int i = 0; i < 30; i++) {
        	// 每一天的用户容量都得达到 1千万
            dailyActivity[i] = new Bitmap(1_000_000); 
        }
    }

    // 记录用户在某天登录
    public void recordLogin(int userId, int dayOffset) {
        dailyActivity[dayOffset].set(userId);
    }

    // 检查用户在30天内是否活跃(至少有一天是在线的)
    public boolean isActiveInLast30Days(int userId) {
        for (int i = 0; i < 30; i++) {
            if (dailyActivity[i].get(userId)) {
                return true;
            }
        }
        return false;
    }

    // 计算某天的用户活跃度
    public int countActiveUsersOnDay(int dayOffset) {
        int count = 0;
        for (int i = 0; i < 1_000_000; i++) {
            if (dailyActivity[dayOffset].get(i)) {
                count++;
            }
        }
        return count;
    }
}

五、真实的应用场景:布隆过滤器的基础

这个位图实现是布隆过滤器(Bloom Filter)的基础,布隆过滤器是一种高效判断元素是否在集合中的概率数据结构。

// 使用位图实现的简单布隆过滤器
public class SimpleBloomFilter {
    private Bitmap bitmap;
    private int size;
    
    public SimpleBloomFilter(int size) {
        this.size = size;
        this.bitmap = new Bitmap(size);
    }
    
    public void add(String item) {
        int hash1 = Math.abs(item.hashCode() % size);
        int hash2 = Math.abs((item.hashCode() * 31) % size);
        int hash3 = Math.abs((item.hashCode() * 37) % size);
        
        bitmap.set(hash1);
        bitmap.set(hash2);
        bitmap.set(hash3);
    }
    
    public boolean mightContain(String item) {
        int hash1 = Math.abs(item.hashCode() % size);
        int hash2 = Math.abs((item.hashCode() * 31) % size);
        int hash3 = Math.abs((item.hashCode() * 37) % size);
        
        return bitmap.get(hash1) && bitmap.get(hash2) && bitmap.get(hash3);
    }
}

这就像是一个高效的俱乐部入场系统:

  • 当有人加入俱乐部时,在多个特定位置留下标记
  • 当有人要进入时,检查这些位置是否都有标记
  • 可能会有误判(误认为某人是会员),但绝不会错过真正的会员

想要了解更多关于布隆过滤器的内容,请点击🔍该链接

六、算法题:判断字符是否唯一(easy)

请点击🔍:该题目链接

在这里插入图片描述
代码示例:

class Solution {
    public boolean isUnique(String astr) {
        // 利用鸽巢原理 来做优化
        if (astr.length() > 26) {
            return false;
        }
        int bitMap = 0; // 位图原理:比特位 有 1 表示已经存在 0 表示还未存在
        for (int i = 0; i < astr.length(); i++) {
            // 判断 bit 位是否已经标志上 1 了
            int x = astr.charAt(i) - 'a';
            if (((bitMap >> x) & 1) == 1) {
                // 表示 bitMap >> x == 0 此时 未标志上
                return false;
            }
            // 把当前字符加⼊到位图中
            bitMap |= (1 << x);
        }
        return true;
    }
}

代码解析:

鸽巢原理:派对人数超过座位数,直接拒之门外

首先,小写字母只有26个,如果字符串长度超过26,那肯定有人得站着(重复)。所以代码开头就用了这个优化:

if (astr.length() > 26) {
    return false;
}

这就像保安大哥看了一眼派对名单,发现人数超过26,直接说:“别来了,里面没位置了!”

位图:每个字符对应一个比特位,0是空位,1是有人

接下来的重头戏是位图。我们用整数bitMap的每一位代表一个字母是否出现过。比如:

  • a → 第0位
  • b → 第1位
  • z → 第25位

假设bitMap是二进制数000...101,表示ac已经到场(第0位和第2位是1)。

操作步骤:查座位、占座位

  1. 查座位
    当字符c(x=2)到来时,我们想知道第2位是否已被占用。
    bitMap右移2位,让第2位变成最低位,然后和1做&运算:

    (bitMap >> x) & 1  // 结果为1表示座位被占
    
  2. 占座位
    如果座位是空的,就用bitMap |= (1 << x)把对应位设为1。
    这相当于给字符c发了一张VIP卡,标记这个座位已有人。

举个栗子 🌰

假设字符串是"abc"

  1. 处理a(x=0)

    • bitMap初始为0(二进制全0)。
    • 检查(0 >> 0) & 1 → 0,座位空。
    • 更新bitMap = 1(二进制000...001)。
  2. 处理b(x=1)

    • 检查(1 >> 1) & 1 → 0(1右移1位是0)。
    • 更新bitMap = 1 | (1<<1) = 3(二进制000...011)。
  3. 处理c(x=2)

    • 检查(3 >> 2) & 1 → 0(3是11,右移2位是0)。
    • 更新bitMap = 3 | (1<<2) = 7(二进制000...0111)。

派对圆满结束,所有字符都有独立座位,返回true

冲突时刻:当字符试图抢占已占座位

如果字符串是"abca",处理最后一个a时:

  • x=0,此时bitMap是7(二进制000...0111)。
  • (7 >> 0) & 1 → 1,座位已被占!
  • 保安立刻将其赶走,返回false
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值