前言
我们知道一个1G=1024M,1M=1024K,1K=1024byte,1byte=8bit,所以1个字节等于8bit,也就是8个二进制位,位图法的概念是用一个位(bit)来标记某个数的存放状态,所以节省了大量的空间。
原理
以二进制位来表示数字
例如:第27位为1,第28位为0。表示在map中27存在28不存在
应用场景
大量数据进行排序,查找和去重上可以使用这个策略来降低内存的使用。举例
1:开发一个用户画像系统,实现用户信息的标签化。用户标签包含用户的社会属性,生活习惯,消费行为。
2:redis 用setbit(bitmap)统计活跃用户
数据结构
unsigned int bit[N],在这个数组里面,可以存储N * PHP_INT_SIZE * 8个数据,但是最大的数只能是N * PHP_INT_SIZE * 8-1。例如我们要存储的数据范围为0-63,则我们只需要将N=1,这样就可以把数据存进去,如下图:
数据为[5,1,7,15,0,4,6,10,14],将这些数据存入这个结构中为
代码实现
<?php
class BitMap {
//创建长度为50的数组并用0填充
private $bitMap;
private $bitSize;
public function __construct() {
$this->bitMap = array_fill(0, 50, 0);
$this->bitSize = PHP_INT_SIZE * 8;
}
/**
* set
* @param array $arr
* @return array
*/
public function set($arr) {
$checkRet = $this->checkParams($arr);
if($checkRet === false) {
return $this->bitMap;
}
foreach ($arr as $item) {
//获取索引位置
list($bytePos, $bitPos) = $this->getIndex($item);
//这个将1是左移多少位
$position = 1 << $bitPos;
//两个数按或运算,作用就是把二进制上对应位置的0置为1
$this->bitMap[$bytePos] = $this->bitMap[$bytePos] | $position;
}
return $this->bitMap;
}
/**
* contain
* @param array $arr
* @return array
*/
public function contain($arr) {
$checkRet = $this->checkParams($arr);
if($checkRet === false) {
return $this->bitMap;
}
$output = array();
foreach($arr as $item) {
//获取索引位置
list($bytePos, $bitPos) = $this->getIndex($item);
//这个将1是左移多少位
$position = 1 << $bitPos;
//将1左移position后,那个位置自然就是1,然后和以前的数据做&,判断是否为0即可
$ret = ($this->bitMap[$bytePos] & $position) != 0;
$output[$item] = (int)$ret;
}
return $output;
}
/**
* del
* @param array $arr
* @return array
*/
public function del($arr) {
$checkRet = $this->checkParams($arr);
if($checkRet === false) {
return $this->bitMap;
}
foreach($arr as $item) {
//获取索引位置
list($bytePos, $bitPos) = $this->getIndex($item);
//这个将1是左移多少位
$position = 1 << $bitPos;
$this->bitMap[$bytePos] &= ~$position;
}
return $this->bitMap;
}
/**
* setAndSort output
* @param array $arr
* @return array
*/
public function setAndSort($arr) {
$checkRet = $this->checkParams($arr);
if($checkRet === false) {
return $this->bitMap;
}
$this->set($arr);
$output = array();
foreach ($this->bitMap as $k => $item){
for($i = 0; $i < $this->bitSize; $i++){
$temp = 1 << $i;
$flag = $temp & $item;
if ($flag){
$output[] = $k * $this->bitSize + $i;
}
}
}
return $output;
}
/**
* 校验参数
* @param array $arr
* @return bool
*/
private function checkParams($arr) {
if (!is_array($arr) || empty($arr)) {
return false;
}
return true;
}
/**
* 获取index
* @param int $item
* @return array
*/
private function getIndex($item) {
//获得byte索引位置
$bytePos = $item / $this->bitSize;
//获得bit的索引位置
$bitPos = $item % $this->bitSize;
return array(
$bytePos,
$bitPos,
);
}
}
$bitmap = new BitMap();
print_r($bitmap->set(array(1,2,3,500)));
print_r($bitmap->contain(array(1,2,3)));
print_r($bitmap->setAndSort(array(100,1,3000,2,3)));
print_r($bitmap->del(array(500)));
print_r($bitmap->contain(array(1,2,3,500)));
?>
补充知识
位运算符
$a << $b Shift left(左移) 将 $a 中的位向左移动 $b 次(每一次移动都表示"乘以 2")。
$a >> $b Shift right(右移) 将 $a 中的位向右移动 $b 次(每一次移动都表示"除以 2")。
$a | $b Or(按位或) 将把 $a 和 $b 中任何一个为 1 的位设为 1。
$a & $b And(按位与) 将把 $a 和 $b 中都为 1 的位设为 1。
$a ^ $b Xor(按位异或) 将把 $a 和 $b 中一个为 1 另一个为 0 的位设为 1。
~ $a Not(按位取反) 将 $a 中为 0 的位设为 1,反之亦然。
bitmap的缺点
无法进行非运算
以一个用户数据为例,用户基本信息如下。
按照年龄标签,可以划分成90、00后两个bitmap
问题引出
当我想获取非90后用户的数量时,是无法直接非运算的,因为如果直接非运算,得到的数时是8,显然是
不符合我们预期的,我们的预期是1
解决思路
借助一个全量的bitmap
我们给定 90 后用户的 Bitmap,再给定一个全量用户的 Bitmap。最终要求出的是存在于全量用户,但又不存在于 90 后用户的部分。
90后用户
全量用户
我们可以使用异或操作,即相同位为0,不同位为1
bitmap优化
问题引出
如果在一个很长的bitmap里只存一两个用户,那岂不是很浪费空间。
问题解决
谷歌所实现的EWAHCompressedBitmap,对bitmap存储空间做了一定的优化。