数据结构与算法学习三(基于JavaScript)----BitMap

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
  }

猜你喜欢

转载自blog.csdn.net/guo187/article/details/88311314