究竟什么是位图

news2025/1/18 7:32:06

引言

为什么要写这篇博文呢,是因为之前面试的时候遇到这样一个问题:

有一款数十亿级别的用户产品,如何统计一周内连续活跃的用户数?

这个问题的特征其实很明显:数据量大,需要交并操作,而且还有一定的性能要求。当时想到了 BitMap 、布隆过滤器这些方法,但是由于理解的不够深入答案自然就漏洞百出。

在实际工作中发现,这类的问题的确是业务中需要解决的问题,并不是面试官为了刁难候选人奇思妙想出来的。

所以经过系统的学习后在此记录一番。


基础知识

在计算机系统中数据储存单位的转换关系是非常基础的内容,譬如:

  • 1B = 1 byte = 8 bit
  • 1KB = 1024 byte = 2^{10} byte
  • 1 MB = 1024 * 1024 byte = 2^{20} byte
  • 1GB = 1024*1024*1024 byte = 2^{30} byte

另外在一些主流的编程语言中,int 类型的数据占用的存储空间通常和操作系统的位数相关:

  • 在 32 位的操作系统中,int 类型的数据占 32 bit,可以表示 2^{32} 个数字,约43亿个。
  • 在 64 位的操作系统中,int 类型的数据占 64 bit,可以表示 2^{64} 个数字。

1024 = 2^{10} \approx 10^{3}

什么是位图

在​​维基百科上位图的解释是一种使用像素数组来表示的图像。

不过这篇博文记录的不是图像,而是一种数据结构,可以简单理解为位数组。位就是计算机中存储的二进制数据位——比特 (bit),位数组是由多个bit组成的数据结构。 

至于为什么叫 BitMap 而不是 BitArray,我理解这是和它的使用方式有关系的。

位图的优势

减少内存占用

回到面试官的问题中,假设我们有两个方案来记录用户是否活跃:

  1. 传统的 map 结构,map-key 使用 int32 类型记录用户的唯一标识,map-value 使用 bool 类型记录用户的活跃状态。
  2. 位图,使用位图的槽位表示用户的唯一标识,使用槽位对应的二进制 0/1 表示用户的活跃状态。

接下来我们比较这2个方案的内存消耗:(为了方便计算我们假定有10亿个用户)

  • 在方案1中,需要 10亿个 int32 类型的数字记录用户的唯一标识,也就是 10^{9} * 4 B \approx 2^{30} * 4 B = 4GB 的存储空间,这么大的数据肯定是无法在内存中计算的。
  • 而在方案2中,只需要长度为10亿的 bit 数组,约占用 10^{9} \approx 2^{30} bit = 2^{30} / 8 B = 1 / 8 GB = 2^{7} MB = 128MB 的存储空间。

通过比较位图可以极大的降低数据占用的存储空间,使得相关的运算在内存中就可以进行。

排序和去重

再回到面试官的问题中,假定用户的唯一标识越小表示用户注册的时间越早,现在需要按照注册时间给活跃用户进行排序然后输出去重的结果。

为了方便讨论问题我们假设只需要给唯一标识分别为 28、17、9、28、100 的这组数据进行去重和排序。

  1. 首先我们初始化一个长度为101的位图,并且设置每一位的值为 0。
  2. 遍历数据,标记唯一标识对应槽位的值为 1。
  3. 从槽位 0 开始遍历位图,输出值为 1 所在的槽位序列,该序列就是经过去重和排序后的活跃用户的列表。

集合运算

再假定产品中有两个功能分别为 P1 和 P2,现在需要计算这两个功能都喜欢的用户有多少。

同样为了方便讨论问题我们假定喜欢功能 P1 的用户为 1、2、9、10、30;喜欢功能 P2 的用户为 0、2、10、29、100。

从图中我们很明显可以看出来同时喜欢两个功能的用户是 2、10,但是我们通过什么手段可以快速计算得到结果呢?

不难发现两个位图对应的二进制值分别是

  • 0110 1100 0001 00
  • 1010 0100 0010 01

对它们进行 & 操作后可得到二进制值 0010 0100 0000 00,其结果中值为 1 的槽位就是2和10,也就是我们需要的结果。 

Golang实现

在 Golang 中 uint32 类型占用4字节,也就是32个bit,我们使用 uint32 类型的切片存储数据。在 Value[0] 中表示 0 ~31;Value[1] 中表示 32 ~ 63......

在实现过程中需要解决的核心问题就是如何找到数组在位图中的具体槽位.

  • 首先数字整除32,得到的结果就是 Value 的下标,即数字所处在 Value 切片的某段中。
  • 然后数字取模32,得到的结果就是 某一段中具体的位置。

譬如对于数字 36 整除 32 后得到 1,即数字 10 在 Value 切片的第 1 段 32~63 中;然后再取模 32 得到  4,即数字 36 对应的槽位是 Value[1] 中的第 5 个位置。注意下标是从0开始的。

具体实现如下:

package bitmap

// 定义数据结构
type BitMap struct {
	Value  []uint32
	length int
}

// Add: 添加一个数字, 如果待添加数字已存在则返回fasle,否则返回true
func (bitmap *BitMap) Add(num uint32) bool {
	if bitmap.IsExist(num) {
		return false
	}

	// 计算数字所在的槽位地址
	var bucketIndex = num / 32
	var bitIndex = num % 32

	if int(bucketIndex) >= len(bitmap.Value) {
		bitmap.Value = append(bitmap.Value, 0)
	}

	// 设置 bitIndex 槽位的值为1
	remainder := 1 << bitIndex
	bitmap.Value[bucketIndex] |= uint32(remainder)
	bitmap.length++

	return true
}

// Remove: 删除一个数字,如果待删除的数字不存在则返回false,否则返回true
func (bitmap *BitMap) Remove(num uint32) bool {
	if !bitmap.IsExist(num) {
		return false
	}
	var bucketIndex = num / 32
	var bitIndex = num % 32

    // 这里使用 -= 运算也可以
	bitmap.Value[bucketIndex] ^= 1 << bitIndex
	bitmap.length--
	return true
}

// Values: 获取添加过的数字,从小到大排列
func (bitmap *BitMap) Values() []uint32 {
	arr := make([]uint32, 0, bitmap.length)
	for index, val := range bitmap.Value {
		if val == 0 {
			continue
		}
		for j := uint(0); j < 32; j++ {
			if val&(1<<j) != 0 {
				arr = append(arr, uint32(32*uint(index)+j))
			}
		}
	}
	return arr
}

// IsExist: 判断数字是否存在
func (bitmap *BitMap) IsExist(num uint32) bool {
	var bucketIndex = num / 32
	var bitIndex = num % 32

	return int(bucketIndex) < len(bitmap.Value) &&
		bitmap.Value[bucketIndex]&(1<<bitIndex) != 0
}

func (bitmap *BitMap) Length() int {
	return bitmap.length
}

位图的缺点

由于位图是用槽位来表示具体的元素值,那天然的在表示稀疏数据的时候是不占优势的,因为位图中会有极大部分的空间是被浪费的。

目前使用压缩手段来解决空间浪费的问题。

RLE 编码 —— Run-Length Encoding

这是一种编码压缩的思想,其核心观点是把连续重复出现的值使用 值+出现次数 的方式表示,从而达到数据压缩的目的。譬如 101 0*20 11 表示该序列的值为 二进制串 101 和 11 中间有20个0,这里只是举个例子说明下这种思想。具体的实现该思想的算法在网上有很多资料可以作为扩展阅读,此处不做深入记录。

这种编码方式局限性比较明显,比如每次读写数据的时候都需要先对数据进行解压才能操作;另外不能进行位运算。

Roaring BitMap

这是一种高效的压缩算法,基本上很好的解决了 RLE 编码中存在的问题,目前使用也十分的广泛。

其核心的思想是把 32位数据分成 2^{16}个桶,使用数据的高16位作为桶的编号,每个桶存放一个 Container 存放数据的低16位数据。

一共有3种Containter,分别为 Array Container、BitMap Container、Run Container,其中 Run 指 RLE。其中 Array Container 用来存放稀疏数据,BitMap Container 用来存储稠密数据,Run Container 使用场景比较少。

具体得,当一个 Container 中的元素小于 4096 时使用 Array Container 存储,否则会使用 BitMap Container 存储。

读写性能方面:

  • BitMap Container 由于只涉及到位运算且可以根据下标直接寻址,所以时间复杂度为 O(1)。
  • Array Container 都需要通过二分查找才能确定元素的位置,所以时间复杂度为 O(logN)。

在空间占用上,BitMap Container 只占用 8KB,而 Array Container 和基数有关。

基础组件应用

  1. Redis 中提供了 SETBIT、GETBIT、BITCOUNT、BITTOP 四个常用命令来处理二进制位数组。

  2. ES 中优化 Filter 查询,由于 Filter 查询不涉及评分操作,只处理文档是否匹配查询条件,所以查询可以被缓存。Lucene 采用的就是 Roaring BitMap。

  3. ClickHouse 中有 bloom_filter 类型的 skipping indexs 用来过滤数据。

扩展阅读

从ClickHouse到ByteHouse:广告业务中的人群预估实践-火山引擎

广告案例|10亿数据、查询<10s,论基于OLAP搭建广告系统的正确姿势 - 掘金

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/896244.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Postgresql+Postgis安装教程

Windows 下载地址 Postgresql&#xff1a;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads Postgis&#xff1a;https://winnie.postgis.net/download/windows/ 我这里安装Postgresql13&#xff0c;所以对应Postgis也选择pg13版本 首先安装Postgresql…

kali 换源

kali 换源 1.使用root权限打开 vim /etc/apt/sources.list # See https://www.kali.org/docs/general-use/kali-linux-sources-list-repositories/ #deb http://http.kali.org/kali kali-rolling main contrib non-free non-free-firmware# Additional line for source pack…

标签(1)(详解)

目录 标签 部分标签 标签之标题 标题介绍与应用 VSCode插件 标签之段落 换行 水平线 标签之图片 网站中最多的元素 图片路径详解 绝对路径 相对路径 网络路径 标签之超文本链接 超链接描述 超链接属性 超链接表现 标签之文本 常用文本标签 标签 部分标签 …

Linux权限系列--给普通用户添加某个命令的sudo权限

原文网址&#xff1a;Linux权限系列--给普通用户添加某个命令的sudo权限_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍Linux系统如何给普通用户添加某个命令的sudo权限。 使用场景 普通开发者可能需要sudo的命令&#xff1a; apt-get&#xff08;经常要安装软件&#x…

《Zookeeper》源码分析(十四)之 投票是如何发送与接收的

目录 MessengerWorkerSenderWorkerReceiver第5步&#xff1a;检验选票的epoch和version第6步&#xff1a;处理投票 Messenger Messenger管理接收到的消息以及待发送的消息&#xff0c;其源码如下&#xff1a; 它的源码比较简单&#xff0c;接下来着重介绍它维护的两个线程&a…

【仿写tomcat】七、项目结构优化以及代码开源

仿写tomcat 项目结构开源地址 项目结构 到目前为止&#xff0c;博主的仿写tomcat就告一段落了&#xff0c;后续有时间了还会继续补充功能&#xff0c;现在的项目结构如下。 在保证功能的前提下作出的改动有&#xff1a; 将各个类中的参数统一成了Config类&#xff0c;通过对…

Java课题笔记~ Ajax

1.1 概述 AJAX (Asynchronous JavaScript And XML)&#xff1a;异步的 JavaScript 和 XML。 我们先来说概念中的 JavaScript 和 XML&#xff0c;JavaScript 表明该技术和前端相关&#xff1b;XML 是指以此进行数据交换。 1.1.1 作用 AJAX 作用有以下两方面&#xff1a; 与服…

百度又开源一款压测工具,可模拟几十亿的并发场景,太强悍了

dperf 是百度开源的一款基于 DPDK 的 100Gbps 网络性能和负载测试软件&#xff0c;能够每秒建立千万级的 HTTP 连接、亿级别的并发请求和数百 Gbps 的吞吐量。 优点 性能强大&#xff1a; 基于 DPDK&#xff0c;使用一台普通 x86 服务器就可以产生巨大的流量&#xff1a;千万…

[oeasy]python0085_[趣味拓展]字体样式_下划线_中划线_闪动效果_反相_取消效果

字体样式 回忆上次内容 \033 xm 可以改变字体样式 0m - 10m 之间设置的 都是字体效果 0m 复原1m 变亮2m 变暗 从3m到10m 又是什么效果 呢&#xff1f;&#xff1f; 真的可以 让文字 blink闪烁吗&#xff1f;&#x1f441; 3m 3m 实现斜体字的效果 4m 4m 对应着下划线 控…

==和equals方法之间的区别,hashcode的理解,String拼接,Spring拆分

==和equals方法之间的区别 字符串有字符串常量池的概念,本身就推荐使用String s="字符串", 这种形式来创建字符串对象, 而不是通过new关键字的方式, 因为可以把字符串缓存在字符串常量池中,方便下次使用,不用遇到new就在堆上开辟一块新的空间 有一对双胞胎姐妹,晓苑…

LabVIEW开发感应电机自动测试台

LabVIEW开发感应电机自动测试台 设计开发先进的电机测试台&#xff0c;能够测试额定功率为0-15hp的单相和三相感应电动机。系统能够测量感应电动机的不同参数&#xff0c;例如电压&#xff0c;电流&#xff0c;有功功率&#xff0c;无功功率&#xff0c;视在功率&#xff0c;功…

有限状态机--实现cp的功能

有限状态机–实现cp的功能 执行的任务 上图是我们想实现的任务&#xff0c;对于A机来说&#xff0c;从fd1读取内容写到fd2&#xff0c;B机要做的是从fd2读取内容写到fd1中。 画出A机的状态。 代码示例 fsm.c #include <stdio.h> #include <stdlib.h> #include …

ShowMeBug CEO李亚飞受邀参加深圳青年创新创业系列沙龙电子信息专场

7月13日下午&#xff0c;由深圳市科技交流服务中心&#xff08;深圳市科技专家委员会办公室&#xff09;主办&#xff0c;深圳新一代产业园承办的“2023深圳青年创新创业系列沙龙——电子信息专场”活动举行。ShowMeBug CEO李亚飞受邀参加此次活动。 深圳市科学技术协会党组成员…

web JS高德地图标点、点聚合、自定义图标、自定义窗体信息、换肤等功能实现和高复用性组件封装教程

文章目录 前言一、点聚合是什么&#xff1f;二、开发前准备三、API示例1.引入高德地图2.创建地图实例3.添加标点4.删除标点5.删除所有标点&#xff08;覆盖物&#xff09;6.聚合点7.自定义聚合点样式8.清除聚合9.打开窗体信息 四、实战开发需求要求效果图如下&#xff1a;封装思…

nginx php-fpm安装配置

nginx php-fpm安装配置 nginx本身不能处理PHP&#xff0c;它只是个web服务器&#xff0c;当接收到请求后&#xff0c;如果是php请求&#xff0c;则发给php解释器处理&#xff0c;并把结果返回给客户端。 nginx一般是把请求发fastcgi管理进程处理&#xff0c;fascgi管理进程选…

基于YOLOX的输电线路异物检测算法研究及软件设计_有系统有文献,整体认知蛮好的

我国自改革开放以来&#xff0c;大力发展工业和经济&#xff0c;对电能同样有着巨大的需求&#xff0c;所需求的电能不仅需要保证其数量&#xff0c;还要保障其质量&#xff0c;因此对整个电力系统安全稳定的运行也提出了更高的要求&#xff0c;电力系统发生故障要实时检测并及…

从零做软件开发项目系列之一综论软件项目开发

1 引言 有一个三个泥瓦匠的故事。 三个泥瓦匠在砌墙&#xff0c;一个人走过来&#xff0c;问他们在干什么。   第一个泥瓦匠没好气地说&#xff0c;你没看见吗&#xff1f;我在辛苦地砌墙呢。   第二个回答&#xff0c;我们正在建一座高楼。   第三个则洋溢着喜悦说&…

Vue2子组件修改父组件的方法

Vuex Vuex 是状态管理器&#xff0c;集中式存储管理所有组件的状态。 Vuex速成整理_AYBAIWAN的博客-CSDN博客https://blog.csdn.net/aybaiwan/article/details/131442547?spm1001.2014.3001.5501vuex中this.$store.commit和this.$store.dispatch的用法_老电影故事的博客-CSD…

第八届XCTF联赛首场国际外卡赛——WACON2023即将开启!

由国际战队SuperGuesser操刀命题 第八届XCTF首场国际外卡赛 WACON2023即将开启 线上资格赛前6名队伍 将晋级WACON2023总决赛 飞往韩国首尔 与全球顶尖白帽黑客一决高下 总决赛冠军队伍将获得&#xff1a; 3千万韩元&#xff08;折合人民币16万&#xff09;高额奖金 &第八…

Java IO流(一)IO基础

概述 IO流本质 I/O表示Input/Output,即数据传输过程中的输入/输出,并且输入和输出都是相对于内存来讲Java IO(输入/输出)流是Java用于处理数据读取和写入的关键组件常见的I|O介质包括 文件(输入|输出)网络(输入|输出)键盘(输出)显示器(输出)使用场景 文件拷贝&#xff08;File&…