C++ 位图及其应用

前言

现实生活中,有很多场景是需要处理数据量很大的数据的,比如:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。

一看到这样的题,我们可能想到的就是

  1. 排序+二分查找
  2. 哈希表 / 红黑树

但是40亿明显是一个很大的数据量,不管哪个方法都不适合。

而这时,位图产生了。

在这里插入图片描述

一. 什么是位图

所谓位图,就是用每一位比特位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。因为比特位只有0 / 1两种状态,用来标识存在与否这种原子性的状态,再合适不过了。

第几个比特位其实类似数组的下标,但是存储的数据只是0 / 1。
比如
在这里插入图片描述
每一个方格代表一个比特位,初始为0。如果存储3,则在3位置的比特位写入1,存储7,则在7位置的比特位写入1。

所以前言的题目,就由最开始的,一个整型存储一个数,需要16G:40亿数据大约是4G,而每一个整数需要4个字节,也就是总共16G
但是如果我们使用位图,只需要一个比特位标记一个数是否存在,所以一共需要40亿个比特位,一个字节又有8个比特位,所以总共需要4GB / 8,也就是512MB
量级一下子小了不少

二. 位图的实现

虽然位图是以比特位为基本单元的,但是我们并不能直接创建比特位,不过我们可以使用char类型,其大小为1字节,8个比特位。
如果我们要存储59,那么其实是存储在59 / 8 =7,59 % 8 =3,第7个char的第3个比特位

1. 基本结构

因为我们需要随机存储,所以我们底层使用vector,内部存储char类型。同时可以使用非类型模板参数,由需求规定当前位图存储数据量的大小

代码如下:

template<size_t N>
class bitset
{
	bitset()
	{
		_bits.resize(N / 8 + 1, 0);
	}
private:
	vector<char>_bits;
};

模板参数的N,就是指定该位图存储数据大小的范围
但是因为基本类型是char,一个char有8个比特位,所以vector实际需要的char并不是N,而是N / 8+1。

2. 数据的标记

存储一个数据,我们需要将其对应的比特位置1
正如我们上述的逻辑,我们首先通过 / 8获取,要在第几个char做修改;再 %8获取修改其第几个比特位
最后我们使用或运算,就修改成功了。
因为我们要置1。
0和1,与1或运算都是1。
0和1,与0或运算都是其本身
我们只要通过左移(将1从低地址移到高地址),使得除修改位置外,其他比特位都是0,就可以只修改特定比特位
代码如下:

	//标记
	void set(size_t N)
	{
		size_t piece = N / 8;
		size_t bit = N % 8;

		_bits[piece] |= (1 << bit);
	}

3. 数据的清除

清除一个数据,我们需要将其对应的比特位置0
基本逻辑相同,也是先获取要修改的位置,使用与运算修改
0 / 1 ,与0与运算都是0
0 / 1, 与1与运算是其本身
所以我们需要将修改的比特位与0与运算,其他比特位与1与运算,只要将1左移再取反就可以了
代码如下:

	//去标记
	void reset(size_t N)
	{
		size_t piece = N / 8;
		size_t bit = N % 8;

		_bits[piece] &= (~(1 << bit));
	}

4. 数据的查找

数据的查找返回bool值,为非0就是true
大致逻辑相同,最后与1与运算即可
代码如下:

	//查找
	bool test(size_t N)
	{
		size_t piece = N / 8;
		size_t bit = N % 8;

		return _bits[piece] & (1 << bit);
	}

5. 测试

我们加一个打印的函数,如果当前位图某比特位为1,那么打印这个数

	//打印
	void Print()
	{
		for (size_t i = 0; i < N; i++)
		{
			if (test(i))
			{
				cout << i << " ";
			}
		}
		cout << endl;
	}

测试代码如下:

void test_bitset1()
{
	bitset<100> bs;
	bs.set(10);
	bs.set(11);
	bs.set(15);
	bs.set(25);
	bs.set(34);
	bs.set(7);

	bs.Print();

	bs.reset(7);
	bs.reset(34);

	bs.Print();
}

运行结果如下:
在这里插入图片描述

回到前言的问题,大致记录方式我们了解了。
但是需要注意的是,不管存储数据个数有多少,对于整型,我们都需要开整型最大值的空间,因为就算是只记录100个数据,这100个数据的大小,大的可能到42亿,小的可能到个位数。模板参数的N,是记录数据的大小范围,不是存储数据个数

可以如下设置

bitset<-1>bs;
bitset<0xFFFFFFFF>bs;

三. 位图的应用

  • 给定100亿个整数,设计算法找到只出现一次的整数

首先,整数的大小范围就是0~42亿,所以100亿个整数,肯定都很多重复数据。
其次,100亿个整数,并不影响我们给位图开的空间,我们依然需要开42亿字节的位图。
但是因为我们要找只出现一次的整数,需要记录一个数出现的次数

一个思路是:我们修改位图的结构,加一个计数的变量

但是这个思路并不合适,因为位图的使用本身就是用来处理海量数据的,因为其节省了很多空间。但是如果给每个比特位添加计数,那么空间使用的成本又上去了。显然两者是相违背的

第二个思路是:我们使用两个位图,通过同一位比特位,两个位图的状态标记,区分一个数出现的次数
没有出现就是00,出现一次是01,出现多次是10。
这样我们只需要在遍历的时候,找到第二个位图是1的数据,就是只出现一次的数据

实现代码如下:

template<size_t N>
class twoBitset
{
public:
	//数据的插入
	void set(size_t n)
	{
		if (_bs1.test(n) == false && _bs2.test(n)== false)
		{
			//第一次出现
			_bs2.set(n);
		}
		else if (_bs1.test(n) == false && _bs2.test(n) == true)
		{
			//第二次出现
			_bs1.set(n);
			_bs2.reset(n);
		}
	}

	//打印
	void Print()
	{
		for (size_t i = 0; i < N; i++)
		{
			if (_bs2.test(i) == true)
			{
				cout << i << " ";
			}
		}
		cout << endl;
	}


private:
	bitset<N>_bs1;
	bitset<N>_bs2;
};
  • 测试
    在这里插入图片描述

  • 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集

有两种方法

  • 第一种方法:

其中一个文件的值映射到位图中,然后读取另一个文件的值,去位图中查找在不在,在就是交集,不在就不是交集。
但是可能有重复数据,所以我们可以在查找成功,即找到交集的数值,将其位图的比特位置0,这样后续再有重复数据也不会查找成功

  • 第二种方法:

两个文件映射到两个位图,然后遍历位图。当两个位图的比特位都是1,代表是交集,反之则不是。

第一种方法适用于文件数据较小的情况,因为方法一查找的次数是根据数据个数改变
而第二种方法不管数据个数多少,都需要查找位图范围,即整型所表示数字范围。


  • 1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

这个简单,我们只需要用两个位图就好
00表示出现0次
01表示出现1次
10表示出现2次
11表示出现3次


位图总的应用大致如下

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

结束语

位图的优点:速度快,节省空间
缺点:只能映射整型,其他类型如:浮点数,string等等不能存储映射。

本篇内容到此就结束了,感谢你的阅读!

如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值