• 位图的使用与实现


    位图的使用与实现

    作者:Grey

    原文地址:

    博客园:位图的使用与实现

    CSDN:位图的使用与实现

    说明

    本文内容使用的编程语言是 Java。其他语言有类似的数据结构。

    原理

    在 Java 中,使用HashSet可以实现如下操作:

    add(T v)

    加入一个元素到HashSet中,重复则覆盖。

    contains(T v)

    判断一个元素是否加入过HashSet

    remove(T v)

    HashSet中删除一个元素。

    如果数据范围固定,使用位图比使用HashSet省空间。

    在 Java 中,一个 int 类型的整数可以表示 32 个 bit,所以,如果数据范围是 [ 0 , 31 ] [0,31] [0,31],可以直接用一个 int 类型的数来完成上述三个操作。

    例如:

    a d d ( 4 ) add(4) add(4)这个操作,就是在如下一个 int 类型数(二进制表示,初始化全为0)中,把第 4 号位置设置为 1:

    image

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

    image

    c o n t a i n s ( 4 ) contains(4) contains(4)这个操作,就是判断 4 号位置是 0 还是 1,如果是 1, 就说明 4 存在,如果是 0 ,说明 4 不存在。

    r e m o v e ( 7 ) remove(7) remove(7)这个操作,就是把 7 号位置置为 0。如下效果

    image

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

    举例说明:

    num = 37,客观上,num 应该在如下位置:

    image

    在 1 号(即:37 / 32)数组元素的第 5 个 bit(即:37 % 32)位置上。

    实现

    为了扩大表示范围,我们可以使用 long 类型来替代 int 类型,因为 long 类型可以表达 64 个 bit,思路还是和上述过程一样。现在说明如何实现上述三个方法,

    先把位图的数据结构和相关方法定义好

    public static class BitMap {
    
            // 使用每个位置的信息。
            private final 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
                // 判断一个元素是否在位图中
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    注:这里只需要考虑非负数,对于负数的情况,也可以转换成正数来处理,比如:-3~6,可以转换成0~9

    首先是位图的初始化,即:如何根据数据范围确定位图应该开辟多大的数组?

    由于是 long 类型,所以,对于 0 ~ x 区间来说,需要准备

    (x + 64) / 64

    这么大的 long 类型数组。

    位图中增加一个元素,比如我们要增加 53 这个元素,先定位它是数组中的哪个元素,即53 / 64 = 0,第 0 号位置的元素,再定位是这个元素中的第几个bit位,即:53 % 64 = 11,即第 11 个 bit 位,我们可以用 1L << 11 后的值与(|)bit[0]即可,代码实现如下

            public void add(int num) {
                bits[num / 64] |= (1L << (num % 64));
            }
    
    • 1
    • 2
    • 3

    由于 num / 64其实就是 num >> 6

    num % 64其实就是num & 63

    由于位运算比算术运算效率要高,所以 a d d add add方法可以进一步写成如下形式

            public void add(int num) {
                //  bits[num / 64] |= (1L << (num % 64));
                // num % 64 ---> num & 63
                // 只适用于 2 的 n 次方
                bits[num >> 6] |= (1L << (num & 63));
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    位图中删除一个元素,其实就是把对应位置的二进制位置为 0,其他位置保持不变,通过

    ~((1L << (num & 63))) 
    
    • 1

    可以预先得到一个除目标位置是 0,其他位置都是 1 的数。

    然后通过这个数去与(&)数组目标位置的元素,即可把对应位置的 1 改为 0,其他位置不变。

            public void remove(int num) {
                bits[num >> 6] &= ~(1L << (num & 63));
            }
    
    • 1
    • 2
    • 3

    位图中是否包含某个元素,其实就是判断对应位置是0还是1, 如果是0 ,就说明存在,不是0 , 则不存在。

            public boolean contains(int num) {
                return (bits[num >> 6] & (1L << (num & 63))) != 0;
            }
    
    • 1
    • 2
    • 3

    位图的完整代码见

        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;
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    测试

    通过实现的位图和 Java 自带的 HashSet 进行对比测试,可以判断我们写的位图是否正确,测试代码如下

    import java.util.HashSet;
    import java.util.Set;
    
    public class Code_BitMap {
    
        public static class BitMap {
    
            private final long[] bits;
    
            public BitMap(int max) {
                bits = new long[(max + 64) >> 6];
            }
    
            public void add(int num) {
                bits[num >> 6] |= (1L << (num & 0b111111));
            }
    
            public void remove(int num) {
                bits[num >> 6] &= ~(1L << (num & 0b111111));
            }
    
            public boolean contains(int num) {
                return (bits[num >> 6] & (1L << (num & 0b111111))) != 0;
            }
        }
    
        public static void main(String[] args) {
            System.out.println("test begin");
            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("test end");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    运行,未打印报错信息,说明我们的算法正确。

    更多

    算法和数据结构学习笔记

    参考资料

    算法和数据结构新手班-左程云

  • 相关阅读:
    奇偶+逆序对构造法:arc102d
    每日三题 6.29
    关于类和对象超级初级小白知识
    红蓝对抗-攻防演练中红队如何识别蜜罐保护自己
    Vue2响应式原理分析(数据代理与数据劫持)
    1、什么是NFT
    【DS】二叉搜索树的介绍和实现
    docker desktop如何一键进入容器内部
    RecursionError: maximum recursion depth exceeded while calling a Python object
    C语言经典算法实例6:斐波那契数列
  • 原文地址:https://blog.csdn.net/hotonyhui/article/details/126575543