位图是一种超级节省空间的数据结构,他利用二进制位(0/1)来表示某个元素是否存在或某种状态是否为真。想象一下,用一个小小的比特位就能记录一个信息,这简直是数据存储界的"极简主义"!
想象你是一个小学班主任,班上有40个学生。周一早上,你需要记录谁到校了,谁没到。
boolean[] attended = new boolean[40]
,这要占用40个布尔值的内存。long
类型变量,每一个对应一个学生的出勤率。这就像是一个微型的"打卡机",每个学生占用一个小格子,来了就打个勾(1),没来就空着(0)。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 座位的灯。问题:这个座位在哪个区域?在哪个位置?
i / 64
:确定在哪个long
中(哪个区域)i % 64
:确定哪个long
中哪个bit
位上(哪个位置)1L << (i % 64)
:创建一个只有那个位置是1的掩码(准备一个只照亮那个座位的聚光灯)|=
:使用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));
}
这个方法就像是派对结束后,你要关掉特定的一盏灯。操作流程是:
i / 64
:确定在哪个long
(哪个区域)i % 64
:确定哪个long
中哪个bit
位上(区域内的灯号)1L << (i % 64)
:创建一个只有那个位置是1的掩码~(1L << (i % 64))
:反转掩码,现在除了那个位置都是1(准备一个"除了那盏灯以外都亮着"的状态)&=
:使用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号座位有人坐着吗?
i / 64
:确定在哪个long
(哪个区域)i % 64
:确定在这个long
的哪一位(区域内的座位号)1L << (i % 64)
:创建一个只有那个位置是1的掩码(准备一个只照亮那个座位的手电筒)bits[i / 64] & (1L << (i % 64))
:使用AND操作检查那一位是否为1(用手电筒照,看看有没有人)!= 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来记录信息
位图的数学原理可以表示为
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天是否登录过。
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);
}
}
这就像是一个高效的俱乐部入场系统:
想要了解更多关于布隆过滤器的内容,请点击该链接
请点击:该题目链接
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
,表示a
和c
已经到场(第0位和第2位是1)。
操作步骤:查座位、占座位
查座位:
当字符c
(x=2)到来时,我们想知道第2位是否已被占用。
把bitMap
右移2位,让第2位变成最低位,然后和1做&
运算:
(bitMap >> x) & 1 // 结果为1表示座位被占
占座位:
如果座位是空的,就用bitMap |= (1 << x)
把对应位设为1。
这相当于给字符c
发了一张VIP卡,标记这个座位已有人。
举个栗子
假设字符串是"abc"
:
处理a(x=0):
bitMap
初始为0
(二进制全0)。(0 >> 0) & 1 → 0
,座位空。bitMap = 1
(二进制000...001
)。处理b(x=1):
(1 >> 1) & 1 → 0
(1右移1位是0)。bitMap = 1 | (1<<1) = 3
(二进制000...011
)。处理c(x=2):
(3 >> 2) & 1 → 0
(3是11
,右移2位是0)。bitMap = 3 | (1<<2) = 7
(二进制000...0111
)。派对圆满结束,所有字符都有独立座位,返回true
!
冲突时刻:当字符试图抢占已占座位
如果字符串是"abca"
,处理最后一个a
时:
bitMap
是7(二进制000...0111
)。(7 >> 0) & 1 → 1
,座位已被占!false
。