BitMap
抛砖引玉
-
首先,我们思考一个问题:如何在3亿个整数(0~2亿)中判断某一个数是否存在?现在只有一台机器,内存只有500M
-
这个问题像不像我们之前提到过的一个在0-10个数中,判断某一个数是否存在的问题呢?当时我们采取的做法是,建立一个长度是11的数组,下标从0开始,如果1存在则data[1] = 1,数字作为数组的下标,若该数字存在则在data[数字] = 1,将其赋值为1。那么我们这个是否可以这么做呢?
-
明显不行。为什么呢?因为我们如果判断2亿个数字是否存在,建立一个2亿长度的数组,疯了么?想想就知道不可能。那么我们应该怎么做呢?
-
这时候我们就需要变换我们的思路了,一个int类型整型,我们占用4个字节,一个字节我们占用8位:1 byte = 8 bit4 byte = 32 bit那么,我们如果将这2亿个数据不按照int的形式存储,而是按照位的形式存储,我们原来存储一个数字(4个字节)现在不就是可以存储32个数字(4个字节 = 32位)了么。秒啊~
什么是BitMap
-
位图(BitMap),又称作栅格图或者点阵图,是使用像素阵列来表示的图像---------来自百度百科。
-
明显上面说的什么,我们也不清楚,反正很高深了就是,其实我觉得简单理解就是我们用位(bit)来操作数据,以此来操作大数据量
-
至于我们为什么用位来操作呢?因为计算机最小的单位就是bit(位)
类型的基础讲解
常用的数据类型和位的转换
1 byte = 8 bit
1 int = 4 byte = 32 bit
1 float = 4 byte = 32 bit
1 long = 8 byte = 64 bit
1 char = 2 byte = 16 bit
-
我们这里就不做详细的解释,只要记住:1 byte = 8 bit,即1字节 = 8位
常用的位操作运算符
-
我们这里讲述0. 常用的6种常用的位运算符(1)按位或(2)按位与(3)按位异或(4)求反(5)左移运算(6)右移运算
按位或运算
-
按位或运算符,记做“|”,是一个双目运算符。
-
简单来说就是二进制的位中只要有一个是1,其计算结果就是1,否则就是0
-
举例说明:
11 | 7
11 0000 0000 0000 0000 0000 0000 0000 1011 | 7 0000 0000 0000 0000 0000 0000 0000 0111 结果 0000 0000 0000 0000 0000 0000 0000 1111 11 | 7 = 1011 | 0111 = 1111 = 15
按位与运算
-
按位与运算符,记做“&”,是一个双目运算符。
-
简单来说就是二进制的位中只有都是1,才会是1,否则就是0
-
举例说明:11 & 7
11 0000 0000 0000 0000 0000 0000 0000 1011 & 7 0000 0000 0000 0000 0000 0000 0000 0111 结果 0000 0000 0000 0000 0000 0000 0000 0011 11 &7 = 1011 | 0111 = 0011 = 3
按位异或运算
-
按位异或运算符,记做“^”,是一个双目运算符。
-
简单来说就是二进制的位中只要两个数字不一样就是1,否则就是0
-
举例说明:
11 ^ 7
11 0000 0000 0000 0000 0000 0000 0000 1011 ^ 7 0000 0000 0000 0000 0000 0000 0000 0111 结果 0000 0000 0000 0000 0000 0000 0000 1100 11 &7 = 1011 | 0111 = 1100 = 12
按位异或运算
public class Main1 { public static void main(String[] args) { System.out.println("11 | 7 的结果是 : "+(11|7)); System.out.println("11 & 7 的结果是 :"+(11&7)); System.out.println("11 ^ 7 的结果是 :"+(11^7)); } } //----------------------------输出如下------------------------------ 11 | 7 的结果是 : 15 11 & 7 的结果是 :3 11 ^ 7 的结果是 :12
按位求反运算
-
按位求反运算符,记做“~”,是一个单目运算符。
-
简单来说就是二进制的位中,原来是1,结果就是0,原来是0,结果就是1
左移运算
-
左移运算符“<<”是一个双目运算符,其功能就是把"<<"左边的运算数的各个二进位全部左移若干位。
-
举例说明:5 << 4
5 0000 0000 0000 0000 0000 0000 0000 0101 <<4 结果 0000 0000 0000 0000 0000 0000 0101 000 5 << 4 = 0101 << 4 = 0101 0000 = 5* 2^4 = 5*16 =80
右移运算
-
右移运算符 ">>" 是一个双目运算符,其功能就是把">>"左边的运算数的各个二进位全部右移若干位。
-
举例说明:16 >> 3
16 0000 0000 0000 0000 0000 0000 0001 0000 结果 0000 0000 0000 0000 0000 0000 0000 0010 16 >> 3 = 0001 0000 >> 3 = 0010 = 16 / 2^3 = 16/8 =2
如何巧妙的运用
-
a >> x
这个就可以记做是:a / 2 ^ x比如说:8 >> 2 = 8 / 2 ^ 2 = 8 / 4 = 2
-
a << x
这个就可以记做是: a * 2 ^ x比如说: 8 << 2 = 8 * 2 ^ 2 = 8 * 4 = 32
-
a % 2^n
这个就可以记做是:a & 2^n-1比如说:7 % 4 = 7 & (4-1) = 3
实际问题分析
问题描述
-
现在我们假设我们有64个数字,最大的数字就是64,那么我们用Bitmap的思想应该怎么解决呢?
分析
-
首先我们这里用int的数组来进行存储,一个int是4个字节,1个字节是8位,因此一个int型的数字可以存32位
-
我们这里最大的数是64
数组 | 存储位数 | 能存多少 |
---|---|---|
data[0] | 0-31 | 32位 |
data[1] | 32-63 | 32位 |
data[2] | 64-95 | 32位 |
由此可以推到出来:我们存取n个数字,其中最大的数字是MAX,则需要data[MAX/32 + 1]长度的数组,这里的32是因为1 int = 4 字节 = 32位
假设我们需要判断64是否在列表中(以int数组存储),我们就应该这样来计算
-
第一步我们判断声明多大的int型的数组
因为我们最大的数是64,所以根据我们的公式:data[MAX/32 + 1] ,由此可以计算出,我们声明的数组长度是data[64/32 + 1] = data[3],也就是说我们应该声明数组长度是3,即data[0] data[1] data[2]
-
先定位到数组,判断是第几个数组中存储着
因为我们一个数组长度是1个int,就是32位,查询的数字是64,所以64 / 32 = 2,因此定位到了data[2]
这里除以32是因为,我们以int数组为例,1 int = 32 bit
-
再定位这个元素是数组中的第几位
同理,我们一个数组的长度是一个int,也就是4个字节,32位,查询的数字是64,64 % 32 = 0,因此定位到了我们是存在data[2]数组中的第0位
这里取余32是因为,我们是以int数组为例,1 int = 32 bit
-
此时我们只需要判断data[2]数组中的第0位是否为1,为1表示已经存在,否则就是不存在。
这就是BitMap的算法的核心思想
公式小结
-
我们以int数组为例,一个int占用4个字节,也就是32位,存储数据的最大值是MAX(比如存1-2000,最大值就是2000)
-
判断总共需要多大的int数组 :
MAX/32 + 1
-
判断当前这个数字n在第几个数组中 :
n / 32
-
判断当前这个数字n在数组中的第几位上 :
n % 32
-
我们上面的32就是int占用的位数,可以换成其他的类型,如果用byte数组,则将32换成8,因为1个byte是一个字节,是8位。
计算存储空间
-
假设我们现在要存储2亿个数字,如果直接用int数组来存,一个int是一个数字,则需要:2亿 * 4字节 / 1024 / 1024 = 762M
-
假设我们现在用BitMap的思想存储,也是使用int数组,只不过一个int存储32个数组,则需要:2亿 / 32 + 1(需要开的int数组) * 4字节 / 1024 / 1024 = 23M
看看,是不是空间就是这么省下来了。
实战举例
题目
-
假设我们有序列 2 9 33 12 11 65 14 , 我们开一个int类型的数组,将其存储进去,然后判断其是否存在,并可以实现某一个数字的删除,并用位运算符实现。
分析
一: 首先我们通过上面的三个公式已经可以很快的知道数组定义多大,在哪个数组中放值(哪一个数组,数组中的哪一位),但是这里我们的要求是使用位运算符,这时候结合我们最开始讲的位运算符将其进行简单的转换
-
判断数组定义多大
a / 2^n = a >> ndata[MAX/32 + 1] = data[MAX / 2 ^ 5 + 1] = data[MAX >> 5 + 1]
-
数字n存在哪一个数组中
a / 2^n = a >> ndata[n/32] = data[n / 2 ^ 5] = data[n >> 5]
-
数字n在数组的哪一位
a % 2^n = a & 2^n - 1data[n%32] = data[n % 2 ^ 5] = data[n & (2 ^ 5 -1)] = data[n & 31]
二: 其次我们在存入数组中需要将数组的第loc位由0变为1
将下标为loc的位由0变为1,可以把1左移loc位,然后使用或操作(只要有1个为1就是1)
所以新增元素公式就是: data[X] = data[X] | (1 << loc),loc为当前数组的哪一位,data[X]就是当前的数组
三: 下一个问题就是我们需要查找一个数字是否已经存在
这时候我们就可以考虑&运算符,即都是1才是1,否则就是0判断下标loc的位是否已经存在,可以把1左移loc位,然后做与操作(都为1才是1,否则是0)
如下图,判断loc位上是否存在:(下图表示已经存在)
如下图,判断loc位上是否存在:(下图表示不存在)
所以查找元素公式就是:0 == data[X] & 1 << loc,loc为当前数组的哪一位 ,这时候用0判断是因为我们除了移位过去的那位上的数字是1,其他位上的数字都是0,所以如果最后结果是0,表示就是不存在,否则就是存在
四: 最后一个问题就是我们要删除一个数字呢?就是将这个数字由1变为0(这里我们假设删除的数字是肯定存在的,不允许做不存在就删除)
这时候考虑我们异或操作:相同则为0,不同则为1,那如果我当前位上存在则肯定是1,如果我拿一个当前位是1的数和其做异或操作不就可以了假设我们需要删除下标是lco上的数我们的异或操作是相同为0,不同为1,所以即使数组原来位置上有1,我们也不用害怕,因为原来位子上是1,但是我们1进行左移loc后,除了loc上的数是1,其他位上的数肯定是0,参考上面的图解
所以删除元素公式:data[X] = data[X] ^ (1 << loc),loc为当前数组的哪一位
代码实现
package com.demo.bitsmap; public class Main2BitsMap { private int[] vals; public Main2BitsMap(int size) { this.vals = new int [size]; } public static void main(String[] args) { int [] t = {1,2,3,4,5,33,34,45,77,108}; Main2BitsMap map = new Main2BitsMap((108>>5)+1); for (int i :t) { map.add(i); } map.print(); map.printBirany(); System.out.println("添加数据109"); map.add(109); System.out.println("是否包含 77 这个数据 ? " +map.find(77)); System.out.println("是否包含 109 这个数据 ? " +map.find(109)); System.out.println("删除数据77"); map.delete(77); System.out.println("是否包含 77 这个数据 ? " +map.find(77)); } public void add(int t) { int index = t>>5; int loc = t&31; vals[index] = vals[index] | 1 << loc; } public boolean find(int t) { int index = t>>5; int loc = t&31; int result = (vals[index]>>loc ) &1; return result ==0 ?false:true; } public boolean delete(int t ) { int index = t>>5; int loc = t&31; if(!find(t)) { return false; } vals[index] = vals[index] ^ (1<<loc); return true; } public void print() { for (int i : vals) { System.out.print(i+" "); } } public void printBirany() { int index =0; System.out.println(); for (int i : vals) { for (int j = 31; j >=0; j--) { System.out.print((i>>j)&1 ); index++; if(index==5) { System.out.print(" "); index=0; } } index =0; System.out.println(); } } }