C++STL详解(十一)– 位图(bitset)

位图的介绍

位图的引入

有一道面试题:

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

对于这道题,我们有两个思路:
内存内查找: 面对40亿个无符号整数,我们可以使用搜索树和哈希表,时间复杂度也就为O(1),因为搜索树不仅存储数据,还要存储颜色,parent,child指针等,哈希表还要存储迭代器,size等内置成员,进而导致内存存不下.

文件内查找:排序 + 二分查找,时间复杂度为0(1),但是数据太大,只能放在文件上,但是磁盘运行效率太低,不好支持二分查找).

综合以上情况,我们可以采取位图解决:
位图不像搜索树和哈希表那样需要存储数据,数据是否在所给数据中,只有两种状态,在或者不在,那么可以使用一个二进制比特位来代表数据是否存在,二进制比特位为1,代表存在,为0,代表不存在,并且使用直接定址法来确定数据映射位置.
例如以下图示:
在这里插入图片描述
位图的大小判断:
在本题中,40亿的无符号整型的范围为:0–4294967295,在开辟位图空间时,我们不是根据数据的个数在位图上映射的,而是根据数据的大小映射在位图上.所以,我们要开2^32-1的比特位大小的空间,让所有无符号整型数据都能映射在位图上.

那么2^32-1个比特位要占用多少空间呢?

图示如下:
在这里插入图片描述

位图的概念

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

位图的应用

1 : 快速查找某个数据打是否在一个集合中.

2: 排序 + 去重 . ( 根据位图性质,哈希函数映射原理)

3: 求两个集合的交集,并集等.

4: 操作系统磁块标记.

位图的使用

位图的定义

方式一: 构造一个8位的位图,所有位默认初始化为0.

bitset<8> bs; //00000000

方式二: 构造一个8位的位图,使用string类型对象初始化.

bitset<8> bs( string("1111" ))  // 00001111

方式三: 构造一个8位的位图,使用字符串"1111"初始化.

bit<8> bs("1111");  //00001111

位图的成员函数

成员函数 作用
set 设置指定位或所有位(状态设为1 )
reset 清空指定位或所有位(状态设为0)
test 获取指定位的状态
flip 反转指定位或者所有位
count 获取被设置位的个数
size 获取位图中可以容纳状态位的个数
any 查看位图所有状态位中是否有1
none 查看位图中状态位是否都为空
all 查看位图中状态位是否都为1
#include <bitset>
int main()
{
	bitset<8> bs("1110");
	
	bs.set(0);                 //设置指定位

	cout << bs << endl;        //00001111

	bs.reset(0);               //清空指定位

	cout << bs << endl;        //00001110

	bs.flip(0);               //反转指定位

	cout << bs << endl;       //00001111

	cout << bs.none() << endl;  //0 

	cout << bs.any() << endl;  //1

	bs.set();                  //将位图所有位设置为1

	cout << bs.all() << endl;       //1
}

注意:
flip,set,reset等成员函数如果未设置指定位时,则默认作用于位图中的全部数据
如果设置指定位,则作用于指定位.

位图运算符的使用

位图中针对运算符进行了重载,我们可以直接在位图中使用.

#include <bitset>
int main()
{     
	bitset<8> bs;      //00000000
	
	//输入运算符
	cin >>  bs;        //1111
    //输出运算符
	cout << bs << endl; //00001111         
   
    bitset<8> bs1("1110");
    
    bitset<8> bs2("1100");
    
    //位运算符
    cout << ( bs1 & bs2) << endl; // 0000 1100
     
    cout << ( bs1 | bs2 ) << endl; // 0000 1110

    cout <<  bs1 ^ bs2 ) << endl;  // 0000 0010
    
    //[]运算符
    bs1[0] = 1;
     
   cout << bs1 << endl;          //0000 1111;

    return 0;
}

位图的模拟实现

成员函数

构造函数

我们开辟内存时,一般是以char类型(1个字节)开辟的,如果有N个数据,那么就要需要映射到N个比特位.此时计算时开辟空间时有两种情况:
如果 N / 8整除,那么我们直接根据结果开辟字节空间.
如果N / 8 不被整除,那么剩下的数据就没有比特映射了.

综合以上两种情况:
我们采用不过整没整除,我们都比计算值多开辟一个字节空间.

 bit_set()
		{
			_bit.resize(N / 8 + 1);
		}

set reset test

ret成员函数作用主要是将x映射的状态位标为1,

其主要有三个步骤:
(1): 计算数据x在第i个char类型大小的字节空间内.

(2): 计算数据x在第i个char类型空间的第j个位中.

(3); 将1左移j位与第i个char类型进行或等运算.
示图如下:
在这里插入图片描述

reset成员函数的作用是将x的映射的状态位标为0

其主要有三个步骤:
(1): 计算数据x在第i个char类型大小的字节空间内.

(2): 计算数据x在第i个char类型空间的第j个位中.

(3): 将1左移j位取反后再与第i个类型进行与等运算.
图示如下:
在这里插入图片描述

test成员函数的作用是检测x映射的状态位的状态

如果第j位的状态为 0, 那么经过与运算的结果就为0,转换为bool就表示false.

如果第j位的状态位 1, 那么经过与运算的结果为1,转换为bool表示true.

在这里插入图片描述

代码如下:

void set(size_t x )
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bit[i] &= ~(1 << j);
		}

flip,size,count

flip成员函数用于反转比特位.

filp成员函数步骤如下:
1: 计算该位位于第i个char类型的第j个比特位.

2: 将1左移j位后再与第i个char类型异或运算.
在这里插入图片描述

void flip ( size_t x )
{
   size_t i =  x / 8;
   size_t j =  x % 8;
   _bit[i] ^= ( 1 << j );
}

size成员函数用于获取位图中可以容纳的位的个数

直接返回非类型模板参数就代表了数据的个数,也就代表了位图的大小.

size_t size()
{
   return N;
}

count成员用于获取被设置位的个数

我们知道,获取位图中被设置的位的个数,也就是统计中位图中状态位为1的个数.
步骤如下:
1: 遍历位图,取类型为char大小的比特位n和n-1进行与运算进而得到新的n.(每次计算都使位图状态位为1的个数-1.)

2: 判断n是否为0,如果不为0则继续循环(循环的次数,就代表该char类型比特位为1的个数)

3: 当位图遍历完,每个char类型的比特位循环的总次数就代表了该位图状态为1的总个数.

size_t count()
		{
			size_t count = 0;

			for ( auto e: _bits)
			{
				char x = e;
				while ( x )
				{
					 x = x & (x - 1);
					count++;
				}
			}
			return count;
		}
			
			

none,any,all

none查看位图中是否状态位都为空

在位图中以char类型大小的比特位进行遍历,查看是否为0.

bool none()
{
    for ( auto e : _bit )
    {
         if( e != 0 )
         {
            return false;}
    return true;
}

any函数查看位图中的状态位是否存在1.

bool any()
{
   return !none();
}

这里是引用all函数查看位图中的状态位是否都为1.

由于我们在构造位图时始终多开了一个char类型大小(8比特位)的空间,且这最后8比特位中只N%8个比特位是有效的,剩下的空间是没有数据映射的,是无效的,此时有两种情况:
步骤如下:
1:检查数据N所占实际的char个数空间大小,即N / 8.

2: 检查最后一个char中有效的比特位是否位1.

在这里插入图片描述

bool all()
		{
			size_t size = _bits.size();

			for (size_t i = 0; i < N / 8; i++)
			{
				if (~_bits[i] != 0)//取反应该为0,否则取反之前不全为1,返回false
					return false;
			}
	
			for (size_t j = 0; j < N % 8; j++) 	//再检查最后一个char的前 N%8 个位
			{
				if ((_bits[ size- 1 ] & (1 << j)) == 0)//最后一个char有多少j个1就循环j次.
					return false;
			}
			return true;
		}

位图应用题扩展

题目一:

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

我们知道1个位图可以表示两种状态,那么两个位图可以表示四种状态,针对该题,我们可以设计以下三种状态:

在这里插入图片描述
为了减少哈希映射位置的计算,我们可以采取复用位图的方式,设计出包含两个位图的类.

其中成员函数set的作用及步骤如下:
如果x映射位置的状态位为00, 则调用位图2的set,进而实现状态位为01.

如果x映射位置的状态位为01,则调用位图1的set,位图2的reset,进而实现总状态位为10.

图示如下:
在这里插入图片描述
代码如下:

	template < size_t N >
	class twobit_set
	{
	public:
		void set(size_t x)                            
		{
			bool inset1 = _bs1.test(x);
			bool inset2 = _bs2.test(x);
			if ( inset1 == false && inset2 == false )  //如果状态为 00
			{
				_bs2.set(x);                           //设计为01;
			}
			else if (inset1 == false && inset2 == true) //如果状态为 01
			{
				_bs1.set(x);                               
				_bs2.reset(x);                         //设计为10
			}
		}
		void print_once_num()                       //遍历比特位,将数据在位图的状态位为01的数据打印.
		{
			for ( size_t i = 0; i < N; ++i )
			{
				if (_bs1.test(i) == false && _bs2.test(i) == true)
				{
					cout << i << " ";
				}
			}
	    }
		
	private:
		bit_set<N> _bs1;
		bit_set<N> _bs2;
	};

代码如下:

题目二:

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

题解如下:
在这里插入图片描述

template < size_t N >
	class twobit_set
	{
	public:
		void set(size_t x)                            // 00 变 01  
		{
			bool inset1 = _bs1.test(x);
			bool inset2 = _bs2.test(x);
			if ( inset1 == false && inset2 == false )
			{
				_bs2.set(x);
			}
			else if (inset1 == false && inset2 == true) // 01 变 10  
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			else if (inset1 == true && inset2 == false)       // 10 变 11;
			{
				_bs1.set(x);
				_bs2.set(x);
			}
		}
		void print_once_num()    
		{
			for ( size_t i = 0; i < N; ++i )
			{
				if (_bs1.test(i) == true && _bs2.test(i) == true)    //将状态为11的数据打印.
				{
					cout << i << " ";
				}
			}
	    }
		
	private:
		bit_set<N> _bs1;
		bit_set<N> _bs2;
	};

位图模拟实现代码

using namespace std;
namespace myBit
{
	template< size_t N >
	class bit_set
	{
	public:
		bit_set()
		{
			_bits.resize(N / 8 + 1, 0);
		}
		void flip(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bits[i] ^= (1 << j);
		}
		void set(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bits[i] |= (1 << j);
		}

		void reset(size_t x)
		{
			size_t i = x / 8;      //计算在位图的第几个char.

			size_t j = x % 8;      //计算在char中的第几个位置.

			_bits[i] &= ~(1 << j);  //只改变第j位的比特位,其他位没有改变.
		}
		//统计set中1的个数
		
		size_t count()
		{
			int count = 0;
		for (size_t i = 0; i <=  N / 8; ++i)
		{
			char  n = _bits[i];
			while (n)
			{
				n = n & (n - 1);
				count++;
			}
		}
		return count;
	}
			
		//	for (auto e : _bits)
		//	{
		//	    char  n = e;             //
		//		while (n)
		//		{
		//			n = n & (n - 1);
		//			count++;
		//		}
		//	}
		//	return count;
		//}
		size_t size()
		{
			return N;
		}
		bool test(size_t x)
		{
			size_t i = x / 8;

			size_t j = x % 8;

			return _bits[i] & (1 << j);

		}
		bool all()
		{
			size_t size = N / 8;

			for (size_t i = 0; i < N / 8; i++)
			{
				if (~_bits[i] != 0)//取反应该为0,否则取反之前不全为1,返回false
					return false;
			}
			//再检查最后一个char的前 N%8 个位
			for (size_t j = 0; j < N % 8; j++)
			{
				if ((_bits[ N - 1 ] & (1 << j)) == 0)//和test的原理一致
					return false;
			}
			return true;
		}
		bool none()
		{
			for (auto e : _bits)
			{
				if (e != 0)
				{
					return false;
				}
			}
			return true;
		}
		bool any()
		{
			return !none();
		}
		
	private:
		vector<char> _bits;
	
	};

	template < size_t N >
	class twobit_set
	{
	public:
		void set(size_t x)                            // 1:00 变 01 2: 01 变 10  
		{
			bool inset1 = _bs1.test(x);
			bool inset2 = _bs2.test(x);
			if ( inset1 == false && inset2 == false )
			{
				_bs2.set(x);
			}
			else if (inset1 == false && inset2 == true)
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			else if (inset1 == true && inset2 == false)       // 10 变 11;
			{
				_bs1.set(x);
				_bs2.set(x);
			}
		}
		void print_once_num()
		{
			for ( size_t i = 0; i < N; ++i )
			{
				if (_bs1.test(i) == false && _bs2.test(i) == true)
				{
					cout << i << " ";
				}
			}
	    }
		
	private:
		bit_set<N> _bs1;
		bit_set<N> _bs2;
	};
	void test_set()                                 //测试代码
	{

		bit_set<9> bit_set;
		bit_set.set(0);
		bit_set.set(1);
		bit_set.set(2);

		bit_set.set(3);
		bit_set.set(4);
		bit_set.set(5);

		bit_set.set(6);
		bit_set.set(7);

		bit_set.set(8);

		bit_set.flip(0);

		cout << bit_set.none() << endl;
		cout << bit_set.count() << endl;
	}


}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>