BitMap

今天在做剑指offer习题的时候,我突然看到这样一段话:
前面的代码中,我们都是用一个char型字符表示十进制数字的一位。8个bit的char型字符最多能表示256个字符,而十进制数字只有0-9的10个数字。因此用char型字符串来表示十进制的数字并没有充分利用内存,有一些浪费。有没有更高效的方式来表示大数。
    这段话引导我去思考处理大数问题的更好的解决方法。我认为BitMap应该是种不错的选择
  在所有具有性能优化的数据结构中,我想大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美,
但是在特定的场合下:
①:对10亿个不重复的整数进行排序。
②:找出10亿个数字中重复的数字。
当然我只有普通的服务器,就算2G的内存吧,在这种场景下,我们该如何更好的挑选数据结构和算法呢?

一:问题分析
这年头,大牛们写的排序算法也就那么几个,首先我们算下放在内存中要多少G: (10亿 * 32)/(1024*1024*1024*8)=3.6G,可怜
的2G内存直接爆掉,所以各种神马的数据结构都玩不起来了,当然使用外排序还是可以解决问题的,由于要走IO所以暂时剔除,因为我们
要玩高性能,无望后我们想想可不可以在二进制位上做些手脚?  
比如我要对{1,5,7,2}这四个byte类型的数字做排序,该怎么做呢?我们知道byte是占8个bit位,其实我们可以将数组中的值作为bit位的

key,value用”0,1“来标识该key是否出现过?下面看图:


从图中我们精彩的看到,我们的数组值都已经作为byte中的key了,最后我只要遍历对应的bit位是否为1就可以了,那么自然就成有序数组了。

可能有人说,我增加一个13怎么办?很简单,一个字节可以存放8个数,那我只要两个byte就可以解决问题了。


可以看出我将一个线性的数组变成了一个bit位的二维矩阵,最终我们需要的空间仅仅是:3.6G/32=0.1G即可,要注意的是bitmap排序不

是N的,而是取决于待排序数组中的最大值,在实际应用上关系也不大,比如我开10个线程去读byte数组,那么复杂度为:O(Max/10)。

浅析对应关系: 
1.求十进制0-N对应在数组a中的下标: 
十进制0-31,对应在a[0]中,先由十进制数n转换为与32的余可转化为对应在数组a中的下标。比如n=24,那么 n/32=0,则24对应在数组a中的下标为0。又比如n=60,那么n/32=1,则60对应在数组a中的下标为1,同理可以计算0-N在数组a中的下标。 
2.求0-N对应0-31中的数: 
十进制0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32求得对应0-31中的数。 
3.利用移位0-31使得对应32bit位为1. 

二:代码

我想bitmap的思想大家都清楚了,这一次又让我们见证了二进制的魅力,当然这些移位都是位运算的工作了,熟悉了你就玩转了。

1:Clear方法(将数组的所有bit位置0)

    比如要将当前4对应的bit位置0的话,只需要1左移4位取反与B[0] & 即可。


2:Add方法(将bit置1操作)

  同样也很简单,要将当前4对应的bit位置1的话,只需要1左移4位与B[0] | 即可。


#include <stdio.h>  
  
#define BITSPERWORD 32  
#define SHIFT 5  
#define MASK 0x1F  
#define N 10000000  
  
int a[1 + N/BITSPERWORD];//申请内存的大小  
  
//set 设置所在的bit位为1  
//clr 初始化所有的bit位为0  
//test 测试所在的bit为是否为1  
  
void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }  
void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }  
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }  
  
int main()  
{   int i;  
    for (i = 0; i < N; i++)  
        clr(i);    
    while (scanf("%d", &i) != EOF)  
        set(i);  
    for (i = 0; i < N; i++)  
        if (test(i))  
            printf("%d\n", i);  
  
    return 0;  
}  

解析本例中的void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); } 
1.i>>SHIFT: 
其中SHIFT=5,即i右移5为,2^5=32,相当于i/32,即求出十进制i对应在数组a中的下标。比如i=20,通过i>>SHIFT=20>>5=0 可求得i=20的下标为0; 
2.i & MASK: 
其中MASK=0X1F,十六进制转化为十进制为31,二进制为0001 1111,i&(0001 1111)相当于保留i的后5位。 
比如i=23,二进制为:0001 0111,那么 
        0001 0111 &  0001 1111 = 0001 0111 十进制为:23 
比如i=83,二进制为:0000 0000 0101 0011,那么 
        0000 0000 0101 0011  &   0000 0000 0001 1111 = 0000 0000 0001 0011 十进制为:19 
i & MASK相当于i%32。 
3.1<<(i & MASK) 
相当于把1左移 (i & MASK)位。 比如(i & MASK)=20,那么i<<20就相当于: 
0000 0000 0000 0000 0000 0000 0000 0001 >>20 =0000 0000 0000 1000 0000 0000 0000 0000 
4.void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }
等价于: 
void set(int i) 

    a[i/32] |= (1<<(i%32)); 
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值