一个简单的问题
已知有n整个整数,这些整数的范围是[0, 100],请你设计一种数据结构,使用数组存储这些数据,并提供两种方法,分别是addMember 和 isExist,下面是这种数据结构的类的定义,请你实现它的两个方法
function FindClass(){
var datas = []; //存储数据
// 加入一个整数 member
this.addMember = function(member){
datas.push(member);
};
// 判断member是否存在
this.isExist = function(member){
for(var i = 0;i < datas.length; i++){ if(datas[i]==member){
return true;
}
}
return false;
};
// 判断member是否存在 方法2
this.isExist = function(member){
if(datas.indexOf(member) >= 0){
return true;
}
return false;
};
}
var fc = new FindClass();
var arr = [0, 3, 5, 6, 9, 34, 23, 78, 99];
for(var 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));
更快的方法
现在,题目难度升级,前面的实现虽然满足要求,但是速度慢,不论使用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(var i = 0;i < datas.length;i++){
datas[i] = 0;
}
// 加入一个整数 member
this.addMember = function(member){
datas[member] = 1;
};
// 判断member是否存在
this.isExist = function(member){
if(datas[member] == 1){
return true;
}
return false;
};
}
var fc = new FindClass();
var arr = [0, 3, 5, 6, 9, 34, 23, 78, 99];
for(var 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));
1.2 中实现的算法,已经很快,但是却面临一个新的问题,如果数据非常多,多达1个亿,每个整数是4个字节,那么一亿个整数数就是4亿个字节,1024字节是1kb,1024kb是1M,4亿个字节就381M的内存空间。
如果没有这么多内存该怎么办?我们需要一种数据压缩的算法,用很少的空间来表示这一亿个数的存在与否。
1.3 更节省空间的算法
1.2 中实现的算法,已经很快,但是却面临一个新的问题,如果数据非常多,多达1个亿,每个整数是4个字节,那么一亿个整数数就是4亿个字节,1024字节是1kb,1024kb是1M,4亿个字节就381M的内存空间。
如果没有这么多内存该怎么办?我们需要一种数据压缩的算法,用很少的空间来表示这一亿个数的存在与否。
2. 街边的路灯
街边有8栈路灯,编号分别是1 2 3 4 5 6 7 8 ,其中2号,5号,7号,8号路灯是亮着的,其余的都处于不亮的状态,请你设计一种简单的方法来表示这8栈路灯亮与不亮的状态。
一个非计算机专业的人看到这个问题,多半会嗤之以鼻,这算什么问题,答案是2578,表示这些编号的路灯是亮的,其余的都是不亮的。
而一个计算机专业的人看到这个问题,应该回答说75,因为75的二进制表示是0 1 0 0 1 0 1 1,恰好与路灯亮灭的状态相对应
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位设置为1,二进制表示就是
00000000 00000000 00000000 00000001
此时,value = 1
如果又增加了一个3,就把value的二进制的第4为设置为1,二进制表示就是
00000000 00000000 00000000 00001001
此时,value = 9, 9可以表示 0 和 3都存在,一个整数,其实可以表示031的存在与否,如果我创建一个大小为10的数组,数组里存储整数,那么这个数组就可以表示0319的存在与否
datas[0] 表示0~31存在与否
datas[1] 表示32~63存在与否
....
datas[9] 表示288~319存在与否
通过这种方式,就可以把空间的使用降低到原来的32分之一,存储一亿个整数的存在与否,只需要12M的内存空间,那么该如何对整数的二进制位进行操作呢。
3. 位运算
内存中的数据,最终的存储方式都是二进制,位运算就是对整数在内存中的二进制位进行操作。
3.1 按位与 &
两个整数进行按位与运算,相同二进制位的数字如果都是1,则结果为1,有一个位为0,则结果为0, 下面是 3 & 7的计算过程
二进制 整数
0 1 1 3
1 1 1 7
0 1 1 3
3 & 7 = 3
3.2 按位或 |
两个整数进行按位或运算,相同二进制位的数字如果有一个为1,则结果为1,都为0,则结果为0, 下面是 5 | 8 的计算过程
二进制 整数
0 1 0 1 5
1 0 0 0 8
1 1 0 1 13
5 | 8 = 13
3.3 左移 <<
二进制向左移动 n 位,在后面添加 n 个0
下面是 3<<1 计算过程。
二进制 整数
1 1 3
1 1 0 6
3<<1 = 6
3.4 小试牛刀
一组数,内容以为 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);
程序输出结果为1573384bitmap
新的实现方式
经过前面一系列的分析和位运算学习,现在,我们要重新设计一个类,实现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] & 1<<bit_index,如果结果不为0,就说明memeber存在
新的实现方法
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); // 决定在数组中的索引
var bit_index = member % 32; // 决定在整数的32个bit位的哪一位上
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; // 决定在整数的32个bit位的哪一位上
var value = bit_arr[arr_index] & 1<<bit_index;
if(value != 0){
return true;
}
return false;
};
}
var bit_map = new BitMap(4);
var arr = [0, 3, 5, 6, 9, 34, 23, 78, 99];
for(var i = 0;i < arr.length;i++){
bit_map.addMember(arr[i]);
}
console.log(bit_map.isExist(3));
console.log(bit_map.isExist(7));
console.log(bit_map.isExist(78));
大数据排序
有多达10亿无序整数,已知最大值为15亿,请对这个10亿个数进行排序。
BitMap存储最大值为15亿的集合,只需要180M的空间,空间使用完全可以接受,至于速度,存储和比较过程中的位运算速度都非常快,第一次遍历,将10亿个数都放入到BitMap中,第二次,从0到15亿进行遍历,如果在BitMap,则输出该数值,这样经过两次遍历,就可以将如此多的数据排序。
为了演示方便,只用一个很小的数组,[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(var i = 0;i < arr.length;i++){
bit_map.addMember(arr[i]);
}
for(var i = 0;i <= 99;i++){
if(bit_map.isExist(i)){
sort_arr.push(i);
}
}
console.log(sort_arr);
布隆过滤器
前面所讲的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不存在。
对于一个布隆过滤器,有两个参数需要设置,一个是预估的最多存放的数据的数量,一个是可以接受的冲突率。
hash函数
哈希函数就是将某一不定长的对象映射为另一个定长的对象,如果你对这个概念感到困惑,你就换一个理解方法,你给hash函数传入一个字符串,它返回一个整数。为了实现一个布隆过滤器,我们需要一个好的hash函数,计算快,冲突又少,很幸运,有很多开源的hash算法,我在github上找到了一个murmurhash的实现,代码如下
最终完整的实现代码
function murmurhash3_32_gc(key, seed) {
var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i;
remainder = key.length & 3; // key.length % 4
bytes = key.length - remainder;
h1 = seed;
c1 = 0xcc9e2d51;
c2 = 0x1b873593;
i = 0;
while (i < bytes) {
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);
h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
}
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;
}
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_ount = Math.ceil(Math.log(2) * (bit_size / max_count));
// 设置位的值
var set_bit = function (bit) {
var arr_index = Math.floor(bit / 31);
var bit_index = Math.floor(bit % 31);
bitMap[arr_index] |= (1 << bit_index);
};
// 读取位的值
var get_bit = function (bit) {
var arr_index = Math.floor(bit / 31);
var bit_index = Math.floor(bit % 31);
return bitMap[arr_index] &= (1 << bit_index);
};
// 添加key
this.add = function (key) {
if (this.isExist(key)) {
return -1; //表示已经存在
}
for (var i = 0; i < hash_ount; i++) {
var hash_value = murmurhash3_32_gc(key, i);
set_bit(Math.abs(Math.floor(hash_value % (bit_size))));
}
};
// 检测是否存在
this.isExist = function (key) {
for (var i = 0; i < hash_ount; 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 bloomFilter = new BoolmFilter(1000000, 0.01);
bloomFilter.add('https://blog.csdn.net/houzuoxin/article/details/20907911');
bloomFilter.add('https://www.jianshu.com/p/888c5eaebabd');
console.log(bloomFilter.isExist('https://blog.csdn.net/houzuoxin/article/details/20907911'));
console.log(bloomFilter.isExist('https://www.jianshu.com/p/888c5eaebabd'));
console.log(bloomFilter.isExist('https://www.jianshu.com/p/888c5eaebabd435'));
两个集合取交集(普通模式)
两个数组,内容分别为[1, 4, 6, 8, 9, 10, 15], [6, 14, 9, 2, 0, 7],请用BitMap计算他们的交集
var arr1 = [1, 4, 6, 8, 9, 10, 15];
var arr2 = [6, 14, 9, 2, 0, 7];
var intersection_arr = []
var bit_map = new BitMap();
for(var i = 0;i<arr1.length; i++){
bit_map.addMember(arr1[i]);
}
for(var i= 0;i<arr2.length; i++){
if(bit_map.isExist(arr2[i])){
intersection_arr.push(arr2[i]);
}
}
console.log(intersection_arr);
支持负数(困难模式)
课程里所讲的BitMap只能存储大于等于0的整数,请你改造BitMap类,不论正数还是负数,都可以存储并判断是否存在
function SuperBitMap(size){
var positive_bit_map = new BitMap.BitMap(size);
var negative_bit_map = new BitMap.BitMap(size);
this.addMember = function(member){
if(member >= 0){
positive_bit_map.addMember(member);
}else{
negative_bit_map.addMember(member);
}
};
this.isExist = function(member){
if(member >= 0){
return positive_bit_map.isExist(member);
}else{
return negative_bit_map.isExist(member);
}
};
}
var arr = [1, 3 ,-6, -8, 8, 9];
var super_bm = new SuperBitMap();
for(var i =0;i<arr.length;i++){
super_bm.addMember(arr[i]);
}
console.log(super_bm.isExist(-8));
console.log(super_bm.isExist(8));
console.log(super_bm.isExist(9));
console.log(super_bm.isExist(-6));
console.log(super_bm.isExist(-5));
查找不重复的数
有一个数组,内容为[1, 3, 4, 5, 7, 4, 8, 9, 2, 9],有些数值是重复出现的,请你改造BitMap类,增加一个isRepeat(member)方法,判断member是否重复出现,并利用该方法找出数组中不重复的数。
用一个bit位可以表示一个数是否存在,但无法表示这个数是否重复,因为一个bit位只有两个状态,恰好对应数的存在与否,那么我们可以用连个bit位来表示一个数的存在与否以及是否重复,这样,一个32位整数,可以表示16个数的存在与否以及重复与否。以0为例,addMember(0)时,第0个bit位置为1,二进制表示可以写成
00000000 00000000 00000000 00000011
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 / 16); // 决定在数组中的索引
var bit_index = member % 16; // 决定在整数的32个bit位的哪一位上
if(!this.isExist(member)){
bit_arr[arr_index] = bit_arr[arr_index] | 1<<bit_index*2;
}else{
bit_arr[arr_index] = bit_arr[arr_index] | 1<<(bit_index*2+1);
}
};
this.isExist = function(member){
var arr_index = Math.floor(member / 16); // 决定在数组中的索引
var bit_index = member % 16; // 决定在整数的32个bit位的哪一位上
var value = bit_arr[arr_index] & 1<<bit_index*2;
if(value != 0){
return true;
}
return false;
};
this.isRepeat = function(member){
var arr_index = parseInt(member / 16); // 决定在数组中的索引
var bit_index = member % 16; // 决定在整数的32个bit位的哪一位上
var value = bit_arr[arr_index] & 1<<(bit_index*2 + 1);
if(value != 0){
return true;
}
return false;
};
}
var arr_1 = [1, 3, 4, 5, 7, 4, 8, 9, 2, 9];
var bm = new BitMap(2);
for(var i = 0; i < arr_1.length; i++){
bm.addMember(arr_1[i]);
}
var arr = []
for(var i = 0; i <=9; i++){
if(!bm.isRepeat(i)){
arr.push(i);
}
}
console.log(arr);