目录
1628
8 分钟
合并 K 个有序链表:最小堆与分治法

题目描述#

给定一个链表数组,每个链表都已按升序排列。将所有链表合并为一个升序链表,并返回合并后的链表头。

输入:lists = [1→4→5, 1→3→4, 2→6]
输出:1→1→2→3→4→4→5→6

解题思路#

这道题是”合并两个有序链表”的扩展。当 K 变大时,如果我们每次都遍历 K 个链表的头节点来找最小值,时间复杂度会退化到 O(K·N)。我们需要更高效的方式。

方法一:最小堆(优先队列)#

核心思想是维护一个大小为 K 的小根堆,每次从堆顶取出最小的节点,再将该节点的下一个节点入堆。

  • 初始化:将 K 个链表的头节点放入最小堆
  • 每轮:弹出堆顶节点追加到结果链表,将该节点的 next 入堆
  • 直到堆为空

时间复杂度:O(N log K),每次堆操作 O(log K)
空间复杂度:O(K),堆中最多同时存放 K 个节点

方法二:分治法两两合并#

利用”合并两个有序链表”作为基本操作,将 K 个链表两两配对合并,层层归并。

  • 第一轮:合并 (0,1), (2,3), (4,5), … → K/2 个链表
  • 第二轮:合并上轮结果两两配对 → K/4 个链表
  • 直到只剩一个链表

时间复杂度:O(N log K),每轮合并 O(N),共 log K 轮
空间复杂度:O(1)(递归栈 O(log K))

两种方法对比#

维度最小堆分治法
时间复杂度O(N log K)O(N log K)
空间复杂度O(K)O(1)
实现难度需要堆数据结构递归直观
适用场景K 很大但 N 不大K 很大且 N 也很大

下面给出两种方法的完整实现。

代码实现(最小堆法)#

C
#include <stdio.h>
#include <stdlib.h>

typedef struct ListNode {
    int val;
    struct ListNode *next;
} ListNode;

/* —— 自定义最小堆 —— */
typedef struct {
    ListNode **data;
    int size;
    int capacity;
} MinHeap;

static void heapSwap(ListNode **a, ListNode **b) {
    ListNode *tmp = *a; *a = *b; *b = tmp;
}

static void heapifyUp(MinHeap *h, int i) {
    while (i > 0) {
        int p = (i - 1) / 2;
        if (h->data[p]->val <= h->data[i]->val) break;
        heapSwap(&h->data[p], &h->data[i]);
        i = p;
    }
}

static void heapifyDown(MinHeap *h, int i) {
    while (1) {
        int left = 2 * i + 1, right = 2 * i + 2, smallest = i;
        if (left < h->size && h->data[left]->val < h->data[smallest]->val)
            smallest = left;
        if (right < h->size && h->data[right]->val < h->data[smallest]->val)
            smallest = right;
        if (smallest == i) break;
        heapSwap(&h->data[i], &h->data[smallest]);
        i = smallest;
    }
}

static void heapPush(MinHeap *h, ListNode *node) {
    h->data[h->size] = node;
    heapifyUp(h, h->size);
    h->size++;
}

static ListNode* heapPop(MinHeap *h) {
    ListNode *res = h->data[0];
    h->data[0] = h->data[--h->size];
    if (h->size > 0) heapifyDown(h, 0);
    return res;
}

ListNode* mergeKLists(ListNode** lists, int listsSize) {
    if (listsSize == 0) return NULL;

    MinHeap h;
    h.data = (ListNode**)malloc(sizeof(ListNode*) * listsSize);
    h.size = 0;
    h.capacity = listsSize;

    // 将 K 个链表的头节点入堆
    for (int i = 0; i < listsSize; i++) {
        if (lists[i]) heapPush(&h, lists[i]);
    }

    ListNode dummy;
    dummy.next = NULL;
    ListNode *tail = &dummy;

    while (h.size > 0) {
        ListNode *minNode = heapPop(&h);
        tail->next = minNode;
        tail = tail->next;
        if (minNode->next) heapPush(&h, minNode->next);
    }

    free(h.data);
    return dummy.next;
}
C++
#include <queue>
#include <vector>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        // 自定义比较器:按节点值从小到大
        auto cmp = [](ListNode *a, ListNode *b) { return a->val > b->val; };
        priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp);

        // 将 K 个链表的头节点入堆
        for (auto head : lists) {
            if (head) pq.push(head);
        }

        ListNode dummy(0);
        ListNode *tail = &dummy;

        while (!pq.empty()) {
            ListNode *minNode = pq.top(); pq.pop();
            tail->next = minNode;
            tail = tail->next;
            if (minNode->next) pq.push(minNode->next);
        }

        return dummy.next;
    }
};
JavaScript
class ListNode {
    constructor(val, next) {
        this.val = (val === undefined ? 0 : val);
        this.next = (next === undefined ? null : next);
    }
}

// 使用最小优先队列的简单实现(手动维护排序数组)
function mergeKLists(lists) {
    // 过滤空链表,收集所有头节点
    const heap = lists.filter(h => h !== null);

    // 按值排序(简易最小堆替代)
    const sortHeap = () => heap.sort((a, b) => a.val - b.val);

    const dummy = new ListNode(0);
    let tail = dummy;

    while (heap.length > 0) {
        sortHeap();
        const minNode = heap.shift(); // 取出最小值
        tail.next = minNode;
        tail = tail.next;
        if (minNode.next) heap.push(minNode.next);
    }

    return dummy.next;
}

// 性能优化版本:分治法(见下方)

代码实现(分治法)#

C
// 合并两个有序链表(基础操作)
ListNode* mergeTwo(ListNode* a, ListNode* b) {
    ListNode dummy;
    dummy.next = NULL;
    ListNode *tail = &dummy;
    while (a && b) {
        if (a->val <= b->val) { tail->next = a; a = a->next; }
        else                  { tail->next = b; b = b->next; }
        tail = tail->next;
    }
    tail->next = a ? a : b;
    return dummy.next;
}

// 分治法:两两合并
ListNode* mergeKListsDivide(ListNode** lists, int listsSize) {
    if (listsSize == 0) return NULL;
    // interval 表示当前每组合并的步长:1→2→4→8→...
    for (int interval = 1; interval < listsSize; interval *= 2) {
        for (int i = 0; i + interval < listsSize; i += interval * 2) {
            lists[i] = mergeTwo(lists[i], lists[i + interval]);
        }
    }
    return lists[0];
}
C++
class Solution {
public:
    // 合并两个有序链表(基础操作)
    ListNode* mergeTwo(ListNode* a, ListNode* b) {
        ListNode dummy(0);
        ListNode *tail = &dummy;
        while (a && b) {
            if (a->val <= b->val) { tail->next = a; a = a->next; }
            else                  { tail->next = b; b = b->next; }
            tail = tail->next;
        }
        tail->next = a ? a : b;
        return dummy.next;
    }

    // 分治法:两两合并
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        int n = lists.size();
        if (n == 0) return nullptr;

        for (int interval = 1; interval < n; interval *= 2) {
            for (int i = 0; i + interval < n; i += interval * 2) {
                lists[i] = mergeTwo(lists[i], lists[i + interval]);
            }
        }
        return lists[0];
    }
};
JavaScript
function mergeTwo(a, b) {
    const dummy = new ListNode(0);
    let tail = dummy;
    while (a && b) {
        if (a.val <= b.val) { tail.next = a; a = a.next; }
        else                { tail.next = b; b = b.next; }
        tail = tail.next;
    }
    tail.next = a ? a : b;
    return dummy.next;
}

function mergeKLists(lists) {
    if (lists.length === 0) return null;
    const n = lists.length;

    for (let interval = 1; interval < n; interval *= 2) {
        for (let i = 0; i + interval < n; i += interval * 2) {
            lists[i] = mergeTwo(lists[i], lists[i + interval]);
        }
    }
    return lists[0];
}

分治法图解#

初始 K=6 个链表:
L0  L1  L2  L3  L4  L5

interval=1 (步长为1):
合并 L0+L1 → L0'
合并 L2+L3 → L2'
合并 L4+L5 → L4'

interval=2 (步长为2):
合并 L0'+L2' → L0''
合并 L4' 独自留下

interval=4 (步长为4):
合并 L0''+L4' → 最终结果

总共 log₂6 ≈ 3 轮合并

复杂度分析#

维度最小堆法分治法
时间复杂度O(N log K)O(N log K)
空间复杂度O(K)O(1)
实现难度依赖语言提供的堆递归/迭代,逻辑简单

其中 N 为所有链表节点总数,K 为链表个数。

关键要点#

  1. 最小堆是”K 指针选最小”的优化:避免每轮都遍历 K 个指针
  2. 分治法的 interval 迭代interval *= 2 的精妙循环,原地合并,空间 O(1)
  3. 与合并两个的区别:K=2 时退化为经典双指针合并;K>2 时需要”多路归并”的框架

相关文章:

合并 K 个有序链表:最小堆与分治法
https://www.hehonglei.cn/posts/linked-list-merge-k-sorted/
作者
Honglei He
发布于
2026-08-10
许可协议
CC BY-NC-SA 4.0