23. 数据结构之位图

前言

之前在讲散列表的时候,提到过位图的概念。位图(Bitmap)作为一种特殊的数据结构,它使用一系列位来表示数据,每个位只有两个状态(0或1)。由于它的高效性和节省空间的特性,位图在很多场景中都有广泛的应用。本节,我们就位图展开详细介绍。

1. 简介

位图使用位来表示数据,这使得它在存储和处理大量数据时具有高效性和节省空间的优点。例如,如果我们需要存储一亿个整数,使用普通的数组需要消耗大约381MB的内存(假设一个整数占用4字节,100000000*4/1024/1024=381MB),而使用位图只需要消耗大约11.92MB(100000000/8/1024/1024)的内存。

如下图所示,可以看到一个整型占用四个字节,一个字节八位,那么一个整型可以表示32个数字。那么理论上来说,如果用位图的话,我们的存储空间,相对于直接整型存储,可以缩小32倍,也就是上面的 381MB / 32 约等于 11.92 MB。
在这里插入图片描述

2. 代码实现

2.1 java工具包自带

import java.util.BitSet;
public class Client {
	@Test
	public void testBitMap(){
	    BitSet bitSet=new BitSet(100000000);
	    for (int i = 0; i < 1000000; i++) {
	        bitSet.set(i);
	    }
	    System.out.println(bitSet.get(200));
	    System.out.println(bitSet.get(343535353));
	}
}

代码运行结果:

true
false

2.2 手写实现位图

2.2.1 基础知识

  1. 移位
    移位运算是指将一个数字转换为2进制后向前向后进位的运算,具体计算举例如下:
    1<<2 : 00000001 转换后为 00000100
    8>>3 : 00001000 转换后为 00000001

  2. 与运算规则如下:1 & 0= 0 ,1&1=1 ,0&0=0
    所以可得出结论:通过和1 按位与运算,可以得出对应的比特位是0还是1

  3. 或运算规则如下:0|1=1,1|1=1, 0|0=0
    所以可得出结论:通过与1 按位或运算,最后得到的结果对应比特位一定是1

2.2.2 代码实现

package org.wanlong.hash;

import java.util.Arrays;

/**
 * @author wanlong
 * @version 1.0
 * @description:
 * @date 2023/6/25 14:37
 */
public class MyBitMap {

    //元素存储
    private byte[] elem;
    //已有元素个数
    private int usedSize;

    public MyBitMap() {
        this.elem = new byte[1];
    }

    /**
     * @param n:
     * @return
     * @Description: 初始化 n代表要使用多少位,也即存储的最大范围值
     * @Author: wanlong
     * @Date: 2023/6/25 15:07
     **/
    public MyBitMap(int n) {
        this.elem = new byte[n / 8 + 1];
    }

    /**
     * @param val:
     * @return void
     * @Description: 设置值
     * @Author: wanlong
     * @Date: 2023/6/25 15:05
     **/
    public void set(int val) {
        if (val < 0) {
            throw new IndexOutOfBoundsException();
        }
        //整数除8 得到元素应该放到哪个下标
        int arrayIndex = val / 8;
        //整数除8求余,得到元素在这个下标的整型的哪个比特位
        int bitIndex = val % 8;

        //扩容
        if (arrayIndex > elem.length - 1) {
            elem = Arrays.copyOf(elem, arrayIndex + 1);
        }
        // 或运算 可以保证如果之前插入过这个值,不会影响这个值还是1,也不会更改别的值
        // 1 向左移位 bitindex 后,则对应的bitindex为1 
        // 如 00000001 向左移动 3 则 为 00001000
        //或运算 0|1=1 ,1|1=1 ,则,按位或运算后,固定的位置bitIndex一定是1 ,其他位置不变
        elem[arrayIndex] |= (1 << bitIndex);
        //元素个数加一
        usedSize++;
    }

    /**
     * @param val: 待查找值
     * @return boolean
     * @Description 判断值是否存在
     * @Author: wanlong
     * @Date: 2023/6/25 15:04
     **/
    public boolean get(int val) {
        if (val < 0) {
            throw new IndexOutOfBoundsException();
        }
        int arrayIndex = val / 8;
        int bitIndex = val % 8;

        //如果算出来查找元素下标大于数组长度,一定不在数组中,返回false
        if (arrayIndex >= elem.length) {
            return false;
        }
        // 1 向左移位 bitindex 后,则对应的bitindex为1 
        // 如 00000001 向左移动 3 则 为 00001000
        // 与运算  0&1 =0,1&1=1  则,如果与的结果不为0,一定是对应的bitIndex=1 ,即,元素存在
        if ((elem[arrayIndex] & (1 << bitIndex)) != 0) {
            return true;
        }
        return false;
    }

    /**
     * @param val: 待重置值
     * @return void
     * @Description: 将val的对应位置置为0
     * @Author: wanlong
     * @Date: 2023/6/25 15:06
     **/
    public void delete(int val) {
        if (val < 0) {
            throw new IndexOutOfBoundsException();
        }
        int arrayIndex = val / 8;
        int bitIndex = val % 8;
        //对应下标置为0
        // 1 向左移位 bitindex 后,则对应的bitindex为1 
        // 如 1<<3 , 00000001 向左移动 3 则 为 00001000
        // ~(1<<3), 00001000 , 11110111  对应的bitindex设为0
        // 1&0=0, 0&0=0 
        // 所以,最终对应的下标bitIndex一定是0 
        elem[arrayIndex] &= ~(1 << bitIndex);
        //元素个数减一
        usedSize--;
    }


    /**
     * @return int
     * @Description: 当前位图记录的元素个数
     * @Author: wanlong
     * @Date: 2023/6/25 15:06
     **/
    public int getUsedSize() {
        return usedSize;
    }
}

2.2.3 测试验证

@Test
public void testMyBitMap() {
    MyBitMap myBitMap = new MyBitMap(1000000);
    for (int i = 0; i < 1000000; i++) {
        myBitMap.set(i);
    }
    System.out.println(myBitMap.get(200));
    System.out.println(myBitMap.get(343535353));
}

运行结果:

true
false

3. 优缺点

3.1 优点

  1. 可存储的数据变多了
  2. 占用空间小,索引速度快

3.2 缺点

  1. 可读性差
  2. 位图存储的元素个数虽然比一般做法多,但是存储的元素大小受限于存储空间的大小。位图存储性质:存储的元素个数等于元素的最大值。比如, 1K 字节内存,能存储 8K 个值大小上限为 8K 的元素。(元素值上限为 8K ,这个局限性很大!)比如,要存储值为 65535 的数,就必须要 65535/8=8K 字节的内存。要就导致了位图法根本不适合存 unsigned int 类型的数(大约需要 2^32/8=5 亿字节的内存)。
  3. 位图对有符号类型数据的存储,需要 2 位来表示一个有符号元素。这会让位图能存储的元素个数,元素值大小上限减半。 比如 8K 字节内存空间存储 short 类型数据只能存 8K*4=32K 个,元素值大小范围为 -32K~32K

4. 应用

4.1 大数据去重

当我们需要处理大量的数据,并且需要去除重复的数据时,可以使用位图。例如,我们可以使用位图来记录用户的访问记录,以去除重复的访问。

4.2 布隆过滤器

布隆过滤器是一种使用位图实现的概率型数据结构,它可以用于检测一个元素是否在一个集合中。由于布隆过滤器可能会有误判,所以它通常用于需要快速检查但可以接受一定误判率的场景,例如网页爬虫、垃圾邮件过滤等。

4.3 位图索引

在数据库中,位图索引是一种使用位图来加快数据检索速度的技术。它特别适用于处理低基数数据(即数据的唯一值数量相对较少)。

5. 经典案例

  1. 40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中
    首先,将这40亿个数字存储到bitmap中,然后对于给出的数,判断是否在bitmap中即可。
  2. 使用位图法判断整形数组是否存在重复
    遍历数组,一个一个放入bitmap,并且检查其是否在bitmap中出现过,如果没出现,放入,否则即为重复的元素。
  3. 使用位图法进行整形数组排序
    首先遍历数组,得到数组的最大最小值,然后根据这个最大最小值来缩小bitmap的范围。(这一步很关键,通过确定最大最小值,可以将位图的大小缩小,比如数字范围是3000~~10000时,可以先将数据减去3000,再放入bitmap中,次数bitmap大小会变小),这里需要注意对于int的负数,都要转化为unsigned int来处理,而且取位的时候,数字要减去最小值。
  4. 在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数
    采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)。其实,这里可以使用两个普通的Bitmap,即第一个Bitmap存储的是整数是否出现,如果再次出现,则在第二个Bitmap中设置即可。这样的话,就可以使用简单的1- Bitmap了。

以上,本人菜鸟一枚,如有错误,请不吝指正。

你可能感兴趣的:(数据结构和算法,数据结构,位图,位图经典案例,手写位图,java)