前序
昨夜晚归,兴之所至,翻阅旧书,《编程珠玑》,薄尘轻蒙,遂感慨无数,静心而读。忽遇难解之习题,问诸西洋必应者,得一文曰[珠玑之椟]位向量/位图的定义和应用,其思明,其言简,然不得其意,研习良久,终顿开茅塞,于今日作拙,欲通俗易懂见诸Java矣。
一、背景介绍
首先我们来看一下Bentley大师在《编程珠玑》第一章提出的问题:
输入:一个最多包含n个正整数的文件,每个数都小于n,其中n=10^7。如果在输入文件中有任何整数重复出现就是致命错误(文件中的整数互异)。没有其他数据与该整数相关联。
输出:按升序排列的输入整数的列表。
约束:最多有1MB的内存空间可用,有充足的磁盘存储空间可用。运行时间最多几分钟,最理想的时间是10S。
其中具体的分析过程不再重复,感兴趣的朋友可以查看原书,下面直接列出大师的解决方案。
数据结构:位向量(亦作位图,不过与图形学中的位图混淆,下作位向量)。一个n位的二进制数据,数据i如果出现在该二进制的第i位,则该位置为1,否则为0。如:用一个10位长的二进制数据表示元素都小于10的集合,{1,2,3,5,8},该集合用二进制数据的表现形式:01110100100
算法分析:分三步解决
1. 初始化集合,每个位都置为0;
2. 读入文件的每个整数,将对应的位置为1;
3. 遍历二进制数据,如果该位为1,则输出相应的整数。
伪代码:
/*第一步:遍历二进制数组,都置为0,进行初始化
for i = [0,n)
bit[i] = 0
/*第二步:读取文件,将整数对于的位置为1
for each i in the file
bit[i] = 1;
/*第三步:将已排序的整数遍历输出
for i = [0,n)
if bit[i] == 1
print(i)
好,以上我们将位向量数据结构的概念了解清楚了,下面遇到一个书上的习题。
二、提出问题
2.如何使用位逻辑运算(如与、或、移位)来实现位向量?
第一眼看到这个题目时我很纳闷儿,不是可以直接操作二进制位的数组吗,为什么还要使用位逻辑运算来实现位向量呢。
然后转念一想,像Java/C++这类高级语言是没有bit这种数据类型的,占位最小的数据类型就是byte,一个字节,八位。
所以我们如果要用Java实现位向量的话,需要考虑如何运用位运算了。
三、解决问题
重新定义问题:假设有一组互异且小于N的正整数,需要使用位向量来进行升序排列,请用Java实现。
问题解析:
首先我们要明确对于位向量来说,要存储一组小于N的正整数,那么就需要N位的二进制数。
而如果使用int数组来表示二进制位,1int=4byte=4*8bit,那么一个int元素就可以表示32位,即存储小于32的正整数集合,两个int元素可以表示64位,即存储小于64的正整数集合。
那么问题来了,如果要表示小于N的正整数集合,需要多少个int元素?
array.length = (N-1)/32 + 1
如何定位正整数i在数组中的位置呢? 一句话:32整除i,商Q是int数组的下标,余数R是1在这个int元素中的位数。
我们可以想象一下,i用位向量表示就是000000….1….,其中1所在的位置是i。可以想象成这个很长的位向量对齐int数组的首位,然后倒向int数组(数组按照一个二进制位一个槽来表示)
目前我们已经定位了i的位置,下一步考虑如何进行三个很重要的操作 1.置位;2.置零,3.读取
置位:用余数(二进制表示,即1 << Q)与相应的int元素做或操作
置零:将余数(二进制表示,即1 << Q)取反,然后结果与int元素做与运算
读取:用余数与相应的int元素做与运算,得到int元素中该位置的值,如为1则返回1,为0则返回0。
实现:
package com.rambo.P1;
import java.util.ArrayList;
import java.util.List;
public class BitVetory {
private int n;
private int[] bitArray;
private static final int BIT_LENGTH = 32;//默认使用int类型
private static int P;
private static int Q;
/** * 初始化位向量 *@param n */
public BitVetory(int n) {
this.n = n;
bitArray = new int[(n-1)/BIT_LENGTH + 1];
init();
}
/** * 初始化操作 */
public void init(){
for (int i = 0; i < n; i++) {
clr(i);
}
}
/** * 获取排序后的数组 *@return */
public List getSortedArray(){
List sortedArray = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (get(i) == 1) {
sortedArray.add(i);
}
}
return sortedArray;
}
/** * 置位操作 *@param i */
public void set(int i){
P = i / BIT_LENGTH;
Q = i % BIT_LENGTH;
bitArray[P] |= 1 << Q;
}
/** * 置零操作 *@param i */
public void clr(int i){
P = i / BIT_LENGTH;
Q = i % BIT_LENGTH;
bitArray[P] &= ~(1 << Q);
}
/** * 读取操作 *@param i *@return */
public int get(int i){
P = i / BIT_LENGTH;
Q = i % BIT_LENGTH;
return Integer.bitCount(bitArray[P] & (1 << Q));
}
}
package com.rambo.P1.test;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import com.rambo.P1.BitVetory;
public class TestMain {
public static void main(String[] args) {
int amount = 15;
List randoms = getRandoms(amount);
System.out.println("排序前数组:");
BitVetory bitVetory = new BitVetory(amount);
for (Integer e : randoms) {
System.out.print(e+",");
bitVetory.set(e);
}
List sortedArray = bitVetory.getSortedArray();
System.out.println();
System.out.println("排序后数组:");
for (Integer e : sortedArray) {
System.out.print(e+",");
}
}
private static List getRandoms(int amount) {
Random random = new Random();
List randoms = new ArrayList<>();
while(randoms.size() < (amount - 1)){
int element = random.nextInt(amount - 1) + 1;//element ∈ [1,amount)
if (!randoms.contains(element)) {
randoms.add(element);
}
}
return randoms;
}
}
输出:
排序前数组:
11,7,12,1,2,9,3,13,6,5,14,4,8,10
排序后数组:
1,2,3,4,5,6,7,8,9,10,11,12,13,14
好,如此这般我们便使用Java语言实现了位向量。
问题?
我们的代码是否还有可优化的空间呢?从位操作符的角度考虑。
.
.
.
.
.
.
..
.
.
.
.
.
.
.
.
.
.
.
.
.
将P = i / BIT_LENGTH改成P = i >> 5(2^5=32) 将Q = i % BIT_LENGTH改成Q = i & 0x1F (0x1F = 11111)