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

文章目录

  • 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 位)的大小即可表示:

C++STL剖析(十)—— 位图(bitset)_第1张图片

那么对于 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] 进行 或运算 得出结果:

C++STL剖析(十)—— 位图(bitset)_第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] 进行 与运算 得出结果即可。

C++STL剖析(十)—— 位图(bitset)_第3张图片

注意:因为在设置指定位的时候,数字 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] 进行 与运算 得出结果即可。

C++STL剖析(十)—— 位图(bitset)_第4张图片

代码示例

// 获取指定位的状态
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; //打印总共打印的位的个数
}

然后我们测试一组数据

C++STL剖析(十)—— 位图(bitset)_第5张图片

测试第二组数据

C++STL剖析(十)—— 位图(bitset)_第6张图片

4. 总结

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

C++STL剖析(十)—— 位图(bitset)_第7张图片

以及各种常用接口

C++STL剖析(十)—— 位图(bitset)_第8张图片

主要的功能如下:

C++STL剖析(十)—— 位图(bitset)_第9张图片

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

  • 快速查找某个数据是否在一个集合中
  • 排序 + 去重
  • 求两个集合的交集、并集等
  • 操作系统中磁盘块标记

你可能感兴趣的:(「C++深入浅出」,c++,面向对象,STL)