热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

BitMap使用实例代码分析

这篇文章主要介绍“BitMap使用实例代码分析”,在日常操作中,相信很多人在BitMap使用实例代码分析问题上存在疑惑,小编查阅了各式资料,整理出简

这篇文章主要介绍“BitMap使用实例代码分析”,在日常操作中,相信很多人在BitMap使用实例代码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”BitMap使用实例代码分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

    What

    BitMap,即位图,是比较常见的数据结构,简单来说就是按位存储,主要为了解决在去重场景里面大数据量存储的问题。本质其实就是哈希表的一种应用实现,使用每个位来表示某个数字。

    举个例子

    假设有个1,3,5,7的数字集合,如果常规的存储方法,要用4个Int的空间。其中一个Int就是32位的空间。3个就是4*32Bit,相当于16个字节。

    如果用Bitmap存储呢,只用8Bit(1个字节)就够了。bitmap通过使用每一bit位代表一个数,位号就是数值,1标识有,0标识无。如下所示:

    BitMap使用实例代码分析

    BitMap的简单实现

    对于 BitMap 这种经典的数据结构,在 Java 语言里面,其实已经有对应实现的数据结构类 java.util.BitSet 了,而 BitSet 的底层原理,其实就是用 long 类型的数组来存储元素,因为使用的是long类型的数组,而 1 lOng= 64 bit,所以数据大小会是64的整数倍。这样看可能很难理解,下面参考bitmap源码写了一个例子,并写上了详细的备注,方便理解

    import java.util.Arrays;
    public class BitMap {
        // 用 byte 数组存储数据
        private byte[] bits;
        // 指定 bitMap的长度
        private int bitSize;
        // bitmap构造器
        public BitMap(int bitSize) {
            this.bitSize = (bitSize >> 3) + 1;
            //1byte 能存储8个数据,那么要存储 bitSize的长度需要多少个bit呢,bitSize/8+1,右移3位相当于除以8
            bits = new byte[(bitSize >> 3) + 1];
        }
        // 在bitmap中插入数字
        public void add(int num) {
            // num/8得到byte[]的index
            int arrayIndex = num >> 3;
            // num%8得到在byte[index]的位置
            int position = num & 0x07;
            //将1左移position后,那个位置自然就是1,然后和以前的数据做|,这样,那个位置就替换成1了。
            bits[arrayIndex] |= 1 << position;
        }
        // 判断bitmap中是否包含某数字
        public boolean contain(int num) {
            // num/8得到byte[]的index
            int arrayIndex = num >> 3;
            // num%8得到在byte[index]的位置
            int position = num & 0x07;
            //将1左移position后,那个位置自然就是1,然后和以前的数据做&,判断是否为0即可
            return (bits[arrayIndex] & (1 << position)) != 0;
        }
        // 清除bitmap中的某个数字
        public void clear(int num) {
            // num/8得到byte[]的index
            int arrayIndex = num >> 3;
            // num%8得到在byte[index]的位置
            int position = num & 0x07;
            //将1左移position后,那个位置自然就是1,然后对取反,再与当前值做&,即可清除当前的位置了.
            bits[arrayIndex] &= ~(1 << position);
        }
        // 打印底层bit存储
        public static void printBit(BitMap bitMap) {
            int index=bitMap.bitSize & 0x07;
            for (int j = 0; j < index; j++) {
                System.out.print("byte["+j+"] 的底层存储:");
                byte num = bitMap.bits[j];
                for (int i = 7; i >= 0; i--) {
                    System.out.print((num & (1 << i)) == 0 ? "0" : "1");
                }
                System.out.println();
            }
        }
        // 输出数组元素,也可以使用Arrays的toString方法
        private static void printArray(int[] arr) {
            System.out.print("数组元素:");
            for (int i = 0; i < arr.length; i++) {
                System.out.print(arr[i]+" ");
            }
            System.out.println();
        }
    }

    下面就来简单玩一玩这个自制的BitMap,先尝试插入一个3,并清理掉它,看看底层二进制结构是怎样变化的

    public static void main(String[] args) {
        // 简单试验
        BitMap bitmap = new BitMap(3);
        bitmap.add(3);
        System.out.println("插入3成功");
        boolean isexsit = bitmap.contain(3);
        System.out.println("3是否存在:" + isexsit);
        printBit(bitmap);
        bitmap.clear(3);
        isexsit = bitmap.contain(3);
        System.out.println("3是否存在:" + isexsit);
        printBit(bitmap);
    }

    输出结果如下:

    BitMap使用实例代码分析

    再通过数组,插入多个元素看看效果

    public static void main(String[] args) {
        // 数组试验
        int[] arr = {8,3,3,4,9};
        printArray(arr);
        int size = Arrays.stream(arr).max().getAsInt();
        BitMap b = new BitMap(size);
        for (int i = 0; i < arr.length; i++) {
            b.add(arr[i]);
        }
        printBit(b);
    }

    输出结果如下:

    BitMap使用实例代码分析

    BitSet源码理解

    前面简单了解了一下BitMap,下面就通过源码来看看java是如何实现BitSet的。

    备注信息

    打开源码,首先映入眼帘的是下面这一段长长的备注,简单翻译一下,便于英语不好的小伙伴理解

    BitMap使用实例代码分析

    源码备注翻译如下

    • 这个类实现了一个根据需要增长的位向量。位集的每个组件都有一个布尔值。BitSet的位由非负整数索引。可以检查、设置或清除单个索引位。一个位集可用于通过逻辑AND、逻辑inclusive OR和逻辑exclusive OR操作修改另一个位集的内容。

    • 默认情况下,集合中的所有位最初的值都为false。

    • 每个BitSet都有一个当前大小,即BitSet当前使用的空间位数。请注意,大小与BitSet的实现有关,因此它可能会随着实现而改变。BitSet的长度与BitSet的逻辑长度有关,并且与实现无关。

    • 除非另有说明,否则将null参数传递给位集中的任何方法都将导致NullPointerException。

    • 如果没有外部同步,BitSet对于多线程使用是不安全的。

    核心片段理解

    首先可以看到源码中,最核心的属性信息。在BitSet 中使用的是long[] 作为底层存储的数据结构,并通过一个 int 类型的变量,来记录当前已经使用数组元素的个数。

    这种类型的属性结构很常见,比如StringBuilder、StringBuffer底层是一个char[] 作为存储,一个int 变量用来计数,相似的还有ArrayList,Vector等

    /**
     * The internal field corresponding to the serialField "bits".
     */
     private long[] words; 
    /**
     * The number of words in the logical size of this BitSet.
     */
     private transient int wordsInUse = 0;

    再往下看,是一个很重要的方法,是用来获取某个数在数组中的下标,采用的算法是将这个数右移6位,这是因为 bitIndex >> 6 = bitIndex / (2^6) = bitIndex /64,而long就是64个字节

     private final static int ADDRESS_BITS_PER_WORD = 6;
     /**
     * Given a bit index, return word index containing it.
     */
    private static int wordIndex(int bitIndex) {
        return bitIndex >> ADDRESS_BITS_PER_WORD;
    }

    接着比较有意思的就是它的空参构造器,BITS_PER_WORD默认是1<<6 也就是64,根据上面方法原理,wordIndex(64-1)+1 = 1 ,所以最终初始化的是长度为1的数组

    private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
    /**
     * Creates a new bit set. All bits are initially {  @code false}.
     */
    public BitSet() {
        initWords(BITS_PER_WORD);
        sizeIsSticky = false;
    }
    private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }

    最后看到这个很经典也很重要的方法,由于底层是数组,在初始化的时候,并不知道将来会需要存储多大的数据,所以对于这一类底层核心实现结构是数组的实体类,通常会使用动态扩容的方法,具体实现细节也都大同小异,这里实现的动态扩容是原本的两倍,和Vector类似。

    /**
     * Ensures that the BitSet can hold enough words.
     *  @param wordsRequired the minimum acceptable number of words.
     */
    private void ensureCapacity(int wordsRequired) {
        // 如果数组的长度小于所需要的就要进行扩容
        if (words.length < wordsRequired) {
            // Allocate larger of doubled size or required size
            // 扩容最终的大小,最小为原来的两倍
            int request = Math.max(2 * words.length, wordsRequired);
            // 创建新的数组,容量为request,然后将原本的数组拷贝到新的数组中
            words = Arrays.copyOf(words, request);
            // 并设置数组大小不固定
            sizeIsSticky = false;
        }
    }

    至于其他的源码细节,因为篇幅有限,就暂且不表,感兴趣的可以自行阅读~

    Why

    BitMap的特点

    根据bitmap的实现原理,其实可以总结出使用bitmap的几个主要原因:

    • 针对海量数据的存储,可以极大的节约存储成本!当需要存储一些很大,且无序,不重复的整数集合,那使用Bitmap的存储成本是非常低的。

    • 因为其天然去重的属性,对于需要去重存储的数据很友好!因为bitmap每个值都只对应唯一的一个位置,不能存储两个值,所以Bitmap结构天然适合做去重操作。

    • 同样因为其下标的存在,可以快速定位数据!比如想判断数字 99999是否存在于该bitmap中,若是使用传统的集合型存储,那就要逐个遍历每个元素进行判断,时间复杂度为O(N)。而由于采用Bitmap存储,只要查看对应的下标数的值是0还是1即可,时间复杂度为O(1)。所以使用bitmap可以非常方便快速的查询某个数据是否在bitmap中。

    • 还有因为其类集合的特性,对于一些集合的交并集等操作也可以支持!比如想查询[1,2,3]与[3,4,5] 两个集合的交集,用传统方式取交集就要两层循环遍历。而Bitmap实现的底层原理,就是把01110000和00011100进行与操作就行了。而计算机做与、或、非、异或等等操作是非常快的。

    虽然bitmap有诸多好处,但是正所谓人无完人,它也存在很多缺陷。

    • 只能存储正整数而不能是其他的类型;

    • 不适合存储稀疏的集合,简单理解,一个集合存放了两个数[1,99999999],那用bitmap存储的话就很不划算,这也与它本来节约存储的优点也背离了;

    • 不适用于存储重复的数据。

    BitMap的优化

    既然bitmap的优点如此突出,那应该如何去优化它存在的一些局限呢?

    • 针对存储非正整数的类型,如字符串类型的,可以考虑将字符串类型的数据利用类似hash的方法,映射成整数的形式来使用bitmap,但是这个方法会有hash冲突的问题,解决这个可以优化hash方法,采用多重hash来解决,但是根据经验,这个效果都不太好,通常的做法就是针对字符串建立映射表的方式。

    • 针对bitmap的优化最核心的还是对于其存储成本的优化,毕竟大数据领域里面,大多数时候数据都是稀疏数据,而我们又经常需要使用到bitmap的特长,比如去重等属性,所以存在一些进一步的优化,比较知名的有WAH、EWAH、RoaringBitmap等,其中性能最好并且应用最为广泛的当属RoaringBitmap

    RoaringBitmap的核心原理

    为了快速把这个原理说清楚,这里就不继续撸源码了,有兴趣的小伙伴可以自行搜索相关源码阅读,下面简单阐述一下它的核心原理:1个Int 类型相当于有32 bit 也就相当于2^32=2^16 x 2^16,这意味着任意一个Int 类型可以拆分成两个16bit的来存储,每一个拆出来的都不会大于2^16, 2^16就是65536,而Int的正整数实际最大值为 2147483647。而RoaringBitmap的压缩首先做的就是用原本的数去除65536,结果表示成(商,余数),其中商和余数是都不会超过65536。

    如下图所示

    BitMap使用实例代码分析

    RoaringBitmap的做法就是将131138 原本32bit的存储结构,拆分成连两个16bit的结构,而拆分出的两个16bit分别存储了131138除65536的商2以及余数66。

    在RoaringBitmap中,把商所处的16bit 被称为高16位,除数所处的16bit 被称为低16位。并用key和Container去存储的这些拆分出来的数据,其中key是short[] ,存放的就是商,因为bitmap的特性,商和余数不会存在完全相同的情况。

    通过商来作为key划分不同的Container,就类似划分不同的桶,key就是标识数据应该存在哪个桶,container用来存对应数据的低16位的数字。比如 1000和60666 除以65536后的结果分别是(0,1000)和(0,60666),所以这两个数据存储到RoaringBitmap中,就会都放到key位0那个container中,container中就是1000和60666。

    由于container中存放的数字是0~65536的一些数据,可能稀疏可能稠密,所以RoaringBitmap依据不同的场景,提供了 3 种不同的 Container,分别是 ArrayContainer 、 BitmapContainer 、RunContainer。

    关于三个Container的存储原理如下:

    • ArrayContainer 存储的方式就是 shot类型的数组, 每个数字占16bit 也就是2Byte,当id 数达到4096个时,占用4096x2 = 8196byte 也就是8kb,而id数最大是65536,占用 65536x2 =131072 byte 也就是128kb。

    • BitmapContainer存储的方式就是bitmap类型,bitmap的位数为 65536,能存储0~65535个数字,占用 65536/8/1024=8kb,也就是bitmap container占用空间恒定为8kb。

    • RunContainer存储的必须是连续的数字,比如存储1,2,3...100w,RunContainer就只会存储[1,100w]也就是开头和结尾的一个数字,其压缩效率取决于连续的数字有多长。

    关于ArrayContainer和BitmapContainer的选择:

    BitMap使用实例代码分析

    如图所示,可以看到ArrayContainer 更适合存放稀疏的数据,BitmapContainer 适合存放稠密的数据。在RoaringBitmap中,若一个 Container 里面的元素数量小于 4096,会使用 ArrayContainer 来存储。当 Array Container 超过最大容量 4096 时,会转换为 BitmapContainer,这样能够最大化的优化存储。

    how

    bitmap就像一柄双刃剑,用的好可以帮助我们破除瓶颈,解决痛点。用的不好不仅会丢失它所有的优点,还要搭上过多的存储,甚至会丧失掉最重要的准确性,所以要针对不同业务场景灵活使用我们的武器,才能事半功倍!

    下面举例bitmap的一些使用场景,来看看实际开发中,到底怎么正确使用bitmap:

    BitMap在用户分群的应用

    假设有用户的标签宽表,对应字段及值如下

    user_id(用户id)city_id(城市id)is_user_start(是否启动)is_evl(是否估价)is_order(是否下单)
    11001111
    21001110
    31002111
    41002100
    51003000

    如果想根据标签划分人群,比如:1001城市 + 下单。

    传统解决方案

    通常是对列值进行遍历筛选,如果优化也就是列上建立索引,但是当这张表有很多标签列时,如果要索引生效并不是每列有索引就行,要每种查询组合建一个索引才能生效,索引数量相当于标签列排列组合的个数,当标签列有1、2 k 的时候,这基本就是不可能的。通常的做法是在数仓提前将热点的组合聚合过滤成新字段,或者只对热点组合加索引,但这样都很不灵活。

    使用BitMap的方案

    通过采用bitmap的办法,按字段重组成Bitmap。

    标签标签值bitmap字段底层二进制结构
    city_id(城市id)100100000110
    city_id(城市id)100200011000
    city_id(城市id)100300100000
    is_user_start(是否启动)100011110
    is_user_start(是否启动)000100000
    is_evl(是否估价)100001110
    is_evl(是否估价)000110000
    is_order(是否下单)100001010
    is_order(是否下单)000110100

    把数据调整成这样的结构,再进行条件组合,那就简单了。比如: 1001城市 + 下单 = Bitmap[00000110] & Bitmap[00001010]= 1 ,这个计算速度相比宽表条件筛选是快很多的,如果数据是比较稠密的,bitmap可以极大的节省底层存储,如果数据比较稀疏,可以采用RoaringBitmap来优化。

    BitMap在A/B实验平台业务的应用

    在支持货拉拉A/B实验平台业务的场景中,会有一个实验对应很多司机的情况,因为要在数仓处理成明细宽表,来支持OLAP引擎的使用,随着维度的增多,数据发散的情况变得很严重,数仓及OLAP的存储与计算资源都消耗巨大。为了解决这个痛点,在架构组同事建议下,引入了BitMap,将组装好的司机id数组转换成RoaringBitmap格式,再传入到OLAP引擎里面使用。

    在数仓应用层,由于引入了BitMap,重构了原本的数据表结构及链路,并优化了数仓的分层。不仅让整个链路耗时缩短了2个多小时,还节省了后续往OLAP引擎导数的时间,再算上数仓层的计算与存储资源的节省,很完美的实现了降本增效!而在OLAP引擎层,由架构组的同事通过二次开发,支持了Bitmap的使用,也取得了很不错的效果。

    到此,关于“BitMap使用实例代码分析”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程笔记网站,小编会继续努力为大家带来更多实用的文章!


    推荐阅读
    • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
    • 提升Python编程效率的十点建议
      本文介绍了提升Python编程效率的十点建议,包括不使用分号、选择合适的代码编辑器、遵循Python代码规范等。这些建议可以帮助开发者节省时间,提高编程效率。同时,还提供了相关参考链接供读者深入学习。 ... [详细]
    • CSS3选择器的使用方法详解,提高Web开发效率和精准度
      本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
    • Java容器中的compareto方法排序原理解析
      本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
    • JavaSE笔试题-接口、抽象类、多态等问题解答
      本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
    • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
    • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
    • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
    • [大整数乘法] java代码实现
      本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
    • WebSocket与Socket.io的理解
      WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
    • Java中包装类的设计原因以及操作方法
      本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
    • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
    • 配置IPv4静态路由实现企业网内不同网段用户互访
      本文介绍了通过配置IPv4静态路由实现企业网内不同网段用户互访的方法。首先需要配置接口的链路层协议参数和IP地址,使相邻节点网络层可达。然后按照静态路由组网图的操作步骤,配置静态路由。这样任意两台主机之间都能够互通。 ... [详细]
    • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
    • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
    author-avatar
    mobiledu2502881853
    这个家伙很懒,什么也没留下!
    PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有