数组与链表的应用-位图数组在Redis中的应用

位数组(Bit Array),这种数组如何在Redis中应用?

统计每个月学习专栏的用户活跃度

  在开始之前,先来考虑一个关于用户行为分析的问题,假设要统计《数据结构精讲:从原理到实战》这个专栏每个月的用户活跃度。在每个月中只要有用户登录并且学习这个专栏,都会将这个用户的ID写入一张MySql表中,如果想知道2019年11和12这两个月内都在学习这个专栏的用户有多少,应该怎么做呢?

很直观的做法是执行类型下面的sql语句

1 SELECT COUNT(*) FROM nov_stats
2 INNER JOIN dec_stats
3 ON nov_stats.user_id = dec_stats.user_id
4 WHERE nov_stats.logged_in_month = "2019-11"
5 AND dec_stats.logged_in_month = "2019-12";

  不过这种做法需要到数据库中去读取数据并做内连接,效率不是那么高,是不是有更简单高效的做法呢?请看接下来的内容:

比特与字节

  我们经常听到一些人打趣的说:“在程序员的眼中,永远只有0和1”。确实,计算机处理信息的最小单位就是一个二进制单位,即比特(Bit),而8个二进制比特位都可以组成另一个单位,及字节(Byte),字节是内存空间中的一个基本单位。 

  因位比特只能表达“0”或者“1”两种状态,它非常适合用来表示布尔型的状态。例如,我们可以用比特表示用户是否有订阅《数据结构精讲:从原理到实战》这个专栏,“0”的状态表示没有订阅,“1”的状态表示已经订阅。

  如果我们声明一个以比特为基本单位的数组,应该怎么做呢?我们都知道,在高级语言里面,是无法声明一个以比特为单位的基本类型的,而比特只有“0”或者“1”两种状态,那最简单的方法是可以声明一个以int为单位的数组,这个数组的值只能为“0”或者“1”,用来表示比特位的“0”和“1”。

  下面以java为例,假设我们要声明一个大小为2的“比特数组”,其代码如下所示:

  int[] d = new int[2];

  根据上面的声明,我们可以利用这个数组表示两种不同的状态。但是这种方法有一个明显的缺点,那就是消耗过多的存储空间。无论是在32位还是64位的机器上,int这种基本类型在java中的大小都是真4个字节空间的,及总共有4*8=32个比特位。从理论上来说,我们只需要其中一个比特位来记录状态,所以这里整个数组浪费掉了31/32= 96.875%的内存空间。

位数组

  那有没有更好的方法呢?当然有,既然一个int类型是有32个比特位的,我们其实可以把数组中的一个int类型元素当作可以表达布尔状态的32个比特位元素。这种将每个元素中每一个比特位都作为状态信息存储的数组称之为位数组(Bit Array)或者位图(Bit Map)。

  那我们来看看上面声明的拥有两个元素的int数组的内存模型是怎样的?

  这个位数组总共可以表达64个状态位,通过上图,我们可以得知,位数组在内存中的结构以及这个位数组索引的分部。

  当我们要操作位数组在位置为“i”这个比特位的时候,应该如何定位它呢?很简单,可以按照下面的公式来定位。

  所在数组中的元素为:        i / data_size

  比特位在元素中的位置为: i % data_size

  那我们以定义索引为35这个比特位为例子来说明一下,套用上面的公式,可以得出:

  所在数组中的元素为:        35 / 32  = 1

  比特位在元素中的位置为: 35 % 32 = 3

  所以这个比特位是位于d[1]这个元素上索引为3的位置。

  一般来说因为位数组的元素只保存“0”或者“1”两种状态,所以对于位数组的操作有以下几种:

  • 获取某个位置状态的比特位
  • 设置某个位置的比特位,也就是将那个位置的比特位设置为“1”;
  • 清除某个位置的比特位,也就是将那个位置的比特位设置为“0”。

位数组的实现

  下面以java为例,自己动手来实现这三个操作的核心部分。

  (1)GetBit

  我们可以声明GetBit的方法签名为: 

1 boolean GitBit(int[] array,long index);

  这个方法用于获取在array位数组中index位上的比特位是什么状态,如果为“1”,则返回true,如果为“0”,则返回false。

  根据上面的介绍,获取比特位所在的元素以及比特位在元素中的位置公式核心的算法如下:

 1 boolean GitBit(int[] array,long index){
 2     ...
 3     int elementIndex = index / 32;
 4     int position = index % 32;
 5     long flag = 1;
 6     flag = flag << position;
 7     if((array[elementIndex] & flag) != 0){
 8         return true;
 9     } else {
10         return false;
11     }
12 }

  我们用以下这个位数组来验证一下,假设这个位数组的值如下图所示:

  

  如果调用了GetBit(d,35)这条语句,将得到elementIndex为1、position为3、flag为0x08,将d[1]和0x08进行位操作的与运算,最后可以得出一个非0的结果,所以这个函数返回true。

  而如果调用了GitBit(d,32)这条语句,将得到elementIndex为1、position为0、flag为0x1,将d[1]和0x1进行位操作的与运算,最后可以得出一个0的结果,所以这个函数返回false。

SetBit

  我们可以声明SetBit方法的签名为:

1 void SetBit(int[] array,long index);

  这个方法用于将array数组中index位上比特位设置为1。

  根据前面的介绍,获取比特位所在的元素及比特位所在元素位置中的公式,核心算法如下:

1 void SetBit(int[] array,long index){
2     ...
3     int elementIndex = index / 32;
4     int position = index % 32;
5     long flag = 1;
6     flag = flag << position;
7     array[elementIndex] | false;
8 }

  我们用下面这个数组来验证一下,假设这个数组的值如下图所示:

  如果调用了SetBit(d,35)这条语句,我们将得到elementIndex为1、position为3、flag为0x08,将d[1]与0x08进行位操作的或运算,设置完成后位数组的状态如下图所示:

 ClearBit

  我们可以声明CleatBit方法签名为:

void ClearBit(int[] array,long index);

  这个方法用户将array位数组中index位上比特位设置为0。

  根据前面的介绍,获取比特位所在的元素以及比特位在元素中的位置公式,核心算法如下:

1 void ClearBit(int[] array,long index){
2     ...
3     int elementIndex = index / 32;
4     int position = index % 32;
5     long flag = 1;
6     flag = ~(flag << position);
7     array[elementIndex] & flag;
8 }

  我们用下面这个数组来验证一下,假设这个数组的值如下入所示:

  如果调用了ClearBit(d,32)这条语句,我们将得到elementIndex为1,position为1,flag为0xFFFFFFFE,将d[1]和0xFFFFFFFE进行位操作的与运算,设置完之后位数组的状态如下入所示:

  上面所介绍的三个位数组操作时间复杂度都是O(1)。

Redis中的BitMap数据结构

  数组在Redis中的应用。Redis是开源的并且使用内存来作为存储空间的高效数据库。

  下面介绍一下Redis里面的一个数据结构——BitMap。BitMap在这里其实就是我们刚刚讲解的位数组。

  BitMap的本质其实就是在Redis里面通过一个Strings类型来表示的,在Redis中Strings类型的最大长度是512MB。  

  也就是说根据上面的计算,BitMap可以用来表示大概42亿多个状态,这对于大多数的应用已经足够了。

  在Redis里面对BitMap的操作命令有以下几种:BITCONUT、BITFIELD、BITOP、GETBIT、SETBIT。其中,GETBIT和SETBIT命令和前面我们自己所实现的GetBIt和SetBit操作原理是一样的。

  回到最开始的那个问题:如果想知道2019年11和12月份学习专栏的用户数是多少,该怎么做呢?

  我们可以用Redis里的BITCOUNT、SETBIT和BITOP来完成。BITCOUNT这个命令其实可以计算一个位数组里面有多少比特位是为“1”的,而BITOP可以针对位数组进行“与”、“或”、“非”、“异或”这样的操作。

  首先针对11月学习的用户和12月学习的用户,我们可以为它们创建单独的位数组,例如,logins:2019:11 logins:2019:12,在11月每当用户学习时,程序会自动调用“logins:2019:11 user_id 1”,同理,对于12月学习的用户,我们也可以调用“logins:2019:12 user_id 1”。SETBIT命令可以将user_id在位数组中相应的位设为“1”。

  当要获得两个月内同时都学习了这个专栏的用户数时,可以调用“BITOP AND logins:2019:11-12 logins:2019:11 logins:2019:12”。将logins:2019:11和logins:2019:12这两个数组做位运算中的与操作,将结果放在“logins:2019:11-12”这个位数组中,最后调用“BITCOUNT logins:11-12”来得到结果。

  Redis的BitMap操作之所以强大,是因为所有操作都是位运算以及发生在内存中,所以速度极快。

心得:

  写到这,这一节课的笔记就算抄写完了,一开始,写一句话思考一句话的时候觉得都还能懂,但是众观全局看下来,又是似懂非懂,尤其是写到最后一节,“Redis中BitMap数据结构”,有些疑虑,茫然,可能是没有接触过Redis的原因,所以只是表面上懂了,略懂皮毛。2020年加油,向20W努力!!!

原文地址:https://www.cnblogs.com/gxkeven/p/12096528.html

时间: 2024-08-29 18:20:04

数组与链表的应用-位图数组在Redis中的应用的相关文章

数组和链表的区别以及数组和结构体的区别

1,数组和链表的区别? 链表和数组都叫可以叫做线性表, 数组又叫做顺序表,主要区别在于,顺序表是在内存中开辟一段连续的空间来存储数据,而且必须是相同类型的数据. 而链表是通过存在元素中的指针联系到一起的,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域,链表既可以靠指针来连接多块不连续的的空间也可以用一段连续的空间, 在逻辑上形成一片连续的空间来存储数据. 两种数据结构各有各的好处,链表方便删除和插入,数组方便排序等. 数组从栈中分配空间, 对于程序员方便快速

数组与链表的优缺点

数组,在内存上给出了连续的空间.链表,内存地址上可以是不连续的,每个链表的节点包括原来的内存和下一个节点的信息(单向的一个,双向链表的话,会有两个). 数组优于链表的: 1.内存空间占用的少,因为链表节点会附加上一块或两块下一个节点的信息.但是数组在建立时就固定了.所以也有可能会因为建立的数组过大或不足引起内存上的问题. 2.数组内的数据可随机访问.但链表不具备随机访问性.这个很容易理解.数组在内存里是连续的空间.比如如果一个数组地址从100到200,且每个元素占用两个字节,那么100-200之

数组、链表、跳表

数组.链表.跳表 数组.链表.跳表基本实现和特性 Array(数组) java,c++: int a[100];//初始化的时候先定义好容量 Python: list=[]//直接定义一个数组 JavaScript: let x=[1,2,3] 时间复杂度 方法 复杂度 prepend O(n) append O(1) lookup O(1) insert O(n) delete O(n) 成员函数 元素访问 at?访问指定元素,同时进行越界检查 operator[]?访问指定的元素 front

数据存储的常用结构 堆栈、队列、数组、链表

数据存储的常用结构有:堆栈.队列.数组.链表.我们分别来了解一下: 堆栈,采用该结构的集合,对元素的存取有如下的特点: 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素).例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹. 栈的入口.出口的都是栈的顶端位置 压栈:就是存元素.即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置. 弹栈:就是取元素.即,把栈的顶端位置元素取出,栈中已有元素依次

数组还是链表?

1.什么是数组和链表? 数组和链表都是2这种不同的数据结构.数据结构可以简单理解为数据之间关系.数组是在内存上是一个联系的结构,而链表可以是一段不连续的结构.两者还有其他的区别: a.数组是静态分配内容,链表是动态分配内存. b.根据下标去获取结构里的第n个元素,数组的时间复杂读为O(1),链表的复杂度为O(n) c.删改第n个元素,数组的时间复杂度为O(n),链表的时间复杂读为O(1) 2.图形比较: 参考:http://blog.csdn.net/amork/article/details/

UVA11988 Broken Keyboard (a.k.a. Beiju Text)【数组模拟链表】

Broken Keyboard (a.k.a. Beiju Text) You're typing a long text with a broken keyboard. Well it's not so badly broken. The only problem with the keyboard is that sometimes the "home" key or the "end" key gets automatically pressed (inter

5.三目运算符,C语言数组,链表和Python字符串,列表的联系

1.三目运算,三元运算 if l==1: name = "alex" else: name = "eric" name = "alex" if l==1 esle "eric" print(name) 2.c与python的联系 str,字符串的功能一般是生成一个新的字符串(去括号,替换等)列表,字典的功能一般是在它们里面做修改这是为什么呢? li = [11, 22] 列表若是在地址中连续存储的话,那么我们要插入,修改要需要

数组和链表区别

数组和链表的区别: 二者都属于一种数据结构从逻辑结构来看1. 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况.当数据增加时,可能超出原先定义的元素个数:当数据减少时,造成内存浪费:数组可以根据下标直接存取.2. 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入.删除数据项.(数组中插入.删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据next指针找到下一个元素从内存存储来看      1. (静态)数组从栈中分配空间, 对于程序员方便快速,但是

数组和链表的区别

数组和链表的区别 数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素.但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中.同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素.如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组. 链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起.比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一