Skip to content

对1个亿数据进行排序,内存只有10M应该使用什么算法

对 1 亿数据排序(内存 10MB)的算法选择

  • 问题背景
  • 需要对 1 亿条数据(假设每条数据为 4 字节整数,约 400MB)进行排序。
  • 可用内存仅 10MB,远小于数据总量,无法一次性加载到内存。
  • 因此,必须使用外部排序(External Sorting)算法,结合磁盘和分块处理。
  • 推荐算法
  • 外部归并排序(External Merge Sort) 是最适合的算法,结合分块、分治和归并,高效处理大数据排序。
  • 核心点
  • 外部归并排序通过分块读取数据到内存、排序后写入磁盘,再归并有序块,适合内存受限场景。

1. 为什么选择外部归并排序

  • 内存限制
  • 1 亿条 4 字节整数 ≈ 400MB,10MB 内存只能加载约 250 万条(10MB / 4B)。
  • 常规内存排序(如快速排序、归并排序)需要加载全部数据,内存不足。
  • 外部排序适用性
  • 外部排序专为大数据设计,利用磁盘存储,分块处理,逐步归并。
  • 外部归并排序是标准方案,广泛用于数据库和文件系统排序。
  • 其他算法不适合
  • 快速排序:需要随机访问全部数据,内存不足。
  • 堆排序:需维护堆结构,内存无法容纳。
  • 桶排序/基数排序:需要额外内存统计或分配,10MB 不够。
  • 插入排序等:时间复杂度高(O(n²)),不适合大数据。

2. 外部归并排序的实现原理

外部归并排序分为两个阶段:分块排序归并

(1) 分块排序(Split and Sort)

  • 步骤
  • 将 1 亿数据分成多个小块,每块大小适合内存(10MB ≈ 250 万条整数)。
  • 依次读取每块到内存,使用内存排序算法(如快速排序或归并排序)排序。
  • 将排序后的块写入磁盘,生成临时文件。
  • 计算
  • 数据总量:1 亿 × 4B = 400MB。
  • 每块 10MB,约 250 万条。
  • 总块数:400MB / 10MB = 40 块。
  • 示例
  • 读取 0-250 万条,内存排序,写入 sorted_chunk_1.tmp
  • 读取 250-500 万条,内存排序,写入 sorted_chunk_2.tmp
  • 重复,直到生成 40 个有序临时文件。

(2) 归并(Merge)

  • 步骤
  • 使用多路归并(K-way Merge)合并 40 个有序临时文件。
  • 每次从每个临时文件读取一小部分数据(缓冲区),放入内存。
  • 使用小顶堆(Min-Heap)或类似结构选择最小元素,写入输出文件。
  • 从对应文件读取下一数据,重复直到所有文件处理完。
  • 内存分配
  • 10MB 内存分配:
    • 输入缓冲区:每个文件 256KB(40 个文件 × 256KB ≈ 10MB)。
    • 输出缓冲区:剩余内存(约几 KB)。
  • 每块 256KB 可存储 6.4 万条(256KB / 4B),足够归并操作。
  • 优化
  • 多轮归并:若一次归并 40 个文件内存不足,可分轮归并(如先归并 20 组,每组 2 个文件)。
  • 缓冲区调整:动态调整输入/输出缓冲区大小,减少 I/O。
  • 示例
  • 初始化 40 个文件指针,读取每文件首条数据到堆。
  • 弹出堆顶(最小值),写入输出文件,从对应文件读取下一条。
  • 重复,直到所有数据归并到最终有序文件。

算法流程

  1. 分块
  2. 读取 10MB 数据(250 万条)。
  3. 内存排序(快速排序,O(n log n),n = 250 万)。
  4. 写入磁盘,生成 sorted_chunk_i.tmp
  5. 重复 40 次。
  6. 归并
  7. 打开 40 个临时文件,每个文件分配 256KB 缓冲区。
  8. 使用小顶堆归并,输出到最终文件 sorted_final.dat
  9. 清理
  10. 删除临时文件,保留最终结果。

3. 时间与空间复杂度

  • 时间复杂度
  • 分块排序:每块 250 万条,快速排序 O(n log n),共 40 块,总计 O(N log n),N = 1 亿。
  • 归并:扫描所有数据,O(N log k),k = 块数(40),近似 O(N)。
  • 总时间:O(N log N),约 1 亿 × log(1 亿) ≈ 1 亿 × 26.6 次比较。
  • 空间复杂度
  • 内存:10MB(缓冲区 + 堆)。
  • 磁盘:临时文件 400MB(与原数据等大),最终文件 400MB。
  • 总磁盘:约 800MB(临时 + 最终,临时文件可边归并边删除)。
  • I/O 开销
  • 读:每条数据读 2 次(分块 + 归并)。
  • 写:每条数据写 2 次(临时 + 最终)。
  • 总 I/O:400MB × 4 = 1.6GB。

4. 优化与注意事项

优化

  • 增大缓冲区
  • 若内存允许,增加每文件缓冲区(如 512KB),减少 I/O 次数。
  • 多轮归并
  • 若 40 路归并内存不足,先归并成 4 个大块(每轮 10 路),再最终归并。
  • 并行化
  • 分块排序可并行(多线程或分布式),如 4 线程处理 10 块。
  • 归并阶段 I/O 密集,需优化磁盘性能。
  • 压缩数据
  • 若数据可压缩(如重复值多),压缩临时文件,降低磁盘 I/O。
  • 专用硬件
  • 使用 SSD 代替 HDD,提升 I/O 速度。
  • 库支持
  • 使用现有框架(如 Hadoop、Spark)实现外部排序,简化开发。

注意事项

  • 数据格式
  • 假设 4 字节整数,若数据复杂(如字符串),需调整块大小。
  • 磁盘性能
  • 外部排序 I/O 密集,需优化文件读写(如顺序 I/O)。
  • 错误处理
  • 处理文件读写失败、中间文件损坏。
  • 并发访问
  • 若数据动态更新,需加锁或快照。

5. 替代算法分析

  • 基数排序(Radix Sort)
  • 适用:数据有固定范围(如整数)。
  • 问题:需要统计直方图,10MB 内存不足以存储计数数组(1 亿数据需大内存)。
  • 结论:不适合。
  • 桶排序(Bucket Sort)
  • 适用:数据均匀分布。
  • 问题:需要分配大量桶,内存不足,且桶需磁盘存储,复杂性高。
  • 结论:不优于外部归并。
  • 归并排序变种
  • 多路归并是外部归并排序的核心,变种(如替换选择算法)可优化块生成,但复杂性增加。
  • 结论:标准外部归并已足够。
  • 分布式排序
  • 若有集群,可用 MapReduce 或 Spark 分片排序。
  • 问题:单机场景下无需分布式。
  • 结论:外部归并更简单。

6. 代码示例(伪代码)

import java.io.*;

public class ExternalMergeSort {
    public static void externalSort(String inputFile, String outputFile, int memorySize, int recordSize) throws IOException {
        // 1. 分块排序
        int numRecordsPerBlock = memorySize / recordSize; // 250万条
        int numBlocks = (int) Math.ceil((double) totalRecords / numRecordsPerBlock); // 40块
        List<String> tempFiles = new ArrayList<>();

        BufferedReader reader = new BufferedReader(new FileReader(inputFile));
        for (int i = 0; i < numBlocks; i++) {
            // 读取一块
            int[] block = new int[numRecordsPerBlock];
            int count = readBlock(reader, block, numRecordsPerBlock);
            // 内存排序
            Arrays.sort(block, 0, count);
            // 写入临时文件
            String tempFile = "sorted_chunk_" + i + ".tmp";
            writeBlock(tempFile, block, count);
            tempFiles.add(tempFile);
        }
        reader.close();

        // 2. 归并
        mergeBlocks(tempFiles, outputFile, memorySize);
    }

    private static int readBlock(BufferedReader reader, int[] block, int max) throws IOException {
        int count = 0;
        String line;
        while (count < max && (line = reader.readLine()) != null) {
            block[count++] = Integer.parseInt(line);
        }
        return count;
    }

    private static void writeBlock(String file, int[] block, int count) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(file));
        for (int i = 0; i < count; i++) {
            writer.write(String.valueOf(block[i]));
            writer.newLine();
        }
        writer.close();
    }

    private static void mergeBlocks(List<String> tempFiles, String outputFile, int memorySize) throws IOException {
        // 使用小顶堆归并
        PriorityQueue<MergeEntry> heap = new PriorityQueue<>(Comparator.comparingInt(e -> e.value));
        BufferedReader[] readers = new BufferedReader[tempFiles.size()];
        int bufferSize = memorySize / tempFiles.size() / 4; // 每文件缓冲区

        // 初始化堆
        for (int i = 0; i < tempFiles.size(); i++) {
            readers[i] = new BufferedReader(new FileReader(tempFiles.get(i)));
            String line = readers[i].readLine();
            if (line != null) {
                heap.offer(new MergeEntry(Integer.parseInt(line), i));
            }
        }

        // 归并
        BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
        while (!heap.isEmpty()) {
            MergeEntry entry = heap.poll();
            writer.write(String.valueOf(entry.value));
            writer.newLine();

            // 读取下一条
            String line = readers[entry.fileIndex].readLine();
            if (line != null) {
                heap.offer(new MergeEntry(Integer.parseInt(line), entry.fileIndex));
            }
        }

        // 清理
        writer.close();
        for (BufferedReader reader : readers) {
            reader.close();
        }
        for (String tempFile : tempFiles) {
            new File(tempFile).delete();
        }
    }

    static class MergeEntry {
        int value;
        int fileIndex;

        MergeEntry(int value, int fileIndex) {
            this.value = value;
            this.fileIndex = fileIndex;
        }
    }
}

7. 面试角度

  • 问“1 亿数据排序,内存 10MB 用什么算法”
  • 提外部归并排序,分块排序 + 多路归并,说明内存限制原因。
  • 问“外部归并排序原理”
  • 提分块(内存排序)、归并(小顶堆),计算块数(40 块)。
  • 问“时间复杂度”
  • 提 O(N log N),分块 O(N log n),归并 O(N log k)。
  • 问“优化”
  • 提增大缓冲区、多轮归并、并行分块、SSD。
  • 问“其他算法”
  • 提基数排序(内存不足)、桶排序(复杂),说明外部归并优势。

8. 总结

  • 问题:1 亿条 4 字节整数(400MB),内存仅 10MB,需排序。
  • 算法:外部归并排序,分为分块排序(每块 10MB,内存排序)和多路归并(小顶堆合并 40 块)。
  • 原理:分 40 块,每块 250 万条,内存排序后写磁盘,再用 256KB 缓冲区归并到最终文件。
  • 复杂度:时间 O(N log N),磁盘约 800MB,I/O 1.6GB。
  • 优化:多轮归并、并行分块、压缩数据、SSD。
  • 面试建议:提算法流程、块数计算、伪代码、优化点,清晰展示理解。