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 >>3 结果 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(); } } }
?
结果显示
62 8198 8192 4096 00000 00000 00000 00000 00000 01111 1000000 00000 00000 00010 00000 00001 1000000 00000 00000 00010 00000 00000 0000000 00000 00000 00001 00000 00000 00添加数据109是否包含 77 这个数据 ? true是否包含 109 这个数据 ? true删除数据77是否包含 77 这个数据 ? false?
总结
- BitMap的时间复杂度是O(1)
- BitMap的优点(1)可以解决数据判重的问题(2)可以对没有重复的数据进行排序(3)存储巧妙,节约空间,效率高
- BitMap的缺点(1)数据不允许重复。因为只有0和1没有其他了(2)数据量少的时候相对于普通的hash没有优势(因为存储数据量小,我们直接用正常的map或者其他数据结构存储就行)
-
- (3)无法处理字符串(4)无法解决Hash冲突(因此我们的前提条件就是数据不重复)
-
原文地址:https://www.cnblogs.com/leaveast/p/12052873.html