BitMap是用bit位来记录数据存在与否的一种算法。在处理大数据时,可以节省大量空间,速度也很快。
先来看一个例子。
已知有n个整数,这些整数的范围是[0,100],请你设计一种数据结构,使用数组存储这些数据,并提供两种方法,
分别是addMember和isExist.下面是这种数据结构的类的定义。
function FindClass(){
var datas = [];//存储数据
//加入一个整数
this.addMember = function(member){
//TODO
}
// 判断member是否存在
this.isExist = function(member){
//TODO
}
}
//测试用例
var fc = new FindClass();
var arr = [0,3,5,6,9,34,23,78,99];
for (let i = 0; i < arr.length; i++) {
fc.addMember(arr[i])
}
console.log(fc.isExist(3));
console.log(fc.isExist(7));
console.log(fc.isExist(78));
当实现addMember和isExist之后,执行上面的代码,预期输出的结果是:
true
false
true
实现方式 一
//实现方式 一
this.addMember = function(member){
datas.push(member)
}
this.isExist = function(member){
for (let i = 0; i < datas.length; i++) {
if(datas[i]==member){
return true;
}
}
return false;
}
//isExist 的实现方式2
this.isExist = function(member){
if(datas.indexOf(member)>=0){
return true;
}
return false;
}
实现方式 二
方式一虽然实现了功能,但是速度慢,不论使用for循环还是indexOf的方法,时间复杂度都是O(n),加入的元素越多,isExist方法的速度越慢, 我们需要一个时间复杂度是O(1)的算法,不论向里面增加了多少数据,isExist的执行速度都是常量时间。
通过索引操作数据,时间复杂度就是O(1)。题目说明这些数在0-100之间,那么就用每个数自身的值作为索引,比如3这个数,可以让data[3]=1, 就表示把3添加进来了。data[2]=0,表示2没有添加进来,如此,isExist方法就可以利用索引来判断member是否存在。
function FindClass(){
var datas = new Array(100);
//先都初始化为0
for (let i = 0; i < datas.length; i++) {
datas[i] = 0;;
}
//添加一个整数
this.addMember = function(member){
datas[i] = 1;
}
//判断member是否存在
this.isExist = function(member){
if(datas[member]==1){
return true;
}else{
return false;
}
}
}
更节省空间的算法
上面的算法已经很快了。但是却面临一个新的问题,如果数据非常多,多达1亿,每个整数是4个字节,那么1亿正式就是4亿个字节,1024个字节是1kb,1024kb是1M,4亿个字节就是381M的内存空间。 如果没有那么多内存怎么办?我们需要一种数据压缩的算法。用很少的空间来表示这1亿个数存在与否。
看下面的例子:
街边有8栈路灯,编号分别为1 2 3 4 5 6 7 8,其中2号、5号、7号、8号路灯是亮着的。其它是不亮的。
请设计一种简单的方法来表示这8栈路灯的亮与不亮的状态。
我们可以用二进制表示。
1 2 3 4 5 6 7 8
0 1 0 0 1 0 1 1
仅8个bit位就能表示8栈路灯的亮灭情况。一个整数有32个bit位,就可以表示32栈路灯的亮灭 情况。
上述中的isExist方法要判断一个数是否存在,是不是也可以借助这种方式呢?
假设 value 是一个int类型的的数据,初始化为0,当addMember传进来的参数为0的时候,就把value的二进制第一个设置为1.
00000000 00000000 00000000 00000001
此时value = 1.
如果又增加了一个3,就把value的二进制的第四位设置为1,
00000000 00000000 00000000 00001001
此时,value = 9,9可以表示0和3都存在。
一个整数可以表示0-31的存在与否,如果创建一个大小为10的数组,数组里存储整数,那么这个数组就可以表示0-319的存在与否
datas[0] 表示0-31存在与否
datas[1] 表示32-63存在与否
。
。
。
datas[9] 表示288-319存在与否
通过这种方式,就可以把空间的使用降到原来的32分之一,存储一亿个整数的存在与否,只需要12M的内存空间。
二进制的位运算
- 按位与&
两个整数进行按位与运算,相同二进制位的数字如果都是1,则结果为1,有一个为0,则结果为0. 如3&7 的计算
0 1 1 -- 3
1 1 1 -- 7
0 1 1 -- 3
即3&7 = 3;
- 按位或 |
两个整数按位或运算,相同二进制的数字如果有一个为1,则结果为1,都为0,则结果为0 。 如 5 | 8
0 1 0 1 -- 5
1 0 0 0 -- 8
1 1 0 1 -- 13
即5|8=13
- 左移操作 <<
二进制向左移动n位,在后面添加n个0
下面是3<<1 的计算过程
0 1 1 -- 3
0 1 1 0 -- 6
即 3<<1 = 6
如:一组数,内容为 3 9 19 20 ,请用一个整数来表示这四个数。
var value = 0;
value = value| 1<<3;
value = value| 1<<9;
value = value| 1<<19;
value = value| 1<<20;
console.log(value);//1573384
新的实现方式 --bitMap
我们重新设计一个类,实现addMember和isExist方法,用更快的速度,更少的内存。
- 数据范围是0-100,那么只需要4个整数就可以表示4*32个数的存在与否,创建一个大小为4的数组。
- 执行addMember时,先用member/32,确定member在数组里的索引(arr_index),然后用member%32,确定在整数的哪个二进制位进行操作(bit_index), 最后执行bit_arr[arr_index]=bit_arr[arr_index]|1<<bit_index;
- 执行isExist时,先用member/32,确定member在数组里的索引(arr_index),然后用member%32,确定在整数的哪个二进制位进行操作(bit_index),最后执行bit_arr[arr_index]=bit_arr[arr_index]&1<<bit_index;如果结果不为0,就说明member存在。
function BitMap(size){
var bit_arr = new Array(size);
for(var i = 0;i<bit_arr.length;i++){
bit_arr[i] = 0;
}
this.addMember = function(member){
//决定在数组中的索引
var arr_index = Math.floor(member/32);
//决定在整数的32个bit位的哪一位
var bit_index = member%32;
bit_arr[arr_index] = bit_arr[arr_index]|1<<bit_index;
}
this.isExist = function(member){
var arr_index = Math.floor(member/32);
var bit_index = member%32;
var value = bit_arr[arr_index] &1<<bit_index;
if(value!=0){
return true;
}
return false;
}
}
这种数据结构基于位做映射,能用很少的内存存储数据,和数组不同,他只能表示某个数是否存在,可以用于大数据去重、大数据排序、两个集合取交集。
BitMap在处理大数据时才有优势,而且要求数据集紧凑,如果处理的数据只有3个1,1000,100000,那么空间利用率太低了,最大的值决定了BitMap要用多少内存。
大数据排序
有多达10亿个无序整数,已知最大值为15亿,请对这10亿个数进行排序。
BitMap存储最大值为15亿的集合,只需要180M的空间。位运算的速度非常快的。
第一次遍历,将10亿个数都放入bitMap中,第二次,从0到15亿进行遍历,如果在bitMap中,则输出该值,这个经历2次遍历,就可以将如此多的数据排序。
为了方便演示,只用很小的数组[0,6,88,7,73,34,10,99,22],已知数组最大值是99,利用bitMap排序算法如下:
var arr = [0,6,88,7,73,34,10,99,22];
var sort_arr = [];
var bit_map = new BitMap(4);
for (let i = 0; i < arr.length; i++) {
bit_map.addMember(arr[i]);
}
for (let i = 0; i < 99; i++) {
if(bit_map.isExist(i)){
sort_arr.push(i);
}
}
console.log(sort_arr);
- 需要强调的是,利用bitMap排序,待排序集合中不能有重复数据。
布隆过滤器
bitMap有很强的局限性,bitMap只能用来处理整数,无法用于处理字符串。
假如让你写一个强大的爬虫,每天爬取数以亿计的网页,那么你就需要一种数据结构,能够存储你已经爬取过的url, 这样,才不至于重复爬取。
你可能会想到使用hash函数对url处理,转成整数。这样,似乎又可以使用bitMap了。但这样还是有问题。
假设bitMap能够映射的最大值是M,一个url的hash值需要对M求模,这样,就会产生冲突,而且随着存储数据的增多,冲突量会越来越大。
布隆过滤器的思想很简单,其基本思路和bitMap是一样的,可以把布隆过滤器看做bitMap的扩展。为了解决冲突率,布隆过滤器要求使用K个hash函数, 新增一个key时,把key散列成K个整数,然后在数组中将这K个整数所对应的二进制位设置为1,判断某个key是否存在时,还是使用K个hash函数对key进行散列, 得到k个整数,如果这k个整数所对应的二进制位都是1,就说明这个key存在,否则,这个key不存在。
对于一个布隆过滤器,有两个参数需要设置,一个是预估的最多存放的数据的数量,一个是可以接受的冲突率。
// 定义一个布隆过滤器
function BoolmFilter(max_count,error_rate){
var bitMap = [];//位图映射变量
var max_count = max_count;//最多可放的数量
var error_rate = error_rate;//错误率
// 位图变量的长度
var bit_size = Math.ceil(max_count*(-Math.log(error_rate)/(Math.log(2)*Math.log(2))));
var hash_count = Math.ceil(Math.log(2)*(bit_size/max_count));
//每次add的时候,都把key散列成k个值,并将这k个值对应的二进制位设置为1,那么为1的这个动作就需要执行n次。
// 设置位的值
var set_bit = function(bit){
var arr_index = Math.floor(bit/32);
var bit_index = Math.floor(bit%32);
bitMap[arr_index] |= (1<<bit_index);
}
var get_bit = function(bit){
var arr_index = Math.floor(bit/32);
var bit_index = Math.floor(bit%32);
return bitMap[arr_index]&=(1<<bit_index)
}
this.add = function(key){
if(this.isExist(key)){
return -1;//表示已存在
}
for(let i = 0;i<hash_count;i++){
var hash_value = murmurhash3_32_gc(key,i);
set_bit(Math.abs(Math.floor(hash_value%bit_size)))
}
}
this.isExist = function(key){
for(let i=0;i<hash_count;i++){
var hash_value = murmurhash3_32_gc(key,i);
if(!get_bit(Math.abs(Math.floor(hash_value%bit_size)))){
return false;
}
}
return true;
}
}
var bloom = new BoolmFilter(1000000,0.01);
bloom.add("https://www.jianshu.com/p/28dd26aaf2ee");
bloom.add("https://sh.qihoo.com/pc/detail?realtime");
console.log(bloom.isExist("https://www.jianshu.com/p/28dd26aaf2ee"));//true
console.log(bloom.isExist("https://sh.qihoo.com/pc/detail?realtime"));//true
console.log(bloom.isExist("https://sh.qihoo.com/pc/detail?realtime123123"));//false
//网上搜的哈希函数
function murmurhash3_32_gc (key, seed) {
let remainder = key.length & 3 // key.length % 4
let bytes = key.length - remainder
let h1 = seed
let c1 = 0xcc9e2d51
let c2 = 0x1b873593
let i = 0
while (i < bytes) {
let k1 =
((key.charCodeAt(i) & 0xff)) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24)
++i
k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff
k1 = (k1 << 15) | (k1 >>> 17)
k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff
h1 ^= k1
h1 = (h1 << 13) | (h1 >>> 19)
let h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff
h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16))
}
let k1 = 0
switch (remainder) {
case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16
case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8
case 1: k1 ^= (key.charCodeAt(i) & 0xff)
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff
k1 = (k1 << 15) | (k1 >>> 17)
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff
h1 ^= k1
}
h1 ^= key.length
h1 ^= h1 >>> 16
h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff
h1 ^= h1 >>> 13
h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff
h1 ^= h1 >>> 16
return h1 >>> 0
}