位图
初识位图
位图, 实际上就是将二进制位作为哈希表的一个个哈希桶的数据结构, 由于二进制位只能表示 0 和 1, 因此通常用于表示数据是否存在.
如下图所示, 这个位图就用于标识 0 ~ 14 中有什么数字存在
可以看到, 我们这里相当于是把下标作为了 key-value 的一员. 但是这样同样也使得位图这个数据结构非常有局限性, 因为下标只能是整数. 因此通常来说, 位图都是用于存储整数是否存在的.
那么位图这么有局限性, 我们为何要使用它呢?
实际上, 我们可以看到, 存储一个数字是否存在, 我们只需要消耗 1 个比特位, 而如果我们使用正常的一个哈希表的键值对来存储的话, 首先是一个整型就是 4 个字节, 其次就是一个 boolean 类型, 我们暂定为 1 字节大小. 而这样相当于就占了 5 个字节, 总共为 40 个比特位.
那么结论也很明显了, 位图存储一个数据是否存在, 只需要消耗 1 比特空间, 相较于直接使用哈希表键值对存储, 大大提升了空间的使用效率.
实现思路
首先要实现位图, 我们肯定需要一个能很好操作比特位的数据类型, 那么在这一点上, 只要是整型都可以轻易的做到, 因此 byte, short, int, long 都是可行的
但是如果只使用单个数, 那肯定是不够的, 以 int 为例, 单个 int 就只有 32 位, 那假如说要存 100 个数字呢? 因此这里最好我们使用一个数组来操作. 我们这里就采用 byte 数组来实现(因为画图好画)
那么由于 byte 是 8 位, 因此我们就可以知道 byte[0] (这里没有数组名因此这样简称) 对应着的是前 8 位, 代表着 0 ~ 7 这八个数字. 而 byte[1] 则往后对应, 对应着 8 ~ 15, 后续同理… 后续我们通过使用 num / 8
就可以轻松的获取到对应下标
但是接下来另一个问题来了, 我们如何操作到位图中的每一个位呢? 如果是最简单的思路, 其实就是靠数字 1 来操作, 因为数字 1 的二进制位为 0000 0001
其中这个单独的 1 位置可以非常便于我们操作, 并且相较于其他的数字可读性非常的高.
那么此时我们会发现一个问题, 假如我想要令 byte[0] 中代表 0 的一位为 1, 那么此时我们还需要通过左移 1 来实现, 如下图所示
这样有没有问题呢? 当然没有, 不过依旧是比较的反直觉, 明明我们修改的数应该是 0 但是却反而需要通过移位来做, 因此我们这里就采用在每一个数字里面里面, 下标都是逆序的设计方式
可以看到此时在 byte[0] 里面, 下标就是从 7 依次减到 0, 而在 byte[1] 里面, 下标从 15 减到 8. 随后如果我们要修改 0 对应的数字, 需要左移的位就是 0 % 8
, 其他的同理
当然, 这部分的设计是完全偏向于个人喜好的, 如果想要实现刚开始的顺序记录, 也不是不可以. 不过此处后续将会采用内部下标逆序的设计方式来书写代码, 感兴趣的可以自行实现顺序的版本.
位的基本操作
由于要操作位图, 相关的位操作我们必须要提前了解下, 这样才会便于我们后续代码的书写. 当然这里介绍的是具体的位相关操作, 如果对二进制位没有基本了解以及位运算也没有基本了解的, 需要自行先去了解一下相关内容
前提要求, 下面的所有第 x 位指的是从右往左, 从 0 开始来计数的
例如 0000 0001
其中 1 在第 0 位
- 给定一个数 n, 确定它的二进制中第 x 位是 0 还是 1
这个其实非常简单, 直接用一个 1 按位与即可, 要么把 1 左移 x 位后按位与, 要么把 n 右移 x 位后按位与
(1 << x) & n == 1 或 (n >> x) & n == 1
这个操作主要用于位图的查找功能
- 将一个数 n 的二进制中的第 x 位修改为 1
要把第 x 位修改为 1, 使用一个 1 左移 x 位后, 采用按位或操作即可
n |= (1 << x)
这个操作主要用于位图的记录功能
- 将一个数 n 的二进制中的第 x 位修改为 0
这个要修改为 0, 其实还是要用 1 左移 x 位, 不过要再取一次反, 然后采用按位与的操作
n &= (~(1 << x))
这个操作主要用于位图的删除功能
位图实现
其实目前所有的基础知识都介绍完了, 尤其是上面的三个基础操作, 接下来实现代码应该是非常简单的
初始化
public class MyBitSet {
private byte[] bits;
private static final int DEFAULT_SIZE = 8;
public MyBitSet() {
this(DEFAULT_SIZE);
}
public MyBitSet(int size) {
bits = new byte[size / 8 + 1];
}
}
存储元素
// 将某个数存入位图
public void set(int index) {
int byteIndex = index / 8;
// 扩容
if(byteIndex >= bits.length){
bits = Arrays.copyOf(bits, byteIndex + 1);
}
int bitIndex = index % 8;
bits[byteIndex] |= (byte) (1 << bitIndex);
}
查找元素
// 查看某个数在位图中是否存在
public boolean get(int index) {
int byteIndex = index / 8;
int bitIndex = index % 8;
return (bits[byteIndex] & (1 << bitIndex)) != 0;
}
删除元素
// 将某个数从位图中清除
public void clear(int index) {
int byteIndex = index / 8;
int bitIndex = index % 8;
bits[byteIndex] &= (byte) ~(1 << bitIndex);