位图的实现和拓展
一:位图的介绍
①:需要位图的场景
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
要判断一个数是否在某一堆数中,我们可能会想到如下方法:
A:将这一堆数进行排序,然后通过二分查找的方法判断该数是否在这一堆数中。
B:将这一堆数插入到unordered_set容器中,然后调用find函数判断该数是否在这一堆数中。
单从方法上来看,这两种方法都是可以,而且效率也不错,第一种方法的时间复杂度是O (NlogN ) O(NlogN)O(NlogN),第二种方法的时间复杂度是O (N)
重点是,40亿个数,占用16G的空间,空间消耗是很大的,不可能用代码直接开辟出16g的空间!
所以,这时候,就需要位图了
②:位图的意义
在上述问题中,我们只需确定某个无符号整数是否存在,即只有两种可能的状态(存在或不存在)。因此,可以用一个二进制位来表示无符号整数的状态:1表示存在,0表示不存在。如图:
无符号整数总共有232个,因此记录这些数字就需要232个比特位,也就是512M的内存空间,内存消耗大大减少。
③:位图的概念及使用场景
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
二:库中的位图的使用方法
①:bitset的定义方式
// 构造一个16位的位图,所有位都初始化为0。
bitset<16> bs1; //0000000000000000//构造一个16位的位图,根据所给值初始化位图的前n位。
bitset<16> bs2(0xfa5); //0000111110100101//构造一个16位的位图,根据字符串中的0/1序列初始化位图的前n位。
bitset<16> bs3(string("10111001")); //0000000010111001
解释: bs2(0xfa5)
用十六进制数 0xfa5 初始化 bs2;0xfa5 的二进制形式是 111110100101(共 12 位)。
由于 bitset 是 16 位的,而 0xfa5 只有 12 位,因此 高位补 0,最终存储为:
0000111110100101(16 位)。
②:bieset的成员函数
#include <iostream>
#include <bitset>
using namespace std;int main()
{bitset<8> bs;bs.set(2); //设置第2位bs.set(4); //设置第4位cout << bs << endl; //00010100bs.flip(); //反转所有位cout << bs << endl; //11101011cout << bs.count() << endl; //6cout << bs.test(3) << endl; //1bs.reset(0); //清空第0位cout << bs << endl; //11101010bs.flip(7); //反转第7位cout << bs << endl; //01101010cout << bs.size() << endl; //8cout << bs.any() << endl; //1bs.reset(); //清空所有位cout << bs.none() << endl; //1bs.set(); //设置所有位cout << bs.all() << endl; //1return 0;
}
运行结果:
bitset还有各种运算符的使用.......不再介绍了
三:位图的模拟实现
前提须知:& 和 | 的规则:
虽然位图有这么多的函数,但是我们实现,只实现set、reset、tes,已经能让我们了解bitset了!
①:构造函数
template<size_t N>
class bitset
{//构造函数bitset(){_bits.resize(N/32 + 1, 0); // 初始化所有位为 0}private:vector<int> _bits;};
解释:N/32+1的意义
我们一般从题目中得到了整形的个数后,用bitset去开辟相应个数的位出来,所以先N/32;
假设现在有50个数,那我们应该开50个位出来,但是50/32只能得到1,所以直接50/32+1等于2,开辟两个整形出来,即64个位;避免小于32的数字在构造函数里面开辟了0个空间;
②:set函数->将第 x
位设为 1
void set(size_t x)
{assert(x <= N); // 检查 x 是否越界size_t i = x / 32; // 计算 x 在哪个 int 中size_t j = x % 32; // 计算 x 在该 int 中的比特位位置_bits[i] |= (1 << j); // 将第 j 位设为 1
}
解释:_bits[i] |= (1 << j)
旨在:通过位运算将第 j
位设为 1
,且不影响其他位。
假设通过前两步得知我们的x对应的位是vector中第一个整形中的第五个二进制位,所以_bits[1] |= (1 << 5) 的效果如图:
符合预期,把第5位 置为了1
这例子很简单,如果原本的vector的其他位也有位1的,这时候进行|=操作后,照样是不影响其他位的,因为|代表一个为1则为1,两个为0才是0,所以不影响!
③:reset函数->将第 x
位设为 0
void reset(size_t x)
{assert(x <= N);size_t i = x / 32;size_t j = x % 32;_bits[i] &= ~(1 << j); // 将第 j 位设为 0
}
解释:_bits[i] &= ~(1 << j)
通过位运算将第 j
位设为 1
,且不影响其他位。
假设通过前两步得知我们的x对应的位是vector中第一个整形中的第五个二进制位,所以_bits[1] &= ~(1 << 5) 的效果如图:
符合预期,把第5位 置为了0
这例子很简单,如果原本的vector的其他位也有位1的,这时候进行&=操作后,照样是不影响其他位的,因为&代表一个为0则为0,两个为1才是1,所以不影响!
④:test函数->检查第 x
位是否为 1
bool test(size_t x)
{assert(x <= N);size_t i = x / 32;size_t j = x % 32;return _bits[i] & (1 << j); // 返回第 j 位的值
}
解释:_bits[i] & (1 << j);
注意:这一步为何没有&= 而是 & ,因为该函数只是想看某一位为什么,不能改变该位!
返回值:若 _bits[i] 的第 j 位为 1,返回 true;否则返回 false。
Q:为什么能判断某一位是否为 1?
A:& 运算后,只有第 j 位可能非0(因为其他位都是 0)。
如果结果 ≠ 0 → 说明 _bits[i] 的第 j 位是 1(返回 true)。
如果结果 = 0 → 说明 _bits[i] 的第 j 位是 0(返回 false)。
四:位图总代码及测试
①:总代码
#pragma once
#include<assert.h>namespace bit
{template<size_t N>class bitset{public:bitset(){_bits.resize(N/32+1, 0);//cout << N << endl;}// 把x映射的位标记成1void set(size_t x){assert(x <= N);size_t i = x / 32;size_t j = x % 32;_bits[i] |= (1 << j);}// 把x映射的位标记成1void reset(size_t x){assert(x <= N);size_t i = x / 32;size_t j = x % 32;_bits[i] &= ~(1 << j);}bool test(size_t x){assert(x <= N);size_t i = x / 32;size_t j = x % 32;return _bits[i] & (1 << j);}private:vector<int> _bits;};
}
②:测试代码
void test_bitset(){bitset<100> bs1;bs1.set(50);bs1.set(30);bs1.set(90);for (size_t i = 0; i < 100; i++){if (bs1.test(i)){cout << i << "->" << "在" << endl;}else{cout << i << "->" << "不在" << endl;}}bs1.reset(90);bs1.set(91);cout << endl << endl;for (size_t i = 0; i < 100; i++){if (bs1.test(i)){cout << i << "->" << "在" << endl;}else{cout << i << "->" << "不在" << endl;}}
预期效果:100个位中 第一次打印:50 30 90 位值为1;第二次打印:50 30 91 为1;
运行效果:
符合预期!
五:位图相关题目
了解了biset的相关实现后,用库中的bitset做几道题目吧~
①:双位图找只出现一次的数字
-
场景:从数组中找出所有只出现一次的数字(类似“单身狗”问题)。
-
数组:int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
-
解决:用
two_bit_set
(双位图)记录每个数字的状态:-
10
:出现多次。 -
01
:出现一次。 -
00
:未出现。 -
遍历数组后,输出状态为
01
的数字。 -
two_bit_set
(双位图)的成员变量就是两个位图即可
-
代码:
#include<iostream>
#include <bitset>
using namespace std;// 双位图:独立类,组合两个bitset
template<size_t N>
class two_bit_set
{
public:void set(size_t x){if (!_bs1.test(x) && !_bs2.test(x)){_bs2.set(x); // 00 → 01}else if (!_bs1.test(x) && _bs2.test(x)){_bs1.set(x); // 01 → 10_bs2.reset(x);}// 10 → 10(无需处理)}bool test(size_t x){if (_bs1.test(x) == false&& _bs2.test(x) == true){return true;}return false;}private:bitset<N> _bs1; // 高位bitset<N> _bs2; // 低位};
void test_bitset2()
{int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };two_bit_set<100> bs;for (auto e : a){bs.set(e);}for (size_t i = 0; i < 100; i++){//cout << i << "->" << bs.test(i) << endl;if (bs.test(i)){cout << i << endl;}}
}int main()
{test_bitset2();return 0;
}
②:求两个数组的交集
-
场景:找出两个数组中共同存在的数字。
-
两个数组:int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 }; int a2[] = { 5,3,5,99,6,99,33,66};
-
解决:
-
用两个
bitset
分别标记两个数组的数字。 -
遍历所有数字,输出在两个位图中均为
1
的数字。
-
代码:
void test_bitset3()
{int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };int a2[] = { 5,3,5,99,6,99,33,66 };bitset<100> bs1;bitset<100> bs2;for (auto e : a1){bs1.set(e);}for (auto e : a2){bs2.set(e);}for (size_t i = 0; i < 100; i++){if (bs1.test(i) && bs2.test(i)){cout << i << endl;}}
}
int main()
{test_bitset3();return 0;
}
运行结果:
此时回到最开始的问题:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
思路:
1:用库中的位图开辟40亿个位出来
2:读取题目给的数据,把40亿个整形对应的位置为1
3:然后在看待查询的数对应的位是否为1即可
代码无法写出来,因为没有这个庞大的数据,但是可以了解一下40亿位如何开辟:
bitset<-1> bs2;bitset<UINT_MAX> bs3;bitset<0xffffffff> bs4;
解释:
1. bitset<-1> bs2
-
模板参数
size_t N
接受-1
时会发生隐式转换 -
-1
转换为size_t
类型会变成最大值(即2³²-1
)
2:bitset<UINT_MAX> bs3
标准定义:
-
UINT_MAX
是<climits>
中定义的无符号整数最大值 -
标准值:
4,294,967,295
(即2³²-1
)
3:bitset<0xffffffff> bs4
十六进制解析:
-
0xffffffff = 4,294,967,295
(即2³²-1
) -
完全等价于
bitset<UINT_MAX>
4:bitset<4294967296> bs1;
记得住数字 也可以这样
③:一些位图解题的思想
Q:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数?
A:如果内存不够,分批次读是不可靠的,因为有可能在不同的批次中都出现了一次,加起来就超过了题目要求;假设要题目数据总大小1g(1024),但我们只有512MB;此时我们先读整数范围前半部分的值 再读范围为后半部分的值,先读0~2^31 再读2^31~2^31-1的范围即可。
所以空间再小一点都可以,只是要将范围分细一点罢了