C++STL剖析(十)—— 位图(bitset)

2023-05-16

文章目录

  • 1. 位图的介绍
  • 2. 位图的概念
  • 3. 位图的实现
    • 🍑 构造函数
    • 🍑 设置指定位
    • 🍑 清除指定位
    • 🍑 获取指定位的状态
    • 🍑 打印函数
  • 4. 总结

1. 位图的介绍

在介绍位图之前先来看一道面试题吧

给 40 亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这 40 亿个数中。

对于判断一个数是否在某一堆数中,主要有以下方法:

  • 将这一堆数插入到 unordered_set/set 容器中,然后调用 find 函数判断该数是否在这一堆数中。
  • 将这一堆数进行外排序,然后通过二分查找的方法判断该数是否在这一堆数中。

对于上面两种方法,第一种的时间复杂度是 O ( N l o g N ) O(NlogN) O(NlogN),第二种的时间复杂度是 O ( N ) O(N) O(N)

但是有一个问题,题目给的是 40 亿个无符号的整数,如果把这些数全部加载到内存当中,那么将会占用 16G 的空间,空间消耗是很大的。因此,上面这两种方法都是不可行的。

那么怎么来解决呢?很简单,我们可以用位图(bitset)来解决!

对于数据是否在给定的整形数据中,只需要判断在或者不在,刚好是两种状态,那么可以使用一个 二进制比特位 来代表数据是否存在的信息,如果二进制比特位为 1,代表存在;如果比特位为 0,代表不存在。

如下图所示,对于 arr 集合里面的数据,我们只需要用 3 个字节(24 个 bit 位)的大小即可表示:

在这里插入图片描述

那么对于 40 亿个无符号的整型数据来说,无符号整数总共有 2 32 2^{32} 232 个,因此记录这些数字就需要 2 32 − 1 2^{32-1} 2321 个比特位(比特位是从 0 开始的,所以要减 1),也就是大约 500M 的内存空间,内存消耗大大减少。

2. 位图的概念

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

3. 位图的实现

位图的定义需要一段连续的物理空间,所以可以拿 vector 来存储,另外,对于位图的存储,我们是不能控制 bit 位的,但是我们可以控制 字节,那么就可以把 char 或者 int 存在数组里面去。

假设数组里面存储的是 char,那么 1 个 char 就代表 8 个 bit 位。

位图的类如下:

// N个比特位的位图
template<size_t N>
class BitSet
{
public:
	// 构造函数
	BitSet();

	// 设置指定位(将x映射的位标记成1)
	void set(size_t x);

	// 清空指定位(将x映射的位标记成0)
	void reset(size_t x);

	// 获取指定位的状态
	bool test(size_t x);
	
	// 容纳的比特位的个数
	size_t size()
	
	//打印函数
	void Print()
private:
	vector<char> _bits; // vector数组里面存的是一个char类型
};

🍑 构造函数

在构造位图时,我们需要根据所给位数 N,创建一个 N 位的位图,并且将该位图中的所有位都初始化为 0。

但是一个 char 有 8 个比特位,因此 N 个位的位图就需要用到 N / 8char 类型,但是实际我们所需的 char 个数是 N/8+1,为什么呢?因为所给非类型模板参数 N 的值可能并不是 8 的整数倍。

举个例子,当 N 为 10 的时候,那么 10 / 8 = 1 相当于只开了 1 个 char 类型,那如果我去访问第 9 个 和 第 10 个 bit 位的话,不就越界了吗?

所以要 N/8+1,我知道有人肯定会担心浪费空间,我想说的是,当 N 是 8 的倍数时,可以被整除,那么此时也就才浪费 1 个 char 也就是 8 个 bit 位而已,所以无伤大雅。

代码示例

// 构造函数
BitSet()
{
	// +1保证了足够的比特位,最多浪费8个
	_bits.resize(N / 8 + 1, 0);
}

🍑 设置指定位

set 接口就是用来把 x 映射的位标记成 1,并且不能影响其它比特位,那么我怎么知道 x 映射的比特位在哪儿呢?

很简单,方法如下:

  • 先用 x / 8 = i 计算 x 应该存储在数组的第 i 个 char 对象中
  • 然后再用 x % 8 = j 计算 x 应该存储在第 i 个 char 中的第 j 个比特位上
  • 最后将第 i 个 char 对象的第 j 个比特位设置为 1 即可

前两步很简单,对于第三步,如何设置呢?

很简单,先将数字 1 左移 j 位后,再和第 i 个 char 对象也就是 _bits[i] 进行 或运算 即可

举个例子,我们现在要把数字 19 映射在位图上。

第一步,先用 19 / 8 = 2 得出要存在数组的第 2 个 char 对象中。

第二步,再用 19 % 8 = 3 得出要存在第 3 个比特位上

第三步,先将数字 1 左移 3 位得到:1 << 3 = 00001000 = 8,然后再和 bits[2] 进行 或运算 得出结果:

在这里插入图片描述

代码示例

// 设置指定位(将x映射的位标记成1)
void set(size_t x)
{
	// x映射的比特位在第几个char对象
	size_t i = x / 8;

	// x在char第几个比特位
	size_t j = x % 8;

	// 先左移,再进行或运算
	_bits[i] |= (1 << j);
}

🍑 清除指定位

reset 接口就是用来把 x 映射的位标记成 0,并且不能影响其它比特位。

很简单,方法如下:

  • 先用 x / 8 = i 计算 x 应该存储在数组的第 i 个 char 对象中
  • 然后再用 x % 8 = j 计算 x 应该存储在第 i 个 char 中的第 j 个比特位上
  • 最后将第 i 个 char 对象的第 j 个比特位设置为 0 即可

前两步很简单,对于第三步,如何设置呢?

很简单,先将数字 1 左移 j 位后,然后再对其进行 按位取反,最后再和第 i 个 char 对象也就是 _bits[i] 进行 与运算

我们还是举个例子,我们现在要把数字 19 从映射的位置上清除掉。

第一步,先用 19 / 8 = 2 得出要存在数组的第 2 个 char 对象中。

第二步,再用 19 % 8 = 3 得出要存在第 3 个比特位上

第三步,先将数字 1 左移 3 位得到:1 << 3 = 00001000 = 8,然后再把数字 8 进行按位取反得到:~8 = 11110111,然后再和 bits[2] 进行 与运算 得出结果即可。

在这里插入图片描述

注意:因为在设置指定位的时候,数字 19 已经被存储在 bits[2] 上了,所以 bits[2] 的比特位不是全为 0 哦!

代码示例

// 清空指定位(将x映射的位标记成0)
void reset(size_t x)
{
	// x映射的比特位在第几个char对象
	size_t i = x / 8;

	// x在char第几个比特位
	size_t j = x % 8;

	// 先左移,再按位取反,最后进行与运算
	_bits[i] &= (~(1 << j));
}

🍑 获取指定位的状态

test 用来获取位图中指定的位的状态,要么是 1,要么是 0。

很简单,方法如下:

  • 先用 x / 8 = i 计算 x 应该存储在数组的第 i 个 char 对象中
  • 然后再用 x % 8 = j 计算 x 应该存储在第 i 个 char 中的第 j 个比特位上
  • 最后判断第 i 个 char 对象的第 j 个比特位的状态
    • 如果是 0,说明该比特位没有被设置
    • 如果是非 0,说明该比特位被设置

前两步很简单,对于第三步,如何判断呢?

很简单,先将数字 1 左移 j 位后,再和第 i 个 char 对象也就是 _bits[i] 进行 与运算

我们还是举个例子,假设数字 19 已经被映射到位图上去了,我们现在要判断数字 19 在不在上面。

第一步,先用 19 / 8 = 2 得出要存在数组的第 2 个 char 对象中。

第二步,再用 19 % 8 = 3 得出要存在第 3 个比特位上

第三步,先将数字 1 左移 3 位得到:1 << 3 = 00001000 = 8,然后再和 bits[2] 进行 与运算 得出结果即可。

在这里插入图片描述

代码示例

// 获取指定位的状态
bool test(size_t x)
{
	// x映射的比特位在第几个char对象
	size_t i = x / 8;

	// x在char第几个比特位
	size_t j = x % 8;

	// 先左移,再进行与运算,最后直接返回与运算的结果即可
	return _bits[i] & (1 << j);
}

🍑 打印函数

最后可以实现一个打印函数 Print,用来检查我们上述代码的正确性。

对于 Print 函数的实现,也很简单,只需要遍历位图所包含的比特位进行打印即可。在打印位图的过程中可以顺便统计位图中位的个数 count,然后将 count 与我们传入的非类型模板参数 N 进行比较,可以判断位图大小是否是符合我们的预期。

代码示例

// 容纳的比特位的个数
size_t size()
{
	return N;
}

//打印函数
void Print()
{
	int count = 0;
	size_t n = _bits.size();

	// 先打印前n-1个数
	for (size_t i = 0; i < n - 1; i++)
	{
		for (size_t j = 0; j < 8; j++)
		{
			if (_bits[i] & (1 << j)) //该位被设置
				cout << "1";
			else //该位未被设置
				cout << "0";
			count++;
		}
	}

	// 再打印最后一个数的前(N%8)位
	for (size_t j = 0; j < N % 8; j++)
	{
		if (_bits[n - 1] & (1 << j)) //该位被设置
			cout << "1";
		else //该位未被设置
			cout << "0";
		count++;
	}
	cout << "\n" << count << endl; //打印总共打印的位的个数
}

然后我们测试一组数据

在这里插入图片描述

测试第二组数据

在这里插入图片描述

4. 总结

其实对于位图在 C++ 库里面是已经实现好了的:位图文档,它是 STL 的一个模板类,它的参数是整形的数值,使用位的方式和数组区别不大,相当于只能存一个位的数组。

在这里插入图片描述

以及各种常用接口

在这里插入图片描述

主要的功能如下:

在这里插入图片描述

最后,说几个位图的应用场景:

  • 快速查找某个数据是否在一个集合中
  • 排序 + 去重
  • 求两个集合的交集、并集等
  • 操作系统中磁盘块标记
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++STL剖析(十)—— 位图(bitset) 的相关文章

随机推荐

  • 组网雷达融合处理组件化设计与仿真

    人工智能技术与咨询 点击蓝色 关注我们 关键词 xff1a 组网雷达 点迹融合 航迹融合 组件化设计 仿真 摘要 数据融合处理是多雷达组网的核心 以典型防空雷达网为参考对象 xff0c 采用组件化设计方式 xff0c 将组网数据融合处理过程
  • 人工智能 知识图谱

    关于举办 2022年数字信息化培训项目系列 知识图谱Knowledge Graph构建与应用研修班线上课程的通知 各有关单位 一 培训目标 本次课程安排紧密结合理论与实践 xff0c 深入浅出 xff0c 循序渐进 从基本概念讲起 xff0
  • 深度学习(Deep Learning)

    知识关键点 1 人工智能 深度学习的发展历程 2 深度学习框架 3 神经网络训练方法 4 卷积神经网络 xff0c 卷积核 池化 通道 激活函数 5 循环神经网络 xff0c 长短时记忆 LSTM 门控循环单元 GRU 6 参数初始化方法
  • 基于深度学习的机器人目标识别和跟踪

    如今 xff0c 深度学习算法的发展越来越迅速 xff0c 并且在图像处理以及目标对象识别方面已经得到了较为显著的突破 xff0c 无论是对检测对象的类型判断 xff0c 亦或者对检测对象所处方位的检测 xff0c 深度学习算法都取得了远超
  • 零基础Linux版MySQL源码方式安装+配置+远程连接完整图解 无坑实录

    无论开发还是运维 xff0c 项目环境搞不定 xff0c 还真让你干不成活 xff0c MySQL在不同场景 不同平台下安装方式也不同 xff0c 本次主要分享centos7下MySQL源码rpm方式安装 xff0c 其它方式后续分享 xf
  • C++,友元,语法+示例,非常详细!!!!

    友元概念 友元的目的就是让一个函数或者类 访问另外一个类中的私有成员 友元的关键字为 friend 友元的几种实现 全局函数做 友元类做 友元成员函数做 友元重载函数做 友元 全局函数做 友元 include lt iostream gt
  • STL——STL简介、STL六大组件

    一 STL是什么 STL standard template library xff1a C 43 43 标准模板库 xff0c 是C 43 43 标准库的重要组成部分 xff0c 不仅是一个可复用的组件库 xff0c 还是一个包罗数据结构
  • 文件流指针和文件描述符

    1 文件流指针和文件描述符的产生 fopen函数打开文件成功后会返回文件流指针 open函数打开文件成功后返回的是文件描述符 他俩的相同点是通过文件流指针和文件描述符都可以对文件进行操作 2 fopen函数和open函数的介绍 fopen函
  • docker 操作

    查看容器 xff1a sudo docker ps a 删除容器 xff1a sudo docker rm NAMES 容器的名字 下载镜像 xff1a sudo docker pull rmus2022 server v1 2 0 查看镜
  • 树莓派32位系统烧录及连接

    目录 前言 一 烧录树莓派系统 1 格式化tf卡 2 烧录系统 二 连接树莓派 1 开启SSH 2 开启网络共享 3 下载Putty 三 开启图形化界面 非必须 最后 xff1a 前言 我在树莓派环境搭建的过程中 xff0c 看了几十篇博客
  • 鸢尾花Iris数据集进行SVM线性分类

    文章目录 一 学习任务二 学习内容1 鸢尾花数据集使用SVM线性分类1 1 SVM介绍1 2 LinearSVC xff08 C xff09 方式实现分类1 3 分类后的内容基础上添加上下边界 三 参考博客 一 学习任务 安装python3
  • intel realsense d435i相机标定中文文档

    intel realsense d435i相机标定中文文档 此文档参考了官方的英文文档 xff0c 原地址面向英特尔 实感 深度摄像头的 IMU 校准工具 intelrealsense com IMU概述 xff1a 惯性测量单元 imu
  • VScode-git提交 无法推送refs到远端

    在将代码同步到远端仓库时 xff0c 弹窗提醒 无法推送refs到远端 您可以试着运行 拉取 功能 xff0c 整合您的更改 但尝试后发现 拉取 功能也无法解决问题 xff0c 最后是因为文件过大原因 xff0c 在这里记录一下解决方法 x
  • VMware16虚拟机中安装OpenEuler详细教程指南

    文章目录 安装前提准备镜像创建虚拟机安装欧拉踩坑指南 x1f351 网络指南 安装前提 Windown 10VMware 16openEuler 20 03 LTS SP3 准备镜像 镜像地址 xff1a OpenEuler 直接在官网下载
  • C/C++排序算法(三)—— 冒泡排序和快速排序

    文章目录 前言1 冒泡排序 x1f351 基本思想 x1f351 图解冒泡 x1f351 动图演示 x1f351 代码实现 x1f351 代码优化 x1f351 特性总结 2 快速排序 x1f351 hoare 版本 x1f345 图解过程
  • C/C++排序算法(四)—— 归并排序和计数排序

    文章目录 前言1 归并排序 x1f351 基本思想 x1f351 算法图解 x1f345 分组 x1f345 归并 x1f345 比较 x1f351 动图演示 x1f351 代码实现 x1f351 非递归实现 x1f345 情况一 x1f3
  • C++深入浅出(九)—— 多态

    文章目录 1 多态的概念2 多态的定义及实现 x1f351 多态的构成条件 x1f351 虚函数 x1f351 虚函数的重写 x1f351 虚函数重写的两个例外 x1f351 C 43 43 11的override 和 final x1f3
  • C++STL剖析(八)—— unordered_set和unordered_multiset的概念和使用

    文章目录 前言1 unordered set的介绍和使用 x1f351 unordered set的构造 x1f351 unordered set的使用 x1f345 insert x1f345 find x1f345 erase x1f3
  • C++STL剖析(九)—— unordered_map和unordered_multimap的概念和使用

    文章目录 1 unordered map的介绍和使用 x1f351 unordered map的构造 x1f351 unordered map的使用 x1f345 insert x1f345 operator x1f345 find x1f
  • C++STL剖析(十)—— 位图(bitset)

    文章目录 1 位图的介绍2 位图的概念3 位图的实现 x1f351 构造函数 x1f351 设置指定位 x1f351 清除指定位 x1f351 获取指定位的状态 x1f351 打印函数 4 总结 1 位图的介绍 在介绍位图之前先来看一道面试