海量数据处理算法—BitMap(Bitmap Sort)

一、背景

来源:

在Jon Bentley的Programming Pearls一书中,第一列介绍了排序问题。当我们更多地了解问题并清楚地定义它的约束时,解决方案从使用磁盘的合并排序(Merge Sort)转换为更为有效的位图排序(Bitmap Sort)。

所谓的Bitmap Sort 就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。

该算法使用位图(或位向量)来表示一组有限的不同整数。例如,如果我们有一个0-5的整数范围,我们可以使用一个6位数组来表示它,例如:
[2,3,5]变为0 0 1 1 0 1 
[1,3,4]变为0 1 0 1 1 0

再看个简单的问题:

假如给你20亿个非负数的int型整数,然后再给你一个非负数的int型整数 t ,让你判断t是否存在于这20亿数中,你会怎么做呢?

有人可能会用一个int数组,然后把20亿个数给存进去,然后再循环遍历一下就可以了。

想一下,这样的话,时间复杂度是O(n),所需要的内存空间

4byte * 20亿,一共需要80亿个字节,

大概需要8GB的内存空间,显然有些计算机的内存一次是加载不了这么这么多的数据的。

这个时候,Jon Bentley提出了他的思路:

我们在使用byte,int,short,long等这些数据类型在存储数据的时候,他们最小的都要占用一个字节的内存,也就是8个bit,也就是说,最小的操作单位是8个bit。根本就没有可以一个一个bit位操作的数据类型啊。

在C/C++的bitMaP实现中,它采用的是用一个int数据来进行存储的。一个int占用84个字节,即32bit,所以一个int可以存储32个数。例如 arr 是一个int类型的数组,则 arr[0]可以存 0 ~ 31,arr[1]可以存32 ~63,以此类推。

 

二、Bit Map的基本思想

32位机器上,一个整形,比如 int a; 在内存中占32bit,可以用对应的32个bit位来表示十进制的0-31个数,bitmap算法利用这种思想处理大量数据的排序与查询。

优点:

  • 效率高,不许进行比较和移位
  • 占用内存少,比如N=10000000;只需占用内存为N/8 = 1250000Bytes = 1.2M,如果采用int数组存储,则需要38M多

缺点:

  • 无法对存在重复的数据进行排序和查找

示例演示:

我们都知道,一个二进制位,有0和1两种状态,所以说,其实我们是可以用一个二进制位来代表一个int型的数是否存在的。例如对于1,3,5,7这四个数,如果存在的话,则可以这样表示:

首先第一个元素是1,那么就把1对应的位置为1(可以这样操作 p+(i/8)|(0x01<<(i%8)) 当然了这里的操作涉及到big-endian和Little-endian的情况,这里为Big-endian:最高位字节存储在最低的内存地址处),因为是从零开始的,所以要把第五位置为一(如下图):

1代表这个数存在,0代表不存在。例如表中01010101代表1,3,5,7存在,0,2,4,6不存在。

那如果8,10,14也存在怎么存呢?如图,8,10,14我们可以存在第二个字节里

附加:字节顺序,又称端序尾序(英语:Endianness

字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称大端序;反之则称小端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。

小端序(英:little-endian)情况

思想比较简单,关键是十进制和二进制bit位需要一个 map 映射表,把10进制映射到bit位上。

 

三、map映射表

假设需要排序或者查找的总数N=10000000,那么我们需要申请的内存空间为 int a[ N/32 + 1 ].其中a[0]在内存中占32位,依此类推:

bitmap表为: 
a[0]--------->0-31 (一个int型占4个字节,共占32位。)
a[1]--------->32-63 
a[2]--------->64-95 
a[3]--------->96-127 
.......... 
那么十进制数如何转换为对应的bit位,下面介绍用位移将十进制数转换为对应的bit位。 

 

四、位移转换

申请一个int一维数组,那么可以当作为列为32位的二维数组,

               |                           32位                                       |

int a[0]    |0000000000000000000000000000000000000|

int a[1]    |0000000000000000000000000000000000000|

………………

int a[N]   |0000000000000000000000000000000000000|

例如十进制0,对应在a[0]所占的bit为中的第一位: 0000000000000000000000000000000
0-31:对应在a[0]中 
i=0                            00000000000000000000000000000000      (i:实际对应的十进制)
temp=0                     00000000000000000000000000000000        (temp=i%32,n=i/32,将a[n]中answer二进制的第temp位"置1")                                                                                                      


i =1                            00000000000000000000000000000001 
temp=1                     00000000000000000000000000000001 
answer=2                 00000000000000000000000000000010 


i =2                            00000000000000000000000000000010 
temp=2                     00000000000000000000000000000010 
answer=4                 00000000000000000000000000000100 

i =30                               00000000000000000000000000011110 
temp=31                         00000000000000000000000000011110 

 


i =31                               00000000000000000000000000011111 
temp=31                         00000000000000000000000000011111 
answer=-2147483648    10000000000000000000000000000000 

32-63:对应在a[1]中 
i =32                            00000000000000000000000000100000  


answer=1                    0000000000000000000000000000000


i =33                            00000000000000000000000000100001 
temp=1                       00000000000000000000000000000001 
answer=2                    00000000000000000000000000000010 


i =34                            00000000000000000000000000100010 
temp=2                        00000000000000000000000000000010 
answer=4                    00000000000000000000000000000100 


i =61                              00000000000000000000000000111101 
temp=29                        00000000000000000000000000011101 
answer=536870912      01000000000000000000000000000000


i =62                                00000000000000000000000000111110 
temp=30                          00000000000000000000000000011110 
answer=1073741824      01000000000000000000000000000000 


i =63                                00000000000000000000000000111111 
temp=31                          00000000000000000000000000011111 
answer=-2147483648     10000000000000000000000000000000
 

如何给数组中添加数值?

浅析上面的对应表,分三步: 

(1) 求十进制数 0-N 对应的在数组 a 中的下标(求a[N]中的N的大小)

index_loc = N / 32即可,index_loc即为n对应的数组下标。例如n = 76, 则loc = 76 / 32 = 2,因此76在a[2]中。

(i/32用按位运算表示i>>5)

(2) 求十进制数0-N对应的bit位

bit_loc = N % 32即可,例如 n = 76, bit_loc = 76 % 32 = 12。 (i%32 用按位运算表示 i & 0x1F)

(3) 利用移位0-31使得对应的32bit位为1

找到对应0-31的数为M, 左移M位:2^M. 然后置1.

将他们合起来就是(注意“|=”.)

a[i>>5] |= (1<<(i & 0x1F));     //等价于a[i/32] |= (1<<(i%32));

附上代码

#define INT_BITS sizeof(int)
#define SHIFT 5 // 2^5=32
#define MASK 0x1f // 2^5=32
#define MAX 1024*1024*1024 //max number
int bitmap[MAX / INT_BITS];

//设置第i位
void set(int i) {
bitmap[i >> SHIFT] |= 1 << (i & MASK);
}

//获取第i位,测试所在的bit为是否为1
int test(int i) {
return bitmap[i >> SHIFT] & (1 << (i & MASK));
}

//清除第i位
int clear(int i) {
return bitmap[i >> SHIFT] & ~(1 << (i & MASK));
}

//初始化所有的bit位为0
void clr(int i) {        
	a[i>>SHIFT] &= ~(1<<(i & MASK)); 
}

由此我们计算10000000个bit占用的空间:

1byte = 8bit

1kb = 1024byte

1mb = 1024kb

占用的空间为:10000000/8/1024/1024mb。

大概为1mb多一些。

 

五、问题实例

1、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数

解法一:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存2^32 * 2 bit=1 GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。

解法二:也可采用与第1题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。”

实现:

// TestWin32.cpp : Defines the entry point for the console application.
//代码来自 https://blog.csdn.net/hguisu/article/details/7880288#commentBox
#include "stdafx.h"
 
#include<memory.h>  
 
//用char数组存储2-Bitmap,不用考虑大小端内存的问题  
unsigned char flags[1000]; //数组大小自定义   
unsigned get_val(int idx)  { 
//	|    8 bit  |
//	|00 00 00 00|  //映射3 2 1 0
//	|00 00 00 00|  //表示7 6 5 4
//	……
//	|00 00 00 00|
 
	int i = idx/4;  //一个char 表示4个数,
	int j = idx%4;  
	unsigned ret = (flags[i]&(0x3<<(2*j)))>>(2*j);  
	//0x3是0011 j的范围为0-3,因此0x3<<(2*j)范围为00000011到11000000 如idx=7 i=1 ,j=3 那么flags[1]&11000000, 得到的是|00 00 00 00|
	//表示7 6 5 4
   return ret;  
}  
      
unsigned set_val(int idx, unsigned int val)  {  
	int i = idx/4;  
    int j = idx%4;  
    unsigned tmp = (flags[i]&~((0x3<<(2*j))&0xff)) | (((val%4)<<(2*j))&0xff);  
    flags[i] = tmp;  
    return 0;  
}  
unsigned add_one(int idx)  
{  
	if (get_val(idx)>=2) {  //这一位置上已经出现过了??
		return 1;  
	}  else  {  
		set_val(idx, get_val(idx)+1);  
		return 0;  
	}  
}  
      
//只测试非负数的情况;  
//假如考虑负数的话,需增加一个2-Bitmap数组.  
int a[]={1, 3, 5, 7, 9, 1, 3, 5, 7, 1, 3, 5,1, 3, 1,10,2,4,6,8,0};  
      
int main()   {  
	int i;  
    memset(flags, 0, sizeof(flags));  
          
    printf("原数组为:");  
	for(i=0;i < sizeof(a)/sizeof(int); ++i)  {  
		printf("%d  ", a[i]);  
		add_one(a[i]);  
	}  
    printf("\r\n");  
      
    printf("只出现过一次的数:");  
    for(i=0;i < 100; ++i)  {  
		if(get_val(i) == 1)  
			printf("%d  ", i);  
        }  
	printf("\r\n");  
	
	return 0;  
}

2、给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?

解法一:可以用位图/Bitmap的方法,申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。

 

六、 其他

1、扩展 

Bloom filter可以看做是对bit-map的扩展 

2、Bit-Map的应用

  1. 可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下。
  2. 去重数据而达到压缩数据

3、函数库实现

C++的STL中有bitmap类,它提供了很多方法,详见:http://www.cplusplus.com/reference/stl/bitset/

4、位图应用

1.枚举:(1)全组合(2)哈米尔顿距离

2.搜索

3.压缩

 

借鉴资料:

https://mp.weixin.qq.com/s/SVePRbrgyLck8UnLVRVJdA
https://mp.weixin.qq.com/s/pCD1UaNunHfVedkS9Lyp3Q
https://wizardforcel.gitbooks.io/the-art-of-programming-by-july/content/06.07.html
http://letsalgorithm.blogspot.com/2012/02/bitmap-sort.html
https://www.cnblogs.com/chanshuyi/p/5287825.html
https://blog.csdn.net/hguisu/article/details/7880288
https://www.kancloud.cn/kancloud/the-art-of-programming/41618
https://blog.csdn.net/hustyangju/article/details/47022791

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值