位图数据结构及其在-Java和-Redis中的应用

目录

位图的基本介绍

概念

什么是位图?BitMap,你们直译为位图. 个人理解是:位图是内存中连续的二进制位(bit),能够用做对大量整形作去重和统计.java

引入一个小栗子来帮助理解一下:程序员

假如咱们要存储三个int数字 (1,3,5),在java中咱们用一个int数组来存储,那么占用了12个字节.可是咱们申请一个bit数组的话.而且把相应下标的位置为1,也是能够表示相同的含义的,好比redis

下标 0 1 2 3 4 5 6 7
二进制值 0 1 0 1 0 1 0 0

能够看到,对应于1,3,5为下标的bit上的值为1,咱们或者计算机也是能够get到1,3,5这个信息的.数据库

优点

那么这么作有什么好处呢?感受更麻烦了鸭,下面这种存储方式,在申请了bit[8]的场景下才占用了一个字节,占用内存是原来的12分之一,当数据量是海量的时候,好比40亿个int,这时候节省的就是10几个G的内存了.数组

这就引入了位图的第一个优点,占用内存小.安全

再想一下,加入咱们如今有一个位图,保存了用户今天的签到数据.下标能够是用户的ID.bash

A:服务器

用户ID 0 1 2 3 4 5 6 7
二进制值 0 1 0 1 0 1 0 0

这表明了用户(1,3,5)今天签到了.数据结构

固然还有昨天的位图,性能

B:

用户ID 0 1 2 3 4 5 6 7
二进制值 0 1 1 1 0 0 0 1

这表明了用户(1,2,3,7)昨天签到了.

咱们如今想求:

  1. 昨天和今天都签到的用户.
  2. 昨天或者今天签到的用户.

在关系型数据库中存储的话,这将是一个比较麻烦的操做,要么要写一些表意不明的SQL语句,要么进行两次查询,而后在内存中双重循环去判断.

而使用位图就很简单了,A & B, A | B 便可.上面的操做明显是一个集合的与或操做,而二进制自然就支持逻辑操做,且众所周知猫是液体.错了,众多周知是计算机进行二进制运算的效率很高.

这就是位图的第二个优势: 支持与或运算且效率高.

哇,这么完美,那么哪里能够买到呢?,那么有什么缺点呢?

不足

固然有,位图不能很方便的支持非运算,(固然,关系型数据库支持的也很差).这句话可能有点难理解.继续举个例子:

咱们想查询今天没有签到的用户,直接对位图进行取非是不能够的.

对今天签到的位图取非获得的结果以下:

用户ID 0 1 2 3 4 5 6 7
二进制值 1 0 1 0 1 0 1 1

这意味着今天(0,2,4,6,7)用户没有签到吗?不是的,存在没有7(任意数字)号用户的状况,或者他注销了呢.

这是由于位图只能表示布尔信息,即true/false.他在这个位图中,表示的是XX用户今天有签到或者没有签到,可是不能额外的表达,xx用户存在/不存在这个状态了.

可是咱们能够曲线救国,首先搞一个全集用户的位图.好比:

全集:

用户ID 0 1 2 3 4 5 6 7
二进制值 1 1 1 1 1 0 1 0

而后用全集的位图和签到的位图作异或操做,相同则为0,不相同则为1.

在业务的逻辑为: 用户存在和是否签到两个bool值,共四种组合.

  1. 用户存在,且签到了. 两个集合的对应位都为1,那么结果就为0.
  2. 用户存在,可是没签到. 全集对应位为1,签到为0,因此结果是1.
  3. 用户不存在,那么必然没可能签到, 两个集合的对应位都是0,结果为0.

因此结果中,为1的只有一种可能:用户存在且没有签到,正好是咱们所求的结果.

A ^ 全集:

用户ID 0 1 2 3 4 5 6 7
二进制值 1 0 1 0 1 0 1 0

此外,位图对于稀疏数据的表现不是很好,(固然聪明的大佬们已经基本解决掉了这个问题).原生的位图来说,若是咱们只有两个用户,1号和100000000号用户,那么直接存储int须要8个字节也就是32个bit,而用位图存储须要1亿个bit.当数据量少,且跨度极大也就是稀疏的时候,原生的位图不太适合.

点击这里跳转到稀疏数据的解决方案

总结

那么咱们来作一下总结:

位图是用二进制位来存储整形数据的一种数据结构,在不少方面都有应用,尤为是在大数据量的场景下,节省内存及提升运算效率十分实用.

他的优势有:

  1. 节省内存. -> 所以在大数据量的时候更加显著.
  2. 与或运算效率高. ->能够快速求交集和并集.

缺点有:

  1. 不能直接进行非运算. -> 根本缘由是位图只能存储一个布尔信息,信息多了就须要借助全量集合等数据辅助.
  2. 数据稀疏时浪费空间. -> 这个不用很担忧,后面会讲到大佬们的解法,基本能够解决掉.
  3. 只能存储布尔类型. -> 有限制,可是业务中不少数据均可以转换为布尔类型.好比上面的例子中, 业务原意:用户天天的签到记录,以用户为维度. 咱们能够转换为: 天天的每一个用户是否签到,就变为了布尔类型的数据.

Java中的位图

上面讲了位图的原理,那么咱们先来本身手动实现一个!

简陋版本

说明:由于后面还有JDK版本,因此这里只实现了很简陋的版本,方便理解位图的核心原理便可.这个简陋版本彻底不能够直接使用,能跑,可是在不少状况下都会直接报错.

虽然简陋,可是必须的仍是要有.

构造方法

写了一个仅支持bit数量的构造参数. 由于咱们是用int数组来保存实际的数据,因此对传入的值右移5(至关于除以32,由于int是32位的嘛)就是int数组的大小.

set方法

支持将某一个位设置为true/false.

为了实现set-true,实际上是有粗暴的符合人类思路的逻辑的,好比当调用set(5,true)的时候,咱们将int数字转化为二进制字符串,获得000000000000000000000000000000(应该是32个我没数),而后将其右边第六位置为1,获得000000000000000000000000100000,而后再转回int数字.

这个方法很符合位图的直接定义,也很好理解,可是对于计算机来讲,太麻烦了,并且过程当中须要一个String,占用太多的内存空间了.

计算机更喜欢使用或运算来解决. 假设现有数字为3,即000000000000000000000000001000,这时候咱们调用了set(10,true),怎么办呢,首先使用左移,将第11位置为1,而后与原来的值进行或操做.像下面这样子:

原来值 :     000000000000000000000000001000
1右移10位:   000000000000000000010000000000

或操做的结果: 000000000000000000010000001000   ----> 能够直接表示 3 和 10 两个位上都为1了.
复制代码

设置某一个位为false,和上面的流程不太同样.除去粗暴的办法以外,还能够 对1右移x位.很拗口,下面是示例:

咱们将3上的设为0.

原来值 :              000000000000000000010000001000    ----> 10和3上为1,
1右移3位:             000000000000000000000000001000
1右移3位取非后:        111111111111111111111111110111

原来的值与取非后取与:   000000000000000000010000000000   ----> 只有10上为1了.
复制代码

get方法

获取某个位上的值.

固然也能够用粗暴的转换二进制字符串解决,可是使用与操做更加快速且计算机友好.

对set方法中的例子来讲,设置了3和10以后,若是获取10上的值,能够:

当前值:        000000000000000000010000001000
1右移10位:     000000000000000000010000000000

与操做的结果:   000000000000000000010000000000    ---> 只要这个数字不等于0,即说明10上为1,等于0则为0.
复制代码

实际的代码加注释以下:

/** * Created by pfliu on 2019/07/02. */
public class BitMapTest {
    // 实际使用int数组存储
    private int[] data;

    /** * 构造方法,传入预期的最大index. */
    public BitMapTest(int size) {
        this.data = new int[size >> 5];
    }

    /** * get 方法, 传入要获取的index, 返回bool值表明该位上为1/0 */
    public boolean get(int bitIdx) {
        return (data[bitIdxToWorkIdx(bitIdx)] & (1 << bitIdx)) != 0;
    }

    /** * 将对应位置的值设置为传入的bool值 */
    public void set(int idx, boolean v) {
        if (v) {
            set(idx);
        } else {
            clear(idx);
        }
    }

    // 将index的值设置为1
    private void set(int idx) {
        data[bitIdxToWorkIdx(idx)] |= 1 << idx;
    }

    // 将index上的值设置为0
    private void clear(int bitIdx) {
        data[bitIdxToWorkIdx(bitIdx)] &= ~(1L << bitIdx);
    }

    // 根据bit的index获取它存储的实际int在数组中的index
    private int bitIdxToWorkIdx(int bitIdx) {
        return bitIdx >> 5;
    }

    public static void main(String[] args) {

        BitMapTest t = new BitMapTest(100);
        t.set(10, true);

        System.out.println(t.get(9));
        System.out.println(t.get(10));

    }
}
复制代码

JDK版本(BitSet源码阅读)

JDK中对位图是有实现的,实现类为BitSet,其中大体思想和上面实现的简陋版本相似,只是其内部数据是使用long数组来存储,此外加了许多的容错处理.下面看一下源码.仍是按照方法分类来看.

常量及变量

// long数组,64位的long是2的6次方
    private final static int ADDRESS_BITS_PER_WORD = 6;
    // 每个word的bit数量
    private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;

    // 存储数据的long数组
    private long[] words;
    // 上面的数组中使用到了的word的个数
    private transient int wordsInUse = 0;
    // 数组的大小是否由用户指定的(注释里写明了:若是是true,咱们假设用户知道他本身在干什么)
    private transient boolean sizeIsSticky = false;
复制代码

构造方法及工厂方法

BitSet提供了两个公开的构造方法以及四个公开的工厂方法,分别支持从long[],LongBuffer,bytes [], ByteBuffer中获取BitSet实例.

各个方法及其内部调用的方法以下:

// ---------构造方法-------

    // 无参的构造方法,初始化数组为长度为64个bit(即一个long)以及设置sizeIsSticky为false.
    public BitSet() {
        initWords(BITS_PER_WORD);
        sizeIsSticky = false;
    }

    // 根据用户传入的bit数量进行初始化,且设置sizeIsSticky为true.
    public BitSet(int nbits) {
        // nbits can't be negative; size 0 is OK
        if (nbits < 0)
            throw new NegativeArraySizeException("nbits < 0: " + nbits);

        initWords(nbits);
        sizeIsSticky = true;
    }
    // ---------构造方法的调用链 -------

    // 初始化数组
    private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }

    // 根据bit数量获取long数组的大小,右移6位便可.
    private static int wordIndex(int bitIndex) {
        return bitIndex >> ADDRESS_BITS_PER_WORD;
    }

    // ---------工厂方法,返回BitSet实例 -------

    // 传入long数组
    public static BitSet valueOf(long[] longs) {
        int n;
        for (n = longs.length; n > 0 && longs[n - 1] == 0; n--)
            ;
        return new BitSet(Arrays.copyOf(longs, n));
    }

    // 传入LongBuffer
    public static BitSet valueOf(LongBuffer lb) {
        lb = lb.slice();
        int n;
        for (n = lb.remaining(); n > 0 && lb.get(n - 1) == 0; n--)
            ;
        long[] words = new long[n];
        lb.get(words);
        return new BitSet(words);
    }

    // 传入字节数组
    public static BitSet valueOf(byte[] bytes) {
        return BitSet.valueOf(ByteBuffer.wrap(bytes));
    }

    // 传入ByteBuffer
    public static BitSet valueOf(ByteBuffer bb) {
        bb = bb.slice().order(ByteOrder.LITTLE_ENDIAN);
        int n;
        for (n = bb.remaining(); n > 0 && bb.get(n - 1) == 0; n--)
            ;
        long[] words = new long[(n + 7) / 8];
        bb.limit(n);
        int i = 0;
        while (bb.remaining() >= 8)
            words[i++] = bb.getLong();
        for (int remaining = bb.remaining(), j = 0; j < remaining; j++)
            words[i] |= (bb.get() & 0xffL) << (8 * j);
        return new BitSet(words);
    }
复制代码

set方法

BitSet提供了两类set方法,

  1. 单点set. 将某个index设置为tue/false.
  2. 范围set. 将某个范围值设置为tue/false.

所以BitSet有四个重载的set方法.

// 将某个index的值设置为true. 使用和上面本身实现的简陋版本相同的或操做.
    public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);

        words[wordIndex] |= (1L << bitIndex); // Restores invariants

        checkInvariants();
    }

    // 将某个index设置为传入的值,注意当传入值为false的时候,调用的是clear方法.
    public void set(int bitIndex, boolean value) {
        if (value)
            set(bitIndex);
        else
            clear(bitIndex);
    }

    // 将index上bit置为0
    public void clear(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        int wordIndex = wordIndex(bitIndex);
        if (wordIndex >= wordsInUse)
            return;

        words[wordIndex] &= ~(1L << bitIndex);

        recalculateWordsInUse();
        checkInvariants();
    }

    // 将from->to之间的全部值设置为true
    public void set(int fromIndex, int toIndex) {
        checkRange(fromIndex, toIndex);

        if (fromIndex == toIndex)
            return;
        // Increase capacity if necessary
        int startWordIndex = wordIndex(fromIndex);
        int endWordIndex   = wordIndex(toIndex - 1);
        expandTo(endWordIndex);

        long firstWordMask = WORD_MASK << fromIndex;
        long lastWordMask  = WORD_MASK >>> -toIndex;
        if (startWordIndex == endWordIndex) {
            // Case 1: One word
            words[startWordIndex] |= (firstWordMask & lastWordMask);
        } else {
            // Case 2: Multiple words
            // Handle first word
            words[startWordIndex] |= firstWordMask;

            // Handle intermediate words, if any
            for (int i = startWordIndex+1; i < endWordIndex; i++)
                words[i] = WORD_MASK;

            // Handle last word (restores invariants)
            words[endWordIndex] |= lastWordMask;
        }

        checkInvariants();
    }

    // 将from->to之间的全部值设置为传入的值,当传入的值为false的适合,调用的是下面的clear.
    public void set(int fromIndex, int toIndex, boolean value) {
        if (value)
            set(fromIndex, toIndex);
        else
            clear(fromIndex, toIndex);
    }

    // 将范围内的bit置为0
    public void clear(int fromIndex, int toIndex) {
        checkRange(fromIndex, toIndex);

        if (fromIndex == toIndex)
            return;

        int startWordIndex = wordIndex(fromIndex);
        if (startWordIndex >= wordsInUse)
            return;

        int endWordIndex = wordIndex(toIndex - 1);
        if (endWordIndex >= wordsInUse) {
            toIndex = length();
            endWordIndex = wordsInUse - 1;
        }

        long firstWordMask = WORD_MASK << fromIndex;
        long lastWordMask  = WORD_MASK >>> -toIndex;
        if (startWordIndex == endWordIndex) {
            // Case 1: One word
            words[startWordIndex] &= ~(firstWordMask & lastWordMask);
        } else {
            // Case 2: Multiple words
            // Handle first word
            words[startWordIndex] &= ~firstWordMask;

            // Handle intermediate words, if any
            for (int i = startWordIndex+1; i < endWordIndex; i++)
                words[i] = 0;

            // Handle last word
            words[endWordIndex] &= ~lastWordMask;
        }

        recalculateWordsInUse();
        checkInvariants();
    }
复制代码

这里有一个须要注意点,那就是当传入的值为true/fasle的时候,处理逻辑是不一样的.具体的逻辑见上面简陋版本中的示例.

get方法

BitSet提供了一个获取单个位置bit值的方法,以及一个范围获取,返回一个新的BitSet的方法.

// 获取某个位置的bit值
    public boolean get(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        checkInvariants();

        int wordIndex = wordIndex(bitIndex);
        return (wordIndex < wordsInUse)
            && ((words[wordIndex] & (1L << bitIndex)) != 0);
    }

    // 返回一个子集,包含传入范围内的bit
    public BitSet get(int fromIndex, int toIndex) {
        checkRange(fromIndex, toIndex);

        checkInvariants();

        int len = length();

        // If no set bits in range return empty bitset
        if (len <= fromIndex || fromIndex == toIndex)
            return new BitSet(0);

        // An optimization
        if (toIndex > len)
            toIndex = len;

        BitSet result = new BitSet(toIndex - fromIndex);
        int targetWords = wordIndex(toIndex - fromIndex - 1) + 1;
        int sourceIndex = wordIndex(fromIndex);
        boolean wordAligned = ((fromIndex & BIT_INDEX_MASK) == 0);

        // Process all words but the last word
        for (int i = 0; i < targetWords - 1; i++, sourceIndex++)
            result.words[i] = wordAligned ? words[sourceIndex] :
                (words[sourceIndex] >>> fromIndex) |
                (words[sourceIndex+1] << -fromIndex);

        // Process the last word
        long lastWordMask = WORD_MASK >>> -toIndex;
        result.words[targetWords - 1] =
            ((toIndex-1) & BIT_INDEX_MASK) < (fromIndex & BIT_INDEX_MASK)
            ? /* straddles source words */
            ((words[sourceIndex] >>> fromIndex) |
             (words[sourceIndex+1] & lastWordMask) << -fromIndex)
            :
            ((words[sourceIndex] & lastWordMask) >>> fromIndex);

        // Set wordsInUse correctly
        result.wordsInUse = targetWords;
        result.recalculateWordsInUse();
        result.checkInvariants();

        return result;
    }
复制代码

逻辑操做

JDK实现的位图固然是有逻辑操做的,主要支持了与,或,异或,与非四种操做,因为代码不难,这里就不贴代码了,简略的贴一下API.

// 与操做
    public void and(BitSet set);
    // 或操做
    public void or(BitSet set);
    // 异或操做
    public void xor(BitSet set);
    // 与非操做
    public void andNot(BitSet set);
复制代码

到这里,BitSet的源码就读完了,可是有没有发现一个问题 ? 前面说的稀疏数据的问题并无获得解决,别急,下面就来了.

EWAHCompressedBitmap

这是google开发的javaEWAH包中的一个类.名字中的EWAH = Enhanced Word-Aligned Hybrid.而Compressed是指压缩.

复习一下稀疏数据的问题,假设咱们在一个位图中,首先set(1),而后set(1亿)会怎样?

咱们使用JDK中的BitSet来试一下,在运行过程当中打断点看一下内部的数组是什么样子.以下图:

2019-07-02-17-12-04

将其序列化输出到文件,文件大小以下图:

2019-07-02-17-19-12

能够看到,咱们为了保存1和1亿这两个数字,花费了一个一千多万长度的long数组,序列化后占用内存将近200m.这是不科学的.

接下来就是EWAHCompressedBitmap了,名字里面都带了压缩,那么想必表现不错.

2019-07-02-17-18-11
2019-07-02-17-18-47

能够看到long数组的长度仅仅为4,且输出的文件大小为96byte.

这就很符合预期了.

在EWAHCompressedBitmap中,数据也是使用long数组来保存的,不过对每个long有类别的定义,Literal WordRunning Length Word.

  • Literal Word: 存储真正的bit位.

  • Running Length Word: 存储跨度信息.

什么是跨度信息呢? 举个例子:

在刚才使用BitSet存储1亿的时候,截图中long数组有一千多万个0,以及以后的一个值.

使用BitSet存储1和1亿(2048为虚拟值,不想算了):

long long long long long long long long long long long long
2 0 0 0 0 0 0 0 ...1千万个0呢 0 0 2048

而在EWAHCompressedBitmap中,则是相似下面这样:

long long long
2 一千万个0 2048

这样看起来好像没什么区别....可是在BitSet中,一千万个0是真的使用了一千万个long来存储的.而在EWAHCompressedBitmap中,这个信息使用一个long来存储,long的值表示具体有多少个0在这个区间内.

这样子作,点题了.与名字中的压缩相对应.将连续的0或者1进行压缩,以节省空间.

这样作有没有什么反作用呢?有的,当你的每一次插入都在一个Running Length Word上,也就是每一次插入都涉及到了Running Length Word的分裂,会降级性能,所以官方建议最好数据的插入从小到大进行.

EWAHCompressedBitmap基本解决了稀疏数据的问题,而当数据很稠密的时候,他的压缩率没有那么好,可是一般也不会差于不压缩的存储方式,所以在平常的使用中,仍是建议你们使用这个类,除非你很清楚且能确保本身的数据不会过于稀疏.

总结

在本节,咱们手动实现了一个极其简陋的位图,而后阅读了JDK中位图实现类BitSet的源码,而后分析了如何使用EWAHCompressedBitmap来解决稀疏数据的问题,对于EWAHCompressedBitmap的源码具体实现没有详细分析,有兴趣的朋友能够本身去查看.

Java语言使用者普遍,所以对于位图的实现,网上各类版本都有,既有大厂维护的开源版本,也有我的编写的版本.在使用时也不用彻底局限于EWAHCompressedBitmap,可使用各类魔改版本,因为位图的实现逻辑不是特别复杂,所以在使用前清楚其具体的实现逻辑便可.

Redis中的位图

这是redis官网对位图的介绍,很短....

Redis是支持位图的,可是位图并非一个单独的数据结构,而是在String类型上定义的一组面向位的操做指令.也就是说,当你使用Redis位图时,其实底层存储的是Redis的string类型.所以:

  1. 因为Redis的string是二进制安全的,因此用它当作位图的存储方式是可行的.
  2. Redis 的String类型最大是512Mb.因此Redis的单个位图只能存储2的32个次方个int.这应该是够用了.(不够用的话能够分key,用前缀来搞.)
  3. 因为底层是string,所以redis是没有对稀疏数据进行处理的,所以在使用时要额外注意这一点,防止这个key拖垮redis服务器.

Redis支持的操做以下:

  • getbit: 获取某个key的某个位置的值. getbit key offset.
  • setbit: 设置某个位置的值. setbit key offset value.
  • bitcount: 计算某个key中为1的bit数量.支持范围. bitcount key start end
  • bitpos: 返回范围内第一个为特定bit的位置. bitpos key bit(0/1) start end
  • bitop: 逻辑运算,支持四种逻辑运算,和上面BitSet支持的四种同样,具体的命令以下:
BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP NOT destkey srckey
复制代码

其中destkey是结果存储的key,其他的srckey是参与运算的来源.

应用场景

应用场景实际上是很考验人的,不能学以至用,在程序员行业里基本上就至关于没有学了吧...

通过本身的摸索以及在网上的浏览,大体见到了一些应用场景,粗略的写出来,方便你们理解而且之后遇到相似的场景能够想到位图并应用他!

用户签到/抢购等惟一限制

用户签到天天只能一次,抢购活动中只能购买一件,这些需求致使的有一种查询请求,给定的id作没作过某事.并且通常这种需求都没法接受你去查库的延迟.固然你查一次库以后在redis中写入:key = 2345 , value = 签到过了.也是能够实现的,可是内存占用太大.

而使用位图以后,当2345用户签到过/抢购过以后,在redis中调用setbit 2019-07-01-签到 2345 1便可,以后用户的每次签到/抢购请求进来,只须要执行相应的getbit便可拿到是否放行的bool值.

这样记录,不只能够节省空间,以及加快访问速度以外,还能够提供一些额外的统计功能,好比调用bitcount来统计今天签到总人数等等.统计速度通常是优于关系型数据库的,能够用来作实时的接口查询等.

用户标签等数据

大数据已经很广泛了,用户画像你们也都在作,这时候须要根据标签分类用户,进行存储.方便后续的推荐等操做.

而用户及标签的数据结构设计是一件比较麻烦的事情,且很容易形成查询性能过低.同时,对多个标签常常须要进行逻辑操做,好比喜欢电子产品的00后用户有哪些,女性且爱旅游的用户有哪些等等,这在关系型数据库中都会形成处理的困难.

可使用位图来进行存储,每个标签存储为一个位图(逻辑上,实际上你还能够按照尾号分开等等操做),在须要的时间进行快速的统计及计算. 如:

用户 0 1 2 3 4 5 6 7 8
爱旅游 1 0 0 1 0 0 1 0 0

能够清晰的统计出,0,3,6用户喜欢旅游.

用户 0 1 2 3 4 5 6 7 8
00后 1 1 0 0 0 0 1 0 0

用户0,1,6是00后.

那么对两个位图取与便可获得爱旅游的00后用户为0,6.

布隆过滤器

这个就比较有名了,关于这个的详细信息能够查看 布隆过滤器(bloom filter)的原理及在推荐去重中的应用

总结

总之,bitmap能够高效且节省空间的存储与用户ID相关联的布尔数据.常见的能够应用其作大量数据的去重以及统计.更多的应用就开发你的想象力吧.

参考文章

redis官网

BitSet/EWAHCompressedBitmap源码


完。



ChangeLog

2019-07-02 完成

以上皆为我的所思所得,若有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文连接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见我的博客------>呼延十

相关文章
相关标签/搜索