Loading

位图的设计与实现

位图的设计与实现

作者: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类型数(二进制表示,初始化全为0)中,把第 4 号位置设置为 1:

image

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

img

\(contains(4)\)这个操作,就是判断 4 号位置是 0 还是 1,如果是 1, 就说明 4 存在,如果是 0 ,说明 4 不存在。

\(remove(7)\)这个操作,就是把 7 号位置置为 0。如下效果

img

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

举例说明:

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

img

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

实现

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

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

public static class BitMap {

    // 使用每个bit位置的信息。
    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
        // 判断一个元素是否在位图中
    }
}

注:这里只需要考虑非负数,对于负数的情况,也可以转换成正数来处理,比如:-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));
}

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

num % 64其实就是num & 63

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

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;
}

位图的完整代码见

// 位图
class BitMap {

    private final 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 即:0b111111
        // 只适用于 2 的 n 次方
        // 注意:这里是1L非1,如果是1,因为要管64位
        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;
    }
}

测试

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

注:需要引入junit

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
package git.snippet.bit;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("位图测试")
public class BitMapTest {
    @Test
    void testBitMap() {
        // 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 {
                assertEquals(bitMap.contains(num), set.contains(num));
            }
        }
        for (int num = 0; num <= max; num++) {
            assertEquals(bitMap.contains(num), set.contains(num));
        }
    }
}

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

本文所有图例见:ProcessOn:位图的使用和实现

更多

算法和数据结构学习笔记

算法和数据结构学习代码

参考资料

算法和数据结构体系班-左程云

posted @ 2022-08-28 22:25  Grey Zeng  阅读(513)  评论(0编辑  收藏  举报