作者:Grey
原文地址:
博客园:位图的使用与实现
CSDN:位图的使用与实现
本文内容使用的编程语言是 Java。其他语言有类似的数据结构。
在 Java 中,使用HashSet可以实现如下操作:
add(T v)
加入一个元素到
HashSet中,重复则覆盖。
contains(T v)
判断一个元素是否加入过
HashSet。
remove(T v)
从
HashSet中删除一个元素。
如果数据范围固定,使用位图比使用HashSet省空间。
在 Java 中,一个 int 类型的整数可以表示 32 个 bit,所以,如果数据范围0 ~ 31,可以直接用一个 int 类型的数来完成上述三个操作。
比如:
add(4)这个操作,就是在如下 int 类型数(二进制表示)中,第 4 号位置设置为 1。如下图

继续执行add(7)这个操作,就是在如下 int 类型数(二进制表示)中,第 7 号位置设置为 1。如下图

contains(4)这个操作,就是判断 4 号位置是 0 还是 1,如果是 1, 就说明 4 存在,如果是 0 ,说明 4 不存在。
remove(7)这个操作,就是把 7 号位置置为 0。如下效果

如果数据范围是 0 ~ 1023, 则可以用一个 int 类型数组来表示,这个数组只需要 32 个元素即可。因为 32 个 int 类型元素,可以表示 1024 位,正好可以覆盖数据范围中的所有数字。对于0 ~ 1023中任意一个数 num,num在数组中存在第num / 32个元素中的第num % 32位中。
举例说明:
num = 37,客观上,num 应该在如下位置:

在 1 号(37 / 32)数组元素的第 5 号( 37 % 32)位置。
为了扩大表示范围,我们可以使用 long 类型来替代 int 类型,因为 long 类型可以表达 64 个 bit,思路还是和如上一样。现在说明如何实现上述三个方法。
先把位图的数据结构和相关方法定义好
public static class BitMap { // 使用每个位置的信息。 private long[] bits; public BitMap(int max) { // TODO // 位图初始化 } public void add(int num) { // TODO // 添加一个元素 } public void remove(int num) { // TODO // 删除一个元素 } public boolean contains(int num) { // TODO // 判断一个元素是否在位图中 } }注:我们这里只考虑非负数,对于负数的情况,也可以转换成正数来处理,比如:-3~6,可以转换成0~9。
首先是位图的初始化,即如何根据数据范围确定位图应该开辟多大的数组?
由于是 long 类型,所以,对于 0 ~ x 区间来说,需要准备
(x + 64) / 64
这么大的 long 类型数组。
位图中增加一个元素,比如我们要增加 53 这个元素,先定位它是数组中的哪个元素,即53 / 64 = 0,第 0 号位置的元素,再定位是这个元素中的第几位,即:53 % 64 = 11,即第 11 位,我们可以用 1L << 11后的值| bit[0]即可,代码实现如下
public void add(int num) { bits[num / 64] |= (1L << (num % 64)); }由于 num / 64其实就是 num >> 6,
num % 64其实就是num & 63,
由于位运算比算术运算效率要高,所以add方法可以进一步写成如下形式
public void add(int num) { // bits[num / 64] |= (1L << (num % 64)); // num % 64 ---> num & 63 // 只适用于 2 的 n 次方 bits[num >> 6] |= (1L << (num & 63)); }位图中删除一个元素,其实就是把对应位置的二进制位置为 0,其他位置保持不变,通过
~((1L << (num & 63))) 可以预先得到一个除目标位置是 0,其他位置都是 1 的数。
然后通过这个数去 &上数组目标位置的元素,即可把对应位置的 1 改为 0,其他位置不变。
public void remove(int num) { bits[num >> 6] &= ~(1L << (num & 63)); }位图中是否包含某个元素,其实就是判断对应位置是 0 还是 1, 如果是 0 ,就说明存在,不是 0 , 则不存在。
public boolean contains(int num) { return (bits[num >> 6] & (1L << (num & 63))) != 0; }位图的完整代码见
public static class BitMap { private long[] bits; public BitMap(int max) { // 准备多少个整数? 0 ~ 63 需要1个整数 // >> 6 就是 除以 64 bits = new long[(max + 64) >> 6]; } public void add(int num) { // bits[num / 64] |= (1L << (num % 64)); // num % 64 ---> num & 63 // 只适用于 2 的 n 次方 bits[num >> 6] |= (1L << (num & 63)); } public void remove(int num) { bits[num >> 6] &= ~(1L << (num & 63)); } public boolean contains(int num) { return (bits[num >> 6] & (1L << (num & 63))) != 0; } }通过实现的位图和 Java 自带的 HashSet 进行对比测试,可以判断我们写的位图是否正确,测试代码如下
package snippet;import java.util.HashSet;import java.util.Set;public class Code_0009_BitMap { public static void main(String[] args) { System.out.println("测试开始!"); int max = 70000; BitMap bitMap = new BitMap(max); Set<Integer> set = new HashSet<>(); int testTime = 90000000; for (int i = 0; i < testTime; i++) { int num = (int) (Math.random() * (max + 1)); double decide = Math.random(); if (decide < 0.333) { bitMap.add(num); set.add(num); } else if (decide < 0.666) { bitMap.remove(num); set.remove(num); } else { if (bitMap.contains(num) != set.contains(num)) { System.out.println("Oops!"); break; } } } for (int num = 0; num <= max; num++) { if (bitMap.contains(num) != set.contains(num)) { System.out.println("Oops!"); } } System.out.println("测试结束!"); }}运行,未打印报错信息,说明我们的算法正确。
算法和数据结构笔记
算法和数据结构新手班-左程云
本文来自博客园,作者:Grey Zeng,转载请注明原文链接:https://www.cnblogs.com/greyzeng/p/16634282.html