比特之绘:位图的二进制诗学
一、位图概念
就是用每一位比特位来存放某种状态,1或0
适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在
下面我用一个场景来进一步解释
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中
- 一:遍历,时间复杂度O(N),如果多次查询,需要多次遍历40亿个无符号整数,消耗大
- 二:排序O(N * logN),用二分O(logn)进行查找,40亿个无符号整数,在32位环境下,一个size_t 整数的大小是4字节(64位环境下,一个size_t 的大小是8字节),那么我们进行推算一下1G大约等于多少字节——>
1G == 1024MB (2^10KB) == 1024 * 1024KB (2^20KB) == 1024 * 1024 *1024Byte(2^30Byte)
——> 那么也就是1G大约是10亿字节(Byte)
- 那么40亿个size_t 整数,一个整数是四个字节,那么40亿个无符号整数就是160亿个字节(Byte),由于1G == 10亿字节,所以40亿个无符号整数约等于16G,要进行排序需要在内存才能进行排序,内存的空间不足以满足这里的16G的需求进行排序,所以方法二也不行
————> 位图 : 位图在这里的思想是就是
你数字是几他就找,第多少个数字个数的比特位 ,然后让这个位置上变成1或者0,表示这个数字存在或不存在,如果判断105是否在,那么他就会找到第105个比特位,然后通过位运算,让这个位置上变成1或者是0 ,因为 进行判断size_t 整数是在或者不在,刚好是两种状态,那么我们就可以使用二进制比特位0, 1来表示这两种状态,如果比特位是1,那么表示存在,如果比特位是0,那么表示不存在,将这40亿个不重复的无符号整数放到位图中表示状态之后,进行查询给出的无符号数就可以使用O(1)的时间复杂度快速的查询判断是否存在
二、位图的逻辑
我们通过存很多比特位,我们通过比特位上的零或一来判断该数字存不存在,由于没有大小为一的类型,所以我们只能存int的类型或者是char类型,这里选择存int类型 ,一个int是四个字节,一个字节是八个比特位 , 所以一个int是32个比特位 ,存一个int代表32个比特位 ,我们只需要将传过来的值除以32表示,看他是第几个32,然后再%上32,看他在该最后一个32内排到第几位,这样我就把那个数字的值转化为了第多少个比特位
一个整形占四个字节及32个比特位,所以一个整形可以表示32个数字存在或不存在
友友们 ,可能会好奇,为什么比特位是这样存的
这就要看,1在电脑中的二进制是怎样存的?这里就要分小端机和大端机 ,小端机是反着存的,
(为什么有小端机和大端机 ,这就类似于古代的候的百家争鸣一样,大家不是统一好的,都是各自有想法,然后各自去做,各自做,看谁做的更好而已)
取出int x=1的x的地址来查看它的内存
这是他的底层存储 ,但是我们的逻辑是这样的 ,00000000000000....001, 友友们 可能在这里想到,如果按他这样的底层存储逻辑,那我们一通过左移,那怎么移到,像我们逻辑存储的那样对应位置上去呢 ,实际上,1在内存中怎么存是,是不重要的,编译器会把它的移动修改到对应上我们逻辑中的移动 , 00 00 00 01,是大端机 ,01 00 00 00 是小端机 (vs)
一定要记住,左移右移不是改变方向方向,而是改变数据位置的高低 ,左移就是往高位移,右移就是往低位移
三、位图的实现
template<size_t N>
class bitset
{
public:bitset(){_a.resize(N / 32 + 1);}// x映射的那个标记成1void set(size_t x){size_t i = x / 32;size_t j = x % 32;_a[i] |= (1 << j);}// x映射的那个标记成0void reset(size_t x)//{size_t i = x / 32;size_t j = x % 32;_a[i] &= (~(1 << j));}bool test(size_t x)//找到该数字存不存在 -- 1或0{size_t i = x / 32;size_t j = x % 32;return _a[i] & (1 << j);}
private:vector<int> _a;
};
- 注意vector,他要开空间 ,因为我们只是定义了一个空的vector,他的size()是零,他的capacity()是零 ,我们需要用resize给他开存储数据的空间 ,就像定义一个数组,在后面要加上括号内的数字一样
- 那我们要那我们要给他开,多少空间呢?一个整形是32个比特位,我们要存多少个数据,我们只需要用那个数据,除以32,为了确保有足够,再加上一个1,这就是我们要开的空间
- 上面的问题是给40亿个不重复的无符号整数,我们不能说只开40亿个数 ,因为位图在这里的逻辑是它的数字是多少,假如是100,我就找到这个,第100个比特位上,然后让他等于1,这里有40亿个不重复的数字,有些数字可能很大,可能超过40亿,所以说我们要开一个范围 ,而size_t 的范围是无符号数的范围是42亿9千万,无符号数在32位下的大小是4个自己,那么42亿9千万个无符号数,当我们使用位图去开空间的时候,就要开42亿9千万个比特位,那么就是42亿9千万比特位就是大约5亿字节,1G大约是10亿字节,那么也就是大约0.5G,也就是500MB,可见使用位图还是很节省空间的
- 如何对位图开42亿9千万比特位的空间呢
way 1:
bit::bitset<-1> bs1;//负一传给无符号,他的二进制就是全部都是1,此时就是最大值了
way2:
bit::bitset<UINT32_MAX> bs2;
- void set(size_t x) 将映射的那个标记成1
- void reset(size_t x) 将映射的那个标记成0
- test(size_t x) 找到该数字存不存在 ——> 1或0
test一下:
#include <iostream>
using namespace std;#include "BitSet.h"int main()
{bit::bitset<1000> bs;bs.set(1);bs.set(10);bs.set(100);cout << bs.test(1) << endl;cout << bs.test(10) << endl;cout << bs.test(100) << endl << endl;bs.reset(10);cout << bs.test(1) << endl;cout << bs.test(10) << endl;cout << bs.test(100) << endl << endl;return 0;
}
四、位图的应用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
给定100亿个整数,设计算法找到只出现一次的整数?
- 由于题目的需求是找到只出现一次的整数,那么我们就需要进行统计整数的次数,这些整数有出现0次的,有只出现一次的,有出现两次及以上的整数,那么我们并不需要真的统计出所有整数出现的次数,而是统计出现0次,出现1次,出现2次进行统计,其中出现2次代表出现了两次及以上的次数
- 我们可以使用两个位图的同一个位置的两个比特位去控制,因为两个比特位可以表示00,01,10,11,对应到二进制位其中00表示出现0次,01表示出现1次,10表示出现了两次(这道题目中特殊应用,使用10表示出现了两次及以上,即当一个数字已经出现了两次之后,再出现一次我们仍然使用两次进行统计,因为这里的两次表示出现了两次即以上),11在这道题目中不需要进行设置,因为00,01,10已经可以满足我们的需求了
- 我们复用先前写的bitset ,用两个 bitset 去封装一个类twobitset 这样有两个位图进行控制,第一个位图控制第一位,第二个位图控制第二位
template<size_t N>
class twobitset
{
public:void set(size_t x){if (!(set1.test(x)) && !(set2.test(x)))//是00的情况{set1.set(x);//变成10 ——> 1个}else if ((set1.test(x)) && !(set2.test(x)))//是10的情况{set2.set(x);//变成11 ——>2个}else if ((set1.test(x)) && (set2.test(x)))//是11的情况{set1.reset(x);//变成01 ——>2个以上}}void only_once(size_t x){if (set1.test(x) && !(set2.test(x)))//10{cout << x << " ";}}bitset<N> set1;bitset<N> set2;
};
- void only_once(size_t x) : 如果这个数字在set 1和set 2中,遍历完之后只是 10 的状态,那说明这个数字只出现了一次
测试一下:
bit::bitset<1000> bs;
bit::twobitset<1000> tbs;
int arr[] = { 1,9,9,8,23,12,7,2,3,7,8,9,9,8,7,2,3,4,5,6,6,5,4,3,2 };
//给定100亿个整数,设计算法找到只出现一次的整数?
for (size_t e : arr)
{tbs.set(e);
}for (size_t e : arr)
{tbs.only_once(e);
}
cout << endl;
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
- 再用位图遍历数字时,相当于是起到了去重的效果,因为就算有相同的数字,那么对于那个数字上的比特位依旧还是1
- 我们用位图bs1和bs2分别去遍历,这两个文件(这里用数组代替),如果在bs1和bs2中,同一个比特位都显示为一,说明这个数字是ta们的交集
int arr1[] = { 1,9,9,2,3,4,8,23,12,7,2,3,3,4,8,23,12,6,5,4,37,8,9,9,8,7,5,6,2 };
int arr2[] = { 1,9,9,8,7,5,6,2 };
bit::bitset<1000> bs1;
bit::bitset<1000> bs2;for (size_t e : arr1)//这里的位图相当于取到了去重的效果
{bs1.set(e);
}for (size_t e : arr2)
{bs2.set(e);
}for (size_t i = 0; i < 1000; i++)
{if (bs1.test(i) && bs2.test(i))cout << i << " ";
}
位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
- 这里的,不超过两次就分为0次,一次和两次
- 依旧用两个位图 ,如果两个位图 在同一个比特位上,两个比特位分别为00 ,则表示零次,01则表示一次,11则表示两次,10则表示两次,则只用检测前三种状态即可
int arr3[] = { 1,9,9,2,3,4,8,23,12,7,2,3,3,4,8,23,12,6,5,4,37,8,9,9,8,7,5,6,2 };bit::twobitset<1000> tbs;for (size_t e : arr3)
{tbs.set(e);
}for (size_t e : arr3)
{if (!(tbs.set1.test(e)) && !(tbs.set2.test(e)))//都为零-00{cout << e << " ";}else if ((tbs.set1.test(e)) && (tbs.set2.test(e)))//11{cout << e << " ";}else if ((tbs.set1.test(e)) && !(tbs.set2.test(e)))//10{cout << e << " ";}
}
五、源代码
test.cpp
#include<iostream>
using namespace std;
#include"BitSet.h"int main()
{bit::bitset<1000> bs;bit::twobitset<1000> tbs;int arr[] = { 1,9,9,8,23,12,7,2,3,7,8,9,9,8,7,2,3,4,5,6,6,5,4,3,2 };//给定100亿个整数,设计算法找到只出现一次的整数?for (size_t e : arr){tbs.set(e);}for (size_t e : arr){tbs.only_once(e);}cout << endl;bit::bitset<0xffffffff> bs2;//给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?int arr1[] = { 1,9,9,2,3,4,8,23,12,7,2,3,3,4,8,23,12,6,5,4,37,8,9,9,8,7,5,6,2 };int arr2[] = { 1,9,9,8,7,5,6,2 };bit::bitset<1000> bs1;bit::bitset<1000> bs2;for (size_t e : arr1)//这里的位图相当于取到了去重的效果{bs1.set(e);}for (size_t e : arr2){bs2.set(e);}for (size_t i = 0; i < 1000; i++){if (bs1.test(i) && bs2.test(i))cout << i << " ";}//位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数int arr3[] = { 1,9,9,2,3,4,8,23,12,7,2,3,3,4,8,23,12,6,5,4,37,8,9,9,8,7,5,6,2 };bit::twobitset<1000> tbs;for (size_t e : arr3){tbs.set(e);}for (size_t e : arr3){if (!(tbs.set1.test(e)) && !(tbs.set2.test(e)))//都为零-00{cout << e << " ";}else if ((tbs.set1.test(e)) && (tbs.set2.test(e)))//11{cout << e << " ";}else if ((tbs.set1.test(e)) && !(tbs.set2.test(e)))//10{cout << e << " ";}}return 0;
}
bitset 和 twobitset
#pragma once
#include<vector>namespace bit
{template<size_t N>class bitset{public:bitset(){_a.resize(N / 32 + 1);}// x映射的那个标记成1void set(size_t x){size_t i = x / 32;size_t j = x % 32;_a[i] |= (1 << j);}// x映射的那个标记成0void reset(size_t x)//{size_t i = x / 32;size_t j = x % 32;_a[i] &= (~(1 << j));}bool test(size_t x)//找到该数字存不存在 -- 1或0{size_t i = x / 32;size_t j = x % 32;return _a[i] & (1 << j);}private:vector<int> _a;};template<size_t N>class twobitset{public:void set(size_t x){if (!(set1.test(x)) && !(set2.test(x)))//是00的情况{set1.set(x);//变成10 ——> 1个}else if ((set1.test(x)) && !(set2.test(x)))//是10的情况{set2.set(x);//变成11 ——>2个}else if ((set1.test(x)) && (set2.test(x)))//是11的情况{set1.reset(x);//变成01 ——>2个以上}}void only_once(size_t x){if (set1.test(x) && !(set2.test(x)))//10{cout << x << " ";}}bitset<N> set1;bitset<N> set2;};
}