哈希的应用——位图

位图

题目思考

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

看到这个问题可能会想到这样的思路:

1. 遍历, 时间复杂度O(N)
2. 排序 + 二分查找
3. 利用哈希表或红黑树, 就是放到set或unordered_set里面进行查找.

上面这些方法有没有什么问题?

我们注意到它这里给了40亿个整数,而1G=1024MB=1024*1024KB=1024*1024*1024byte, 1G约等于10亿byte, 40亿个整数约等于16G.

上面那些方法最关键的问题是16G的数据可能都不能一次全部放到到内存中, 内存可能都不够用.
二分查找的话排序不是问题, 如果要排序那就是用归并排序了, 分开放到一个个的小文件里面, 进行归并, 但是关键是内存开不出这么大的连续重进, 不能支持下标访问, 无法用二分查找.
放到set或unordered_set里面查找也是一样, 内存可能不够, 哈希表或红黑树还有额外的消耗, 因为还要存一些其它的成员变量, 可以分开每次处理一小部分, 但这样效率就不太行了。

所以上面这些思路都不太合适,而且我们这里只是要判断在不在,其实没必要把它们全部存起来。


位图概念 

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

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

比如:

回到题目, 题目说的是40亿个不重复的无符号整数, 无符号整数的最大值是2^32-1,即4294967295, 所以需要2^32次方个比特位, 需要的内存就是上面的16G/32 = 0.5G, 0.5G内存就可以开辟出来了.

所以我们开这样一个数组. 每个元素的大小是1个比特位, 因为我们用1个比特位就可以来标识当前位置下标所对应的值存不存在, 所以它其实就是一个直接定址法的思想.

没有类型的大小是1个bit, 所以我们可以开成int类型或者char类型的数组, 类型无所谓, 只要能取到对应的比特位即可.这里0到31映射到第一个int, 32到63映射到第二个int, 以此类推.


实现位图 

位图结构与构造函数 

模板参数可以用一个非类型模板参数作为位图所表示的数据个数. 

内部的vector应该开辟多大的空间?

这里的N是需要表示的数据个数, 在位图中就是N个比特位, N/8*4就是对应的整型个数, 但是可能不是整除会有剩余, 所以还需要+1. 而且初始的时候要把vector里面全放成0.

set和reset接口实现 

位图中的两个核心操作是setreset:

set就是把x映射的那个位置的比特位设置成1,表示这个数存在, reset就是把它设置成0, 表示不存在.

我们现在开的是int数组, 里面是一个个的int(32bit), 所以我们首先要找到数据x映射到第几个int, 然后找数据x映射到这个int的哪一位.

设i是映射的那个int, j是int里的位数, i = x/32, j = x%32, 最终x = i*32 + j. 

我们找到了这个比特位, 如何把它设置成1或者0呢?

先来看set, 把x映射的比特位设置成1, 怎么做呢?

其实就是第j位设置为1, 其它位全为0, 假如j是3, 我们可以给这个位置按位或
这样改变这个位置的同时还没有影响其它位置,因为一个数或0还是它本身.
所以我们让x | (1<<j) 就可以.

那reset就是把x映射的比特位设置成0

假设j还是3, 给这个位置按位与1111 1111 1111 1011就可以.
所以我们只需让x & ~(1<<j)的结果按位与就行了, 这样x映射的比特位变成0, 其它位置也不受影响.

test接口实现

除了这两个还有一个比较核心的接口——test, 它是去判断某个值存不存在(它映射的位置是否被设置成了1)

让x映射的这个比特位 和 1<<j 进行按位与, 如果是0, 就表明不存在; 如果是1, 就存在.

 简单测试一下:

void TestBS()
{
	test::bitset<100> bs;
	bs.set(34);
	if (bs.test(34))
		cout << "34存在" << endl;
	else
		cout << "34不存在" << endl;

	bs.reset(34);
	if (bs.test(34))
		cout << "34存在" << endl;
	else
		cout << "34不存在" << endl;
}

 

执行set后:

 执行reset后:


回到最开始的那道题, 40亿个无符号整数, 我们的位图应该给多大?

开40亿个可以吗, 不可以.
要注意我们不能按个数去开, 而是要按照范围去开.
就算现在变成10亿个无符号整数, 我们也应该开4294967295(即2^32-1,无符号整型最大值)个, 因为我们不知道这10亿个整数的取值范围, 它可能就包含了最大值, 所以我们要确保不论它多大, 就可以映射到位图中一个确定的位置上.

test::bitset<-1> bs;

位图其实C++STL库里面有提供的现成的:


位图的应用(海量数据处理)

下面来看几个位图相关的题:

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

首先这里是100亿个整数, 我们还开0xFFFFFFFF这么多空间吗?
虽然有100亿个, 但它的范围还是不变的, 不会超过整型最大值, 只能说明有很多重复值. 那我们还是用位图来解决, 找出只出现一次的整数, 1个比特位只能找出是否存在, 2个比特位就可以判断出现的次数了:

我们只看两位, 00就是0次, 01就是一次, 10是1次以上, 不一定就是两次, 因为我们set的时候如果是10就可以不进行操作了, 因为找的是只出现一次的整数, 只要证明出现不是1次就行.
1.我们可以给上面实现的位图改造一下, 改造成每个位置占两个比特位的位图.
2. 也可以不改造, 我们还是用上面的位图, 但是我们开两个位图.

所以我们可以封装一个Twobitset: 

两个位图中映射位置的值:

如果是00, 就变成01;

如果是01, 就变成10;

如果是10, 已经超过1次了, 就可以不处理了,因为已经能判断出来出现不止1次.

template<size_t N>
class Twobitset
{
public:
	void set(size_t x)
	{
		if (!bs1.test(x) && !bs2.test(x))
			bs2.set(x);
		else if (!bs1.test(x) && bs2.test(x))
		{
			bs2.reset(x);
			bs1.set(x);
		}
		//其它的情况不需要再处理了, 因为已经能判断出来出现不止1次
	}

	void PrintOnce()
	{
		for (size_t i = 0; i < N; i++)
		{
			//打印只出现一次的
			if (!bs1.test(i) && bs2.test(i))
				cout << i << " ";
		}
	}
private:
	bitset<N> bs1;
	bitset<N> bs2;
};

测试:

void TestBS3()
{
	int a[] = { 1,4,7,9,44,88,1,4,88,99,78,5,7 ,7,7,7 };
	test::Twobitset<100> bs; //假设数据范围只有100
	for (auto e : a)
	{
		bs.set(e);
	}
	bs.PrintOnce();
}

 


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

把两个文件的数据分别映射到两个位图里面, 然后遍历其中一个文件依次取值, 如果一个值在两个文件里都存在, 就是交集.

void TestBS4()
{
	size_t N = 100; //假设数据范围是0~100
	int a[] = { 1,4,7,9,44,88,1,4,88,99,78,5,7 ,7,7,7 };
	int b[] = { 1,4,7,10,44,88,1,4,88,100,60,5,7 ,7,7,7 };

	test::bitset<100> bs1;
	test::bitset<100> bs2;

	for (auto e : a)
	{
		bs1.set(e);
	}

	for (auto e : b)
	{
		bs2.set(e);
	}

	for (size_t i = 0; i< N;i++)
	{
		if (bs1.test(i) && bs2.test(i))
			cout << i <<" ";
	}
}

 


3. 位图应用变形: 1个文件有100亿个int, 我们只有1G内存, 设计算法找到出现次数不超过2次的所有整数.

只需要把第一个题的双位图改一改即可, 最后打印出01和10即可:

template<size_t N>
class Twobitset
{
public:
	void set(size_t x)
	{
		if (!bs1.test(x) && !bs2.test(x))
			bs2.set(x);
		else if (!bs1.test(x) && bs2.test(x))
		{
			bs2.reset(x);
			bs1.set(x);
		}
		else if (bs1.test(x) && !bs2.test(x))
		{
			bs2.set(x);
		}
		//11就是出现3次及以上了
	}

	void PrintOnce()
	{
		for (size_t i = 0; i < N; i++)
		{
			if (!bs1.test(i) && bs2.test(i) || bs1.test(i) && !bs2.test(i))
				cout << i << " ";
		}
	}
private:
	bitset<N> bs1;
	bitset<N> bs2;
};
void TestBS5()
{
	int a[] = { 1,4,7,9,44,88,1,4,88,99,78,5,7 ,7,7,7 };
	test::Twobitset<100> bs;
	for (auto e : a)
	{
		bs.set(e);
	}
	bs.PrintOnce();
}


关于搜索的总结: 

1.暴力查找: 数据量大了, 效率就低.

2.排序 + 二分查找 

问题a: 排序有代价

问题b: 数组不方便增删

3. 搜索树: 引申出->ALV树和红黑树, 性能整体比较稳定, 插入不会有太大波动.

4. 哈希: 搜索比较快, 但是整体不稳定, 插入是有波动的, 某次的插入可能需要扩容, 扩容代价比较高

还有极端场景下某个桶的数量可能很高, 但可以改挂红黑树解决.

以上数据结构, 空间消耗很高.

 对于数量很大的数据的场景?
5、[整形]的是否存在及其扩展问题--位图及变形节省空间, 但是位图的局限是只能处理整型.

6、[其他类型]的存在问题呢?--布隆过滤器, 下面会介绍.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值