1. 概述

本文将介绍如何利用堆(Heap)结构高效地合并多个已排序的数组。这是一个在实际开发中经常遇到的经典问题,比如多路归并、外部排序等场景。使用最小堆(min-heap)可以将时间复杂度控制在一个非常理想的范围内,避免暴力合并带来的性能损耗。

2. 算法原理

我们的问题目标是:合并 k 个已排序的数组,输出一个整体有序的结果数组。为了高效解决这个问题,采用 最小堆(min-heap) 是一种经典且高效的策略。

✅ 什么是 min-heap?

min-heap 是一种特殊的二叉树结构,满足以下性质:

  • 根节点的值是整个树中最小的;
  • 每个节点的值都小于等于其左右子节点的值。

通常,min-heap 使用数组实现,通过下标关系快速定位父子节点:

对于数组 A[] 中索引为 i 的元素:

  • 父节点索引:(i - 1) / 2
  • 左子节点索引:2 * i + 1
  • 右子节点索引:2 * i + 2

下面是 min-heap 的图示及其对应的数组表示:

MinHeapMerge

🔧 合并算法步骤

要合并多个有序数组,我们可以按如下流程操作:

  1. ✅ 创建结果数组,总长度为所有输入数组长度之和。
  2. ✅ 取每个数组的首个元素,构建一个大小为 k(数组个数)的初始堆。
  3. ✅ 将该数组构造成 min-heap。
  4. ✅ 重复以下步骤直到结果数组填满:
    • 提取堆顶(最小值),放入结果数组;
    • 从该元素来源的原数组中取出下一个元素,替换堆顶;
    • 重新调整堆结构(heapify),维持 min-heap 性质。

⚠️ 时间复杂度分析

  • 总共需要处理 k 个数组中的所有元素,设总元素个数为 N
  • 每次 heapify 操作的时间复杂度是 O(log k),因为堆的高度是 log k
  • 因此整体时间复杂度为:**O(N log k)**

💡 对比:如果直接合并再排序,复杂度会退化到 O(N log N),当 k << N 时,O(N log k) 明显更优。

🧪 示例输入与输出

输入数组:

{ { 0, 6 }, { 1, 5, 10, 100 }, { 2, 4, 200, 650 } }

期望输出:

{ 0, 1, 2, 4, 5, 6, 10, 100, 200, 650 }

这个例子能直观展示算法如何从小到大逐个“挑选”最小元素。

3. Java 实现

接下来我们用 Java 实现上述算法。核心思路是使用两个类:

  • HeapNode:封装堆中每个节点的信息;
  • MinHeap:实现堆的操作和合并逻辑。

3.1. 堆节点定义(HeapNode)

我们需要知道每个堆节点的值来自哪个数组,以及下一个待取元素的位置。

public class HeapNode {

    int element;              // 当前元素值
    int arrayIndex;           // 来自第几个数组(索引)
    int nextElementIndex = 1; // 下一个要取的元素在原数组中的位置

    public HeapNode(int element, int arrayIndex) {
        this.element = element;
        this.arrayIndex = arrayIndex;
    }
}

✅ 说明:

  • arrayIndex 用于定位该元素来自哪个输入数组;
  • nextElementIndex 初始为 1,表示下次从原数组的 index=1 处取数;
  • 我们省略了 getter/setter 以保持代码简洁,生产环境可根据需要补充。

3.2. MinHeap 类与合并逻辑

public class MinHeap {

    HeapNode[] heapNodes;

    public MinHeap(HeapNode heapNodes[]) {
        this.heapNodes = heapNodes;
        heapifyFromLastLeafsParent();
    }

    int getParentNodeIndex(int index) {
        return (index - 1) / 2;
    }

    int getLeftNodeIndex(int index) {
        return (2 * index + 1);
    }

    int getRightNodeIndex(int index) {
        return (2 * index + 2);
    }

    HeapNode getRootNode() {
        return heapNodes[0];
    }

    // 调整以 index 为根的子树,使其满足 min-heap 性质
    void heapify(int index) {
        int leftNodeIndex = getLeftNodeIndex(index);
        int rightNodeIndex = getRightNodeIndex(index);
        int smallestElementIndex = index;

        if (leftNodeIndex < heapNodes.length 
            && heapNodes[leftNodeIndex].element < heapNodes[index].element) {
            smallestElementIndex = leftNodeIndex;
        }
        if (rightNodeIndex < heapNodes.length
            && heapNodes[rightNodeIndex].element < heapNodes[smallestElementIndex].element) {
            smallestElementIndex = rightNodeIndex;
        }
        if (smallestElementIndex != index) {
            swap(index, smallestElementIndex);
            heapify(smallestElementIndex); // 递归调整
        }
    }

    // 从最后一个叶子节点的父节点开始,自底向上构建堆
    void heapifyFromLastLeafsParent() {
        int lastLeafsParentIndex = getParentNodeIndex(heapNodes.length - 1);
        while (lastLeafsParentIndex >= 0) {
            heapify(lastLeafsParentIndex);
            lastLeafsParentIndex--;
        }
    }

    // 交换两个节点
    private void swap(int i, int j) {
        HeapNode temp = heapNodes[i];
        heapNodes[i] = heapNodes[j];
        heapNodes[j] = temp;
    }

    // 主方法:合并 k 个有序数组
    public static int[] merge(int[][] arrays) {
        int k = arrays.length;
        if (k == 0) return new int[0];

        // Step 1: 初始化 heapNodes,取每个数组的第一个元素
        HeapNode[] heapNodes = new HeapNode[k];
        int totalSize = 0;

        for (int i = 0; i < k; i++) {
            if (arrays[i].length == 0) continue; // 跳过空数组
            heapNodes[i] = new HeapNode(arrays[i][0], i);
            totalSize += arrays[i].length;
        }

        MinHeap minHeap = new MinHeap(heapNodes);
        int[] result = new int[totalSize];
        
        // Step 2: 循环提取最小值并补充新元素
        for (int i = 0; i < totalSize; i++) {
            HeapNode root = minHeap.getRootNode();
            result[i] = root.element;

            // 判断是否还有下一个元素可取
            if (root.nextElementIndex < arrays[root.arrayIndex].length) {
                root.element = arrays[root.arrayIndex][root.nextElementIndex++];
            } else {
                // 没有更多元素了,设为最大值占位(相当于移除)
                root.element = Integer.MAX_VALUE;
            }

            // 重新调整堆
            minHeap.heapify(0);
        }

        return result;
    }
}

⚠️ 关键点说明:

  • 使用 Integer.MAX_VALUE 作为“已耗尽数组”的标记,确保它不会再被选为最小值;
  • heapify(0) 每次只调整根节点,因为只有根被修改了;
  • heapifyFromLastLeafsParent() 保证初始堆结构正确。

4. 测试验证

我们用前面提到的例子进行测试,确保逻辑正确。

import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class MinHeapTest {

    @Test
    public void givenSortedArrays_whenMerge_thenSuccess() {
        int[][] inputArray = { 
            { 0, 6 }, 
            { 1, 5, 10, 100 }, 
            { 2, 4, 200, 650 } 
        };
        int[] expectedArray = { 0, 1, 2, 4, 5, 6, 10, 100, 200, 650 };

        int[] resultArray = MinHeap.merge(inputArray);

        assertThat(resultArray.length, is(equalTo(10)));
        assertThat(resultArray, is(equalTo(expectedArray)));
    }
}

✅ 测试通过,结果符合预期。

💡 提示:实际项目中建议增加边界测试,如空数组、单数组、含重复元素等情况。

5. 总结

本文介绍了如何使用 min-heap 高效合并多个有序数组,核心思想是:

  • 利用堆快速获取当前最小值;
  • 动态维护 k 个候选元素(每数组一个);
  • 时间复杂度稳定在 O(N log k),远优于暴力合并。

这种模式在实际开发中非常实用,比如:

  • 多路归并排序;
  • 分布式系统中合并多个有序数据流;
  • 日志聚合、搜索引擎结果合并等。

🔗 示例代码已托管至 GitHub:https://github.com/example-java/algorithm-demo/tree/main/merge-sorted-arrays

掌握这个技巧,下次遇到类似问题就能简单粗暴地秒了。


原始标题:Efficiently Merge Sorted Java Sequences