位图(bitset)--明确场景极致性能

前言

        位图实际上就是运用哈希的思想将内存极致的利用,尽可能地提升效率。哈希的关键就是建立映射关系,提供高效的访问,下面我们来看到面试题。

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

       首先想到的就是将这40亿个数进行排序,然后进行二分查找,快速排序时间复杂度为O(NlogN),二分查找一次为O(logN)。这种方法在时间复杂度层面还是不错的,但空间复杂度相对来说是比较高的,下面我们来计算下上述算法需要多少内存。

        40 0000 0000 *4 /1024 /1024 /1024 大约等于15 G,我们普通计算机如今只有16的内存,硬盘大小可扩充,但程序是在内存上运行的,也就是说运行上述程序可能使我们的内存爆满,如果内存不够就会崩溃!!。

        我们可以先思考如下分割后的简单小问题。

         假如是如下情况 int a[] = { 5,6,3,8,4,9 };判断一个数是否在 这6个数中,我们可以开辟一个大小为10的bool数组,如下图。数组初始化全为假即0

        遍历数组,将该数对应位置设置为true即1,这样判断某个数在不在数组中,就可以直接访问数组对应下标位置即可,每次查询的效率为O(1),建立了一种哈希映射。

        

        我们在回过头看上面的例子,对于每个数他只有两种情况,在与不在,可以用0与1表示,而bool类型为1字节,但一个比特位就可以表示0与1两种状态,相当于每个bool类型我们浪费了7个比特位。

        于是我们便引入位图的概念,用一个比特位表示两者状态,这样就充分利用每一块空间,此时再处理上述的面试题空间大概占用 4,294,967,295/8 /1024 /1024 /1024 大约等于0.5 G!查询的效率也达到了恐怖的O(1),十分的高效。

        接下来我们来实现位图。

位图实现

        在C++中一般个人使用最小的内存大小为字节,而位图则是对比特位进行操作,我们不能直接的开辟60位出来,但我们可以间接的开辟出比特位。

        假如我们需要60位,我们开辟两个无符号整型数组,一个无符号整型大小为32位,两个位64位满足我们的需求,接着就是找打第60位。

        

        面对数字60,我们可以通过下面除与模来找到他的具体位置

int i = x / 32;
int j = x % 32;

        例如60/32=1,那么60在bs[1]数中,60%32=28,那么60就在bs[1]下标28处,由此我们便可以找到每个数对应的比特位。相当于我们把数组中每一个无符号数看为一个小数组,由32个比特位组成。

        经过上述分析我们就可以写出如下框架

#pragma once
#include<iostream>
#include<vector>
using namespace std;


template<size_t N>
class bit_set
{
public:
	bit_set()
	{
		//这里加一解决出现31/32=0或者32/32=1不足的情况
		_bs.resize(N / 32 + 1);
	}
private:
	vector<size_t> _bs;
};

        其中我们采用了模板参数传递最大值是多少,然后 计算要开辟多大数组。(模板除了传递类型,还可以传递具体的参数)。

        接下来我们实现位图中几个重要的函数。

set

        set就是将数对应位置比特位设置为1

//将对应位设置为1
bit_set& set(size_t x)
{
	
}

        给我们一个数,如何将对应位置设置为1?这里就要用到位运算。 如下图

       

        于是我们便可以完成函数操作。

//将对应位设置为1
bit_set& set(size_t x)
{
	int i = x / 32;
	int j = x % 32;

	_bs[i] |= (1 << j);

	return *this;
}

        reset

         reset就是将数对应位置比特位设置为0.原理如下图。

        那么现在的问题就是如何得到第j位为0,其余为为1的数?通过观察我们可以发现只要将原来1左移j位的值取反即可得到目标值。

        于是我们便可以完成函数操作。在位运算时能加括号尽量加上括号,否则可能由于运算符优先级形成错误问题。

//将对应位设置为0
bit_set& reset(size_t x)
{
	int i = x / 32;
	int j = x % 32;

	_bs[i] &= (~(1 << j));

	return *this;
}

test

        test是用来检测当前数对应为是否为1的,当前位为1返回真否则返回假,原理如下

//当前位为1返回真否则返回假
bool test(size_t x) const
{
	int i = x / 32;
	int j = x % 32;

	return _bs[i] & (1 << j);
}

计数

        最后就是两个关于计数大小的函数,第一个返回当前支持多少位,第二个返回有多少位为1

size_t size()
{
	return N;
}


//返回1的个数
size_t count()
{
	size_t ret = 0;
	for (int i = 0; i < N; i++)
	{
		if (test(i))
			ret++;
	}
	return ret;
}

        位图的效率十分高,但主要适用于整数,对于一些其他的类型无法很好的映射。一些场景可以使用布隆过滤器,来达到位图的高效。

题目      

387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

          最后做个题目练习一下。

        这里开辟一个位图是不够的,我们要表示每个数的状态有0 1 多次三种,就可以开辟两个位图来处理。先遍历一遍处理位图,再遍历一遍寻找出现一次的数字

    class Solution {
    public:
        int firstUniqChar(string s)
        {
			bitset<26> bs1; // 0(0次) 1(1次)
			bitset<26> bs2;//  1(多次) 

			//先遍历一遍统计次数
			for (int i = 0; i < s.size(); i++)
			{
				char ch = s[i] - 'a';
				bool flag1 = bs1.test(ch);
				bool flag2 = bs2.test(ch);

				if (!flag1 && !flag2)// 0  0
				{
					bs1.set(ch);
				}
				else if (flag1 && !flag2)// 1 0
				{
					bs2.set(ch);
				}
				else// 多次出现 不处理
				{
					// 1 1
				}
			}

			for (int i = 0; i < s.size(); i++)
			{
				char ch = s[i] - 'a';
				bool flag1 = bs1.test(ch);
				bool flag2 = bs2.test(ch);

			    if (flag1 && !flag2)// 1 0
				{
					return i;
				}
			}

			return -1;
        }
    };

VS开辟大小细节问题

        我们自己实现的bitset大小可以开辟出无符号整型最大值,但STL模板实现的不可以如下图。

        

        STL实在栈区开辟的数组,而栈区通常只有8M左右,开辟这么大内存会栈溢出,所以下面错误是stackoverflow。而我们自己写的是在堆区申请内存,堆区足够大可以申请出来。

    自定义版本

         对于我们自己实现的bitset在堆区开辟可以通过打印地址来验证。   此时将类中vector权限放开,改为public,否则报错

void test2()
{
	bit_set<100> bs1;
	int* p = new int(4);
	int c = 0;

	cout <<"p :" << (void*)p << endl;
	cout << "bs1:" << (void*)&bs1._bs[0] << endl;
	cout << "c :" << (void*)&c << endl;

}

        通过上图可以发现bs1开辟数据存储的地方例栈区c比较远,但离堆区开辟的p近,由此便可证明我们实现的在堆区开辟。

        STL版本

void test2()
{
	bitset<100> bs2;
	int* p = new int(4);
	int c = 0;

	cout <<"p :" << (void*)p << endl;
	cout << "c :" << (void*)&c << endl;

	cout << "bs2:" << (void*)&bs2 << endl;

}

        上述打印结果bs2的地址离栈区c位置近,离堆区p位置远,可以证明bitset在栈区开辟空间么?答案是否定的!!我们只能证明变量bs2存在栈区,但bs2里的内存不一定存在栈区,可能在bs2内部有个指针,指向真正存储的内存。

        假如我们假设之前的bs1,如下图

        我们可以发现变量bs1的确存储在栈区,但其内部有指针指向其他内存,所以之前打印的是帧数数据的地址,(void*)&bs1._bs[0] ,而不是bs1的地址。

        对于自定义类型我们可以修改内部权限让我们访问真正内存的地址,但对于封装好的类,我们无法直接修改,只能通过其他手段验证。

        有三个方面可以证明。

        1.当我们开辟过大内存时,报错是栈溢出。

        2.当我们开辟不同大小时,变量大小在变化

        我们可以推测他底层与我们实现类似,20位要一个无符号整型,所以4个字节,100位要4个无符号整型,所以要16字节。

        3.看内存

        程序运行时在内存中查找bs1地址变化

        内存的结果与我们预测一样,从而经过上述充分分析,我们可以断定STL就是在栈区开辟空间。

        当然我们也有办法让他强制在堆区开辟内存,如下代码

void test3()
{
	bitset<UINT_MAX>* p = new bitset<UINT_MAX>;

}

        结果也是可以运行的。

总结

        优点               

1.访问效率高

 2.占用内存少

        缺点

1.只适用于整型,其他类型无法完美映射

代码

        头文件

#pragma once
#include<iostream>
#include<vector>
using namespace std;


template<size_t N>
class bit_set
{
public:
	bit_set()
	{
		//这里加一解决出现31/32=0或者32/32=1不足的情况
		_bs.resize(N / 32 + 1);
	}

	//将对应位设置为1
	bit_set& set(size_t x)
	{
		int i = x / 32;
		int j = x % 32;

		_bs[i] |= (1 << j);

		return *this;
	}

	//将对应位设置为0
	bit_set& reset(size_t x)
	{
		int i = x / 32;
		int j = x % 32;

		_bs[i] &= (~(1 << j));

		return *this;
	}

	//当前位为1返回真否则返回假
	bool test(size_t x) const
	{
		int i = x / 32;
		int j = x % 32;

		return _bs[i] & (1 << j);
	}

	size_t size()
	{
		return N;
	}


	//返回1的个数
	size_t count()
	{
		size_t ret = 0;
		for (int i = 0; i < N; i++)
		{
			if (test(i))
				ret++;
		}
		return ret;
	}


private:
	vector<size_t> _bs;
};

源文件
 

#include"Bit_Set.h"
#include<bitset>
void test1()
{
	bit_set<100> bs;

	int a[] = { 10,25,36,40,26 };#include"Bit_Set.h"
#include<bitset>
void test1()
{
	bit_set<100> bs;

	int a[] = { 10,25,36,40,26 };

	for (int e : a)
		bs.set(e);

	for (int i = 0; i < 100; i++)
	{
		if(bs.test(i))
			cout << i << " " << endl;
	}

}


int main()
{
	test1();

	int a[] = { 5,6,3,8,4,9 };
	
	return 0;
}

	for (int e : a)
		bs.set(e);

	for (int i = 0; i < 100; i++)
	{
		if(bs.test(i))
			cout << i << " " << endl;
	}

}


int main()
{
	test1();

	int a[] = { 5,6,3,8,4,9 };
	
	return 0;
}

        到这里就结束了,如果有错误欢迎在评论区指出。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值