位图排序
应用位图的排序
引言
生活中我们不可避免的会遇到大数据排序问题,这里是对《编程珠玑》中第1章中所描述的一类排序需求的简单手记。
背景
类似对存储7位电话号码的数据库进行整理,即对不超过一千万个不重复数据进行排序,限制内存占用1M和时间占用小于10s。
预备
时间
对于一趟循环即时间复杂度为O(n)的程序需要至少一千万此操作,嵌套一层循环则为O(n^2),即一千万的平方次,显然是不现实的。所以最佳方案是一趟排序即完成。
内存
在磁盘中排序显然没有全部读入内存中快速,所以要考虑的是如何在1M内存入最多的数据。Jon Bentley给出的思路令我非常惊奇,利用位图存储数据,即用一位数来表示一个7位数据。
位图表示数据
用一个20位长的字符串来表示一个所有元素都小于20的非负整数集,例如可用如下字符串来表示集合{1,2,3,5,8,13}
0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0
代表集合中数据的位置置1,其余位置置0。
思路
用一个10000000位数组对从0000000~9999999的不超过一千万个数进行按位存储,顺序输出即为排序后的数据。
伪代码
define m 10000000
//第一步,置零
for i = [0,m]
bit[i] = 0
//第二步,置数
for each i in the input file
bit[i] = 1
//第三步,取数
for i = [0,m]
if (bit[i] == 1)
print i
实践
随机生成一千万个数
Random random = new Random();
for (int i = 0; i < bit; i++) {
arr[i] = -1;
arr[i] = random.nextInt(bit-1);
mSort[i] = false;
}
排序
for(int i = 0; i< bit; i++){
if(arr[i] != -1)
mSort[arr[i]] = true;
}
输出
for(int i=0 ; i<99;i++){
if(mSort[i])
System.out.println(i);
}
完整实例
package bitmapsort;
import java.util.Random;
public class BitMap {
final int bit = 10000000;
private int[] arr = new int[bit];
private boolean[] mSort = new boolean[bit];
public BitMap() {
// TODO Auto-generated constructor stub
Random random = new Random();
for (int i = 0; i < bit; i++) {
arr[i] = -1;
arr[i] = random.nextInt(bit-1);
mSort[i] = false;
}
}
public void sort() {
for(int i = 0; i< bit; i++){
if(arr[i] != -1)
mSort[arr[i]] = true;
}
}
public void print(){
for(int i = 0 ; i< bit ;i++){
if(mSort[i])
System.out.println(i);
}
}
public static void main(String[] args) {
long time ;
time = System.currentTimeMillis();
BitMap BM = new BitMap();
time = System.currentTimeMillis() - time;
System.out.println("create time :"+time+"ms");
//位图排序
time = System.currentTimeMillis();
BM.sort();
time = System.currentTimeMillis() - time;
System.out.println("bit sort time :"+time+"ms");
// BM.print();
}
}
运行
第一次运行
第二次运行
第三次运行
第四次运行
此时可看到,使用该算法排序不超过一千万个7位整数仅需不到80ms左右,非常惊艳。
分析
此算法的排序速度虽然很快,但是也有一个弊端,就是这一千万个数中不能出现相同数据,不然会出现数据丢失。适用范围有限。
那么如果改进一下呢?
改进
version 1.1
假设同一个数会存在至多128个,则可以使用byte[]。
代码修改略
实际测试中时间依然为70ms左右。
version 1.2
既然时间充足,如果取消内存限制的话,可以尝试更换为int[],java中int的范围为“-232”到“232-1”,即“-2147483648”到“2147483647”。
此时足够处理一千个数都为同一个数的极限情况。
修改后的代码
public class BitMap {
final int bit = 10000000;
private int[] arr = new int[bit];
private int[] mSort = new int[bit];
public BitMap() {
// TODO Auto-generated constructor stub
Random random = new Random();
for (int i = 0; i < bit; i++) {
arr[i] = -1;
arr[i] = random.nextInt(bit-1);
mSort[i] = 0;
}
}
public void sort() {
for(int i = 0; i< bit; i++){
if(arr[i] != -1)
mSort[arr[i]] ++;
}
}
public void print(){
for(int i = 0 ; i < bit ;i++){
if(mSort[i] > 0)
System.out.println(i+"*"+mSort[i]);
}
}
测试
可见排序时间暴涨了一倍多,但还是在可接受范围内。
version 2.1
既然用到了interger,而interger又有2147483647这么大的范围,我们可以尝试将排位范围也提高到200000000即不超过2亿个9位整数(为什么不用20亿?因为java会提示堆栈内存超限)。
测试
此时时间达到了4s左右,但是对比我们排序的2亿个数,成绩应该很可喜了。
分段输出
由于如此庞大的数据除了导出到文本文件里,几乎不可能实时查看,所以我们对某一区间进行测试输出。
- 0~50
- 1234567~1234600
间接反映了随机数生成方法还是比较稳定的。。。(笑)
总结
除了对Jon Bentley的佩服还是佩服。