1、基础
在Java语言中,一个int类型变量占用4Byte,即32Bit内存空间。
提问:10亿个int类型变量,需要占用多少内存空间?
回答:(10亿 * 4) ÷ (1024 * 1024 * 1024) ≈ 3.73G
如果要对10亿个,不重复的,int类型变量进行排序,将至少需要3.73G内存。
还有更好的办法吗?BitMap算法登场。
2、做个游戏
画8个相邻的小正方形,表示1Byte,即8Bit,给它起个名字叫bitMapArr[0],在它的下方同样画8个小正方形,起名叫bitMapArr[1]。
每个正方形内有一个灯泡,它只能是亮着的(用1表示),或灭着的(用0表示)。
从右至左,从上至下,依次为每个正方形编号:0、1、2 。。。,见下图:
你发现了吗?这些正方形下面的序号,“歪打正着”,恰好可以表示0和正整数。就上图而言,仅仅2个Byte就能表示0到15这16个整数。正常情况下,需要16*4,即64个Byte。
我在最初接触BitMap算法时,一度被正方形里的值,即0或1,和正方形下面的序号搞晕了。通过这个小游戏可以很形象地说清楚他们的区别了。
3、排序
假设要对3、1、2、7这几个数进行排序。如果能像下面描述的那样操作,不就达到排序的效果了吗?
- 依次将序号为3、1、2、7的正方形里的灯点亮,即赋1。其余正方形里的灯默认为熄灭,即赋0。
- 从右到左,从上到下,遍历这两排正方形,仅把亮灯的正方形下面的序号依次记录下来,就是排序后的结果。
4、Java代码
/**
* 代码改编自:https://www.cnblogs.com/zldmy/p/11565991.html
*/
public class BitMapV1 {
/**
* 字节数组,即上图中若干8个相邻的小正方形
*/
private byte[] bitMapArr;
/**
* 为描述方便,假设要对若干个整数进行排序。
* 此处,capacity可以理解为,待排序的若干个整数中,最大的那个整数。
* 举例:对1、9这两个数排序,capacity应该赋值为9,即最大值,而不是2,即有2个整数待排序
*/
private int capacity;
public BitMapV1(int capacity) {
this.capacity = capacity;
// 右移3位相当于除以2^3,即除以8,下面语句等效于capacity/8 + 1
int len = (capacity >> 3) + 1;
System.out.println("### 字节数组的大小=" + len);
bitMapArr = new byte[len];
}
/**
* 将某个整数添加到字节数组中
* 形象地说,就是已知一个“序号”,把对应的正方形里的灯点亮
*/
public void add(int num) {
// num这个数,存在于第0排,还是第1排
int index = num >> 3;
// num这个数,在某排的,从右边数,第几个“序号”
int position = num & 0x07;
// 将“序号”对应的正方形里的灯点亮,即赋值为1
bitMapArr[index] |= 1 << position;
}
/**
* 是add(int num)方法的逆过程
* 形象地说,就是已知一个“序号”,把对应的正方形里的灯熄灭
*/
public void clear(int num){
int index = num >> 3;
if (index >= bitMapArr.length) {
return;
}
int position = num & 0x07;
bitMapArr[index] &= ~(1 << position);
}
/**
* num这个数,是否存在于某个小正方形对应的“序号”
*/
public boolean isExist(int num){
int index = num >> 3;
if (index >= bitMapArr.length) {
return false;
}
int position = num & 0x07;
int result = bitMapArr[index] & 1 << position;
return result != 0;
}
public byte[] getBitMapArr() {
return bitMapArr;
}
/**
* 以二进制字符串形式表示一个byte
* 形象地说,就是亮灯的显示为1,熄灭的显示为0
*/
public static String getBit(byte by){
StringBuffer sb = new StringBuffer();
sb.append((by>>7)&0x1)
.append((by>>6)&0x1)
.append((by>>5)&0x1)
.append((by>>4)&0x1)
.append((by>>3)&0x1)
.append((by>>2)&0x1)
.append((by>>1)&0x1)
.append((by>>0)&0x1);
return sb.toString();
}
/**
* 打印整个字节数组
* 形象地说,就是亮灯的显示为1,熄灭的显示为0
*/
public void print() {
for (byte b : bitMapArr) {
System.out.print(getBit(b));
System.out.println();
}
}
}
import org.junit.Assert;
import org.junit.Test;
public class BitMapV1Test {
/**
* 假设要对3、1、2、7进行排序
*/
@Test
public void test_01() {
System.out.println("### 待排序:3 1 2 7");
// 7是指待排序中的最大值
BitMapV1 bitMapV1 = new BitMapV1(7);
int length = bitMapV1.getBitMapArr().length;
// 用脚指头算,也知道1个Byte,即8Bit,就够用了
Assert.assertEquals(1, length);
bitMapV1.add(3);
bitMapV1.print();
bitMapV1.add(1);
bitMapV1.print();
bitMapV1.add(2);
bitMapV1.print();
bitMapV1.add(7);
bitMapV1.print();
// 看看哪些灯亮着,把它们的序号依次记录下来,即排序完成后的结果
System.out.print("### 排序后的结果:");
for (int i = 0; i < 8; i++) {
if (bitMapV1.isExist(i)) {
System.out.print(i + " ");
}
}
System.out.println();
// 把上面点亮的灯,逐一熄灭
System.out.println("### 把上面点亮的灯,逐一熄灭:");
bitMapV1.clear(3);
bitMapV1.print();
bitMapV1.clear(1);
bitMapV1.print();
bitMapV1.clear(2);
bitMapV1.print();
bitMapV1.clear(7);
bitMapV1.print();
// 测试一个不存在的整数
bitMapV1.clear(9999999);
}
/**
* 假设要对3、1、2、13进行排序
*/
@Test
public void test_02() {
System.out.println("### 待排序:3 1 2 13");
BitMapV1 bitMapV1 = new BitMapV1(13);
int length = bitMapV1.getBitMapArr().length;
// 由于13大于第0排最大的序号7了,需要2个Byte,即16Bit
Assert.assertEquals(2, length);
bitMapV1.add(3);
bitMapV1.print();
bitMapV1.add(1);
bitMapV1.print();
bitMapV1.add(2);
bitMapV1.print();
bitMapV1.add(13);
bitMapV1.print();
// 看看哪些灯亮着,把它们的序号依次记录下来,即排序完成后的结果
System.out.print("### 排序后的结果:");
for (int i = 0; i < 16; i++) {
if (bitMapV1.isExist(i)) {
System.out.print(i + " ");
}
}
System.out.println();
// 把上面点亮的灯,逐一熄灭
System.out.println("### 把上面点亮的灯,逐一熄灭:");
bitMapV1.clear(3);
// 注意:两行为一组了
bitMapV1.print();
bitMapV1.clear(1);
bitMapV1.print();
bitMapV1.clear(2);
bitMapV1.print();
bitMapV1.clear(13);
bitMapV1.print();
}
}
另一个实现版本:
package com.jjk;
/**
* 改编自:https://stackoverflow.com/questions/23278469/why-is-my-bitmap-sort-not-infintely-faster-than-my-mergesort
*/
public class BitMapV2 {
byte[] bits;
int size;
public BitMapV2(int n) {
size = n;
bits = new byte[(int) Math.ceil((double) n / (double) Byte.SIZE)];
for (Byte b : bits) {
b = 0;
}
}
private String toBinary(byte b) {
return String.format(Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
}
void set(int i) {
int index = i / Byte.SIZE;
bits[index] = (byte) ((bits[index] | (byte) (1 << (Byte.SIZE - 1 - (i % Byte.SIZE)))));
}
void unset(int i) {
int index = i / Byte.SIZE;
bits[index] = (byte) ((bits[index] ^ (byte) (1 << (Byte.SIZE - 1 - (i % Byte.SIZE)))));
}
boolean isSet(int i) {
int index = i / Byte.SIZE;
byte mask = (byte) ((bits[index] & (byte) (1 << (Byte.SIZE - 1 - (i % Byte.SIZE)))));
return (bits[index] & mask) != 0;
}
}
5、BitMap的不足
- 为了方便描述,还以排序为例,假设要为1、2、9999999这三个数排序,需要一个长度为1250000的字节数组。除第0排、最后一排的小正方形里有亮灯外,其余正方形里的灯都是熄灭的,好浪费啊。
- 以排序为例,如果待排序的整数中有重复的数,排序后,重复的数仅保留了一个。
- 如果待排序的整数中有负数,上述代码有缺陷,待完善。