读书笔记 《编程珠玑》 第1章 开篇

133 阅读4分钟

问题

怎样给一个磁盘文件排序? 是否一定要使用归并排序算法?

一个只由纯数字组成的文件, 例如手机号码, 且文件保证了手机号码不会重复出现, 有哪些排序思路呢?

  • 输入: 数据规模n=10^7的整数列表
  • 输出: 按升序排列的整数列表
  • 约束: 1MB内存、充足的磁盘、尽可能短的运行时间(秒级)

思路

  • 多次归并 (文件排序)磁盘排序 运行时间仍然需要几天

image.png

  • 多趟归并 内存排序

如果每个号码都使用32位整数来表示的话,在1MB存储空间里就可以存储250 000个号码。

因此,可以使用遍历输入文件40趟的程序来完成排序。

在第一趟遍历中,将0至249 999之间的任何整数都读入内存,并对这(最多)250 000 个整数进行排序,然后写到输出文件中。

第二趟遍历排序250 000至499 999之间的整数,依此类推,到第40趟遍历的时候对9 750 000至9 999 999之间的整数进行排序

image.png

  • 神奇排序 不借助外部文件、不需要多趟 image.png

神奇排序的实现 位图

可用一个 20 长的字符串来表示一个所有元素都小于 20 的简单的非负整数集合。

例如,可以用如下字符串来表示集合{1,2,3,5,8,13}:即把元素作为位图的索引, 用位图中的值1表示存在,0表示不存在

0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0

排序实现伪代码如下

/* phase 1: initialize set to empty */
for i = [0,n)
bit[i] = 0
/* phase 2: insert present elements into the set */
for each i in the input file
bit[i] = 1
/* phase 3: write sorted output */
for i = [0,n)
if bit[i] == 1
write i on the output file

位图实现代码

public class Bitmap {
    private final byte[] bits;
    private final int size;

    public Bitmap(int size) {
        this.size = size;
        /*
          一个字节8位,所以需要size/8 个字节
          如果size不能被8整除,需要额外一个字节
         */
        bits = new byte[(size + 7) / 8];
    }

    public void set(int n) {
        if (n < 0 || n >= size) {
            throw new IllegalArgumentException("out of range");
        }
        /*
            n % 8 得到字节中的第几位
            1 << (n % 8) 得到一个只有第n%8位为1的字节
            bits[n / 8] |= 1 << (n % 8) 将第n%8位置为1
         */
        /*
         * 例如n为10
         * n % 8 = 2
         * 1 << 2 = 0000 0100
         * bits[10 / 8] = bits[1] = 0000 0000
         * bits[1] |= 0000 0100 = 0000 0100
         */
        /*
         bits[0] 表示第0-7位; bits[1] 表示第8-15位; bits[2] 表示第16-23位; ...
         bits[1] = 0000 0101 表示第8, 10位为1, 同一字节中倒序存放
         */
        bits[n / 8] |= (byte) (1 << (n % 8));
    }

    public boolean test(int n) {
        if (n < 0 || n >= size) {
            throw new IllegalArgumentException("out of range");
        }
        return (bits[n / 8] & (1 << (n % 8))) != 0;
    }

    public static void main(String[] args) {
        Bitmap bitmap = new Bitmap(100);
        bitmap.set(10);
        bitmap.set(20);
        bitmap.set(30);
        System.out.println(bitmap.test(10));
        System.out.println(bitmap.test(20));
        System.out.println(bitmap.test(30));
        System.out.println(bitmap.test(40));
    }
}

拓展 Redis中的位图

Redis中的位图(Bitmap)是一种特殊类型的字符串值,它可以用来处理特定类型的数据集,如一组元素(例如用户ID)的出现与否。位图本质上是一个由位组成的数组,每个位的值只能是0或1。

在Redis中,位图主要通过以下几个命令来操作:

  1. SETBIT key offset value:将键key中的位offset设置为value。value必须是0或1。如果key不存在,Redis会创建一个新的字符串值。如果offset超过了字符串的当前长度,字符串会被扩展,并用0填充。
  2. GETBIT key offset:获取键key中位offset的值。如果offset超过了字符串的当前长度,或者key不存在,返回0。
  3. BITCOUNT key [start end]:计算键key中设置为1的位的数量。可以通过start和end参数指定一个字节范围。
  4. BITOP operation destkey key [key ...]:对一个或多个位图进行位运算,并将结果存储在destkey中。

这些命令可以用来实现各种有趣的功能,如统计在线用户数量、实现简单的搜索引擎等。

注意:Redis中的位图是以字节为单位存储的,所以如果你有一个非常大的位图,但只设置了其中的一小部分位,Redis仍然会为整个位图分配内存。因此,位图最适合用于稠密的数据集。

小结

本文由文件排序问题引出位图,虽是两种不同的数据处理逻辑, 却能处理相同的问题, 关键是在明确问题、找出正确的问题、找出数据的规律,并找到合理的解决方案