Bit-map压缩及使用
Bit-map空间压缩和快速排序去重
1. Bit-map的基本思想
32位机器上,对于一个整型数,比如int a=1 在内存中占32bit位,这是为了方便计算机的运算。但是对于某些应用场景而言,这属于一种巨大的浪费,因为我们可以用对应的32bit位对应存储十进制的0-31个数,而这就是Bit-map的基本思想。Bit-map算法利用这种思想处理大量数据的排序、查询以及去重。
Bitmap在用户群做交集和并集运算的时候也有极大的便利。
2. Bit-map应用之快速排序
假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0,
对应位设置为1:
遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。
优点:
运算效率高,不需要进行比较和移位;
占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M。
缺点:
所有的数据不能重复。即不可对重复的数据进行排序和查找。
3. Bit-map应用之快速去重
2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
首先,根据“内存空间不足以容纳这2.5亿个整数”我们可以快速的联想到Bit-map。下边关键的问题就是怎么设计我们的Bit-map来表示这2.5亿个数字的状态了。其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间几十兆左右。
接下来的任务就是遍历一次这2.5亿个数字,如果对应的状态位为00,则将其变为01;如果对应的状态位为01,则将其变为11;如果为11,,对应的转态位保持不变。
最后,我们将状态位为01的进行统计,就得到了不重复的数字个数,时间复杂度为O(n)。
4. Bit-map应用之快速查询
同样,我们利用Bit-map也可以进行快速查询,这种情况下对于一个数字只需要一个bit位就可以了,0表示不存在,1表示存在。假设上述的题目改为,如何快速判断一个数字是够存在于上述的2.5亿个数字集合中。
同之前一样,首先我们先对所有的数字进行一次遍历,然后将相应的转态位改为1。遍历完以后就是查询,由于我们的Bit-map采取的是连续存储(整型数组形式,一个数组元素对应32bits),我们实际上是采用了一种分桶的思想。一个数组元素可以存储32个状态位,那将待查询的数字除以32,定位到对应的数组元素(桶),然后再求余(%32),就可以定位到相应的状态位。如果为1,则代表改数字存在;否则,该数字不存在。
5. Bit-map扩展——Bloom Filter(布隆过滤器)
当一个元素被加入集合中时,通过k各散列函数将这个元素映射成一个位数组中的k个点,并将这k个点全部置为1.
有一定的误判率--在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误判为属于这个集合.因此,它不适合那些"零误判"的应用场合.在能容忍低误判的应用场景下,布隆过滤器通过极少的误判换区了存储空间的极大节省.
Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注:如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。
在判断y是否属于这个集合时,对y应用k次哈希函数,若所有hi(y)的位置都是1(1≤i≤k),就认为y是集合中的元素,否则就认为y不是集合中的元素。
6. 总结
使用Bit-map的思想,我们可以将存储空间进行压缩,而且可以对数字进行快速排序、去重和查询的操作。Bloom Fliter是Bit-map思想的一种扩展,它可以在允许低错误率的场景下,大大地进行空间压缩,是一种拿错误率换取空间的数据结构。
7. 应用
适用范围:可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下
基本原理及要点:使用bit数组来表示某些元素是否存在,比如8位电话号码
扩展:bloom filter可以看做是对bit-map的扩展
问题实例:
1、已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
8位最多99 999 999,大概需要99m个bit,大概10几M字节的内存即可。
2、在2.5亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数。
方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存232*2bit=1GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。
空间占用、以及第一次分配空间需要的时间
在一台2010MacBook Pro上,offset为2^32-1(分配512MB)需要~300ms,offset为2^30-1(分配128MB)需要~80ms,offset为2^28-1(分配32MB)需要~30ms,offset为2^26-1(分配8MB)需要8ms。<来自官方文档>
大概的空间占用计算公式是:($offset/8/1024/1024)MB
使用场景一:用户签到
很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况,如果使用bitmap我们怎么做?一言不合亮代码!
<?php $redis = new Redis(); $redis->connect('127.0.0.1'); //用户uid $uid = 1; //记录有uid的key $cacheKey = sprintf("sign_%d", $uid); //开始有签到功能的日期 $startDate = '2017-01-01'; //今天的日期 $todayDate = '2017-01-21'; //计算offset $startTime = strtotime($startDate); $todayTime = strtotime($todayDate); $offset = floor(($todayTime - $startTime) / 86400); echo "今天是第{$offset}天" . PHP_EOL; //签到 //一年一个用户会占用多少空间呢?大约365/8=45.625个字节,好小,有木有被惊呆? $redis->setBit($cacheKey, $offset, 1); //查询签到情况 $bitStatus = $redis->getBit($cacheKey, $offset); echo 1 == $bitStatus ? '今天已经签到啦' : '还没有签到呢'; echo PHP_EOL; //计算总签到次数 echo $redis->bitCount($cacheKey) . PHP_EOL; /** * 计算某段时间内的签到次数 * 很不幸啊,bitCount虽然提供了start和end参数,但是这个说的是字符串的位置,而不是对应"位"的位置 * 幸运的是我们可以通过get命令将value取出来,自己解析。并且这个value不会太大,上面计算过一年一个用户只需要45个字节 * 给我们的网站定一个小目标,运行30年,那么一共需要1.31KB(就问你屌不屌?) */ //这是个错误的计算方式 echo $redis->bitCount($cacheKey, 0, 20) . PHP_EOL;
使用场景二:统计活跃用户
使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1
那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令
命令BITOP operation destkey key [key ...]
说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
说明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数
//日期对应的活跃用户 $data = array( '2017-01-10' => array(1,2,3,4,5,6,7,8,9,10), '2017-01-11' => array(1,2,3,4,5,6,7,8), '2017-01-12' => array(1,2,3,4,5,6), '2017-01-13' => array(1,2,3,4), '2017-01-14' => array(1,2) ); //批量设置活跃状态 foreach($data as $date=>$uids) { $cacheKey = sprintf("stat_%s", $date); foreach($uids as $uid) { $redis->setBit($cacheKey, $uid, 1); } } $redis->bitOp('AND', 'stat', 'stat_2017-01-10', 'stat_2017-01-11', 'stat_2017-01-12') . PHP_EOL; //总活跃用户:6 echo "总活跃用户:" . $redis->bitCount('stat') . PHP_EOL; $redis->bitOp('AND', 'stat1', 'stat_2017-01-10', 'stat_2017-01-11', 'stat_2017-01-14') . PHP_EOL; //总活跃用户:2 echo "总活跃用户:" . $redis->bitCount('stat1') . PHP_EOL; $redis->bitOp('AND', 'stat2', 'stat_2017-01-10', 'stat_2017-01-11') . PHP_EOL; //总活跃用户:8 echo "总活跃用户:" . $redis->bitCount('stat2') . PHP_EOL;
假设当前站点有5000W用户,那么一天的数据大约为50000000/8/1024/1024=6MB
使用场景三:用户在线状态
前段时间开发一个项目,对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,不在线就设置为0,和上面的场景一样,5000W用户只需要6MB的空间。
//批量设置在线状态 $uids = range(1, 500000); foreach($uids as $uid) { $redis->setBit('online', $uid, $uid % 2); } //一个一个获取状态 $uids = range(1, 500000); $startTime = microtime(true); foreach($uids as $uid) { echo $redis->getBit('online', $uid) . PHP_EOL; } $endTime = microtime(true); //在我的电脑上,获取50W个用户的状态需要25秒 echo "total:" . ($endTime - $startTime) . "s"; /** * 对于批量的获取,上面是一种效率低的办法,实际可以通过get获取到value,然后自己计算 * 具体计算方法改天再写吧,之前写的代码找不见了。。。 */