概述
王者有一段时间没玩了,待再次上线的时候,TM(天美)发来了一封邮件,亲爱的召唤师,欢迎回归王者荣耀,你已有88日没有登录过游戏,这是为你精心准备的回归大礼包,礼包是一些体验卡和砖石等。but作为一名程序猿,让楼主更在意的是88这个数字的统计方式。
我们知道王者荣耀用户数很多,假设有一亿用户,如何来记录用户的登录信息,如何来查询活跃用户(如一周内登录三次以上的),最常规的做法就是建一张用户登录信息表,有用户ID,有登录时间这样的,然后用户每登录一次就往表中插入一条数据,没毛病,那么假设一天之内有1亿用户登录,那么2天表中就会有2亿数据,这里会有很严重的问题,首先表中不可能承载这么多数据量,其次就算可以装得下这么多数据,那你怎么统计这么多数据的表?效率性能如何?所以在传统数据库存储层面是不好解决这个问题。
因此,我们不妨设置用一个1bit位来标识用户的登录状态,1/0,1是代表登录,0是代表没登录,那么可以建立如下的数字模型
假设有10个用户,统计一周之内用户的登录次数,模型假如是这样的
星期一:0000011111
星期二:1001011011
星期三:1001011111
星期四:1011000001
星期五:1001011001
横着来看:就标识着星期一这天后边5个用户登录了,前5个用户没登录,星期二1,4,6,7,9,10用户登录其余没有,其余同理,清晰可见。
竖着来看:就标识这同一个人一周之内的登录情况,比如第一个人,周二三五登录了游戏,二四就没有玩,其余同理,便于统计。
这里的数字模型可以是一个字符串或者是数组,这是简体思路。
下面进入主题,位图算法,了解一下!
数据库做持久化的时候,把数据做成数字模型这种形式来存储(比如只存用户ID),若有数据就标志为1或true,若无数据标志为0或false。
比如有一数字模型{5,2,1,2} 这里最大值为5,所以数组的长度就是5,而0到5中不存0,3,4数字
所以:Array[0]=0,Array[1]=1,Array[2]=2,Array[3]=0,Array[4]=0,Array[5]=1
数组模型如下 :int[] ={0,1,2,0,0,1}
上面数中由于2有两个,所以只能用int存数组的值,不用boolean型,这样如果有多个同样的数字可以用值表示个数。如上面Array[2]=2,就表示2有2个。
又如:
假设我们有{0,6,3,4}这数组,在位图中数据结构初始化状态应该就是这样的,首先最大是6,那我们申请l大小为6的数组
通过位图算法处理后,得到的位图是这样的
这种算法的缺点在于,最大值和最小值之间不能相差太大,否则浪费申请数组的空间。(蛋士可以优化滴~)
实际应用:
- 判断一个数是否存在某数据中,假如有40亿数据,我们如何快速判断指定一个数是否存在?
申请512M的内存 512M=51210241024B*8=4294967296比特(bit) 这个空间可以装40亿了
一个bit位代表一个int值
读入40亿个数,设置相应的bit位
读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在
- 判断整形数组是否重复
它的做法是按照集合中最大元素max创建一个长度为max+1的新数组,然后再次扫描原数组,遇到几就给新数组的第几位置上1,如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话效率还能提高一倍。
- 给数组排序
首先遍历数组,得到数组的最大最小值,然后根据这个最大最小值来缩小bitmap的范围。这里需要注意对于int的负数,都要转化,而且取位的时候,数字要减去最小值。
给出JAVA代码
public class WeiTu {
public static int[] bitmapSort(int[] arr) {
// 找出数组中最值
int max = arr[0];
int min = max;
for (int i : arr) {
if (max < i) {
max = i;
}
if (min > i) {
min = i;
}
}
//初始化位图数组大小
int temp=0;//用于解决数组有负数的情况
int[] newArr=null;
if(min<0){
temp=0-min;
newArr = new int[max - min + 1];
}else{
newArr = new int[max+1];
min=0;
}
//构建位图
for(int i:arr){
newArr[i+temp]++;//算法体现
}
// 重新调整arr数组中的元素
int index = 0;
for (int i = 0; i < newArr.length; i++) {
// 位图是1的就输出,对数组排序
while (newArr[i] > 0) {
arr[index] = i + min;
index++;
newArr[i]--;
}
}
return arr;
}
public static void main(String[] args) {
int[] arr={5,2,3,7,1};
//int[] arr={-5,2,-3,7,1};
int[] arrsort=bitmapSort(arr);
for(int i:arrsort)
System.out.println(i);
}
}
- 做交集和并集效率极高
举个例子:
有一位图0000101,代表喜欢吃苹果用户
另一位图0000111,代表喜欢吃西瓜用户统计喜欢吃苹果或西瓜的用户,0000101|0000111=0000111
位图模型详解
有40亿个无符号的整型数据,现在给定一个目标数字,判断这个数字是否在这40亿数据中。题目看起来确实非常简单,有的同学说直接遍历一遍不就ok了吗?还有的同学给出了更高效的查找方式就是将这些数字排序然后进行二分查找。但是,这是有问题的,问题并不在于你搜索这个数字的效率问题,而是你在遍历也好排序也罢,这些数字在内存中放的下么?
一个整型int就是4个字节,10亿个int差不多已经需要4G的内存了,40亿个int就是16G。所以这里方法行不通的根本原因实际上是内存不够,但是我们今天的讲的位图却能很好的帮助我们处理这个问题。
既然根本原因是这些数据用int放不下,那么是否有更小的东西标记这些数字呢?没错,有的同学想到了,char只占一个字节或许能表示一个数字,但是随着数字位数的增多,依旧不可能使用一个字符表示一个数字,这就意味着小于4G内存还是不能解决这个问题。
其实说到这里,我们的问题就转化为如何使用更小的内存单元来标记一个数字,而在程序中我们最小的访问单位的bit位,所以现在我们一起来看使用比特位如何标记(映射)这些数据。
现在我们发现,4个字节本来只能存储一个int,而现在使用位图我们就存了(映射)32个数字,意味着16G/32约等于500m左右我们就能映射这些数据,那么这些数据是怎么映射到位图种的呢?接着看。
设计位图
为了方便,我们将位图用一个数组表示,让vector帮我们开辟一段连续的空间,我们只负责将数据设置或者移除就行。
1. 初始化
class BitMap
{
public:
BitMap(size_t range)
{
//右移5位相当于除以32,加1是因为小于32的数字如果与32相除则得到0
_bitTable.resize((range >> 5) + 1);//需要 (maxnum / 类型字节数)+ 1 的空间来存储
}
private:
vector<int> _bitTable;//下标代表每一段bit空间,_bitTable[i]的值代表这一段里面有没有表示数(0无,1有)
};
2. 位图元素的设置
void SetBit(size_t x)
{
size_t index = x >> 5;
size_t num = x % 32;
//初始该段_bitTable[index]无数字,为全0;现放入x后,该段_bitTable[index]对应位置变成1,代表其中有数字。
// 0 | 0 == 0,0 | 1 == 1,或运算,原本没数的位置还是0,放入数的位置变成1
_bitTable[index] |= (1 << num);//该段中 第num位下标 表示的数 存在了,值置1,所以1左移num个
}
来看看为什么需要size_t index = x >> 5和size_t num = x % 32两步操作:我们看看要映射5和32这俩个数
5表示放在第1个整型空间的第5位上,32则表示放在第2个整型空间第一位上。而**bitTable[index] |= (1 << num)**能保证把第num位上的数字设置为1,其余数字保持不变。
3. 位图元素的移除
比较简单,需要知道的是**~(1 << num)**表示出了num位为0,其余位都为1.
void RemoveBit(size_t x)
{
size_t index = x >> 5;
size_t num = x % 32;
//1 & 0 == 0, 0 & 1 == 0 所以有数的位置1被移除变成0,其他没数的位置本来就是0,&也不会影响
_bitTable[index] &= ~(1 << num);
}
4.位图元素的查找
这个没啥好说的,很简单,说到这里,你的位图也就实现完了,非常简单把
bool TestBit(size_t x)
{
size_t index = x >> 5;
size_t num = x % 32;
//1 & 1 == 1, 0 & 1 == 0 表明bitmap中该位置有匹配数结果就为1,无匹配数结果为0
return _bitTable[index] & (1 << num);
}
完整代码实现
class BitMap
{
public:
BitMap(size_t range)
{
_bitTable.resize((range >> 5) + 1);
}
//标识一个数字在位图中的位置
void SetBit(size_t x)
{
size_t index = x >> 5;
size_t num = x % 32;
_bitTable[index] |= (1 << num);
}
//取消数字在位图当中的标识.
void RemoveBit(size_t x)
{
size_t index = x >> 5;
size_t num = x % 32;
_bitTable[index] &= ~(1 << num);
}
bool TestBit(size_t x)
{
size_t index = x >> 5;
size_t num = x % 32;
return _bitTable[index] & (1 << num);
}
private:
vector<int> _bitTable;
};
拓展
现在将问题修改为让你寻找出40亿个数据中出现过两次的数据,此时我们就需要使用两位来标记同一个数据了。
N位位图的实现如下:
class NBitMap
{
public:
NBitMap(size_t range)
{
_bitTable.resize((range >> 4) + 1);
}
void SetBit(size_t x)
{
size_t index = x >> 4;
size_t num = x % 16;
num *= 2;
bool first = _bitTable[index] & (1 << num);
bool second = _bitTable[index] & (1 << (num + 1));
if (!(first && second))
{
_bitTable[index] += (1 << num);
}
}
bool TestBit(size_t x)
{
size_t index = x >> 4;
size_t num = x % 16;
num *= 2;
return (_bitTable[index] >> num) & 0x03;
}
private:
vector<int> _bitTable;
};
关于位图的讲解就到这里,现在我让你查找10亿个字符串中出现一次的那个字符串,有的同学丝毫不犹豫就要用我们使用的位图,但是仔细思考,我们这里的位图只是可以映射数字类型的数据,变成字符串以及其他文件好像就不再那么得心应手了,别急,聪明的大佬们又想到了一种骚东西叫做布隆过滤器,那么布隆过滤器是什么呢?请看下篇博客哦。
转载自:https://blog.csdn.net/lucky52529/article/details/90172264
https://www.cnblogs.com/zhuoqingsen/p/9214709.html