最近刷牛客网的时候又把 BM5 这道题翻了出来。题目名字叫“合并k个已排序的链表”,看着就是一个入门题的加强版,但真到提交的时候,不少人会在原地卡很久。原因其实不是题面难懂,而是“合并两个有序链表”到“合并k个有序链表”之间,藏着不少关于时间复杂度和代码组织方式的坑。这篇文章我把这道题从头到尾拆一遍:暴力、顺序合并、分治、最小堆四种解法都写清楚,再补上 JavaScript(Node) 在牛客网环境下的提交细节,希望能给正在刷这道题的朋友一点实打实的参考。
1. 先别急着写代码:把题目条件和坑点捋清楚
1.1 题目表面是“合并链表”,实际考的是“多路归并”
BM5 的题面很简洁:给一个包含 k 个链表头节点的数组,每个链表内部都是升序排列,要求把所有节点合并成一个大的升序链表,返回新链表的头节点。
题面简洁归简洁,它其实在考一个很经典的数据结构问题——多路归并(k-way merge)。你如果只是把“合并两个有序链表”的函数拿出来循环调用,代码确实能过,但面试时或者讨论复杂度时就会露怯。多路归并的本质是:从 k 个有序序列中,反复取出当前最小的元素,直到所有元素都被取完。关键是怎么高效地“找到当前最小”。
这个问题在真实业务里也特别常见,最典型的就是外部排序:一堆有序文件或者有序分片要合并成一个大的有序结果集。你不可能把所有数据一次性加载进内存,所以只能靠多路归并的思路一点一点往外取。理解了这层背景,你再回头看 BM5,就会明白它不是在考你会不会写链表,而是在考你对归并逻辑和数据结构的掌握程度。
1.2 空数组、空节点、重复值:边界条件一网打尽
刷题最容易翻车的不是主逻辑,而是边界条件。BM5 里这几个边界情况,我建议在一开始就列出来:
- 传入的 lists 是空数组,即
lists = [],此时应该返回null。 - 某些链表本身就是空链表,也就是
lists[i] = null,合并时需要跳过。 - 所有链表都为空,结果也是
null。 - 链表中允许出现重复值,两个链表里都有节点值相等,合并顺序随便,但必须保证整体升序。
- 只有一个链表,直接返回这个链表,不需要额外复制节点。
记得在牛客网这类在线评测系统里,链表节点通常定义成下面这样:
javascript复制function ListNode(x) {
this.val = x;
this.next = null;
}
有一点你要特别注意:返回的链表必须复用原来的节点,不是 new 一堆新节点然后复制 val。虽然复制节点也能过,但空间复杂度会变高,而且明显不贴合“合并”的语义。实现时用 dummy 头节点来统一操作,这是处理链表题的常规手法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 两种“朴素”解法:能跑通,但代价藏得很深
2.1 暴力收集加排序:5分钟内能做出来,但面试官会皱眉
如果你不追求效率,最无脑的解法就是:遍历所有链表,把所有节点放进一个数组,然后对数组按照 val 排序,最后把数组里的节点串成一条新链表。
代码写起来确实快:
javascript复制function mergeKLists(lists) {
if (!lists.length) return null;
const nodes = [];
for (let head of lists) {
let cur = head;
while (cur) {
nodes.push(cur);
cur = cur.next;
}
}
if (!nodes.length) return null;
nodes.sort((a, b) => a.val - b.val);
for (let i = 0; i < nodes.length - 1; i++) {
nodes[i].next = nodes[i + 1];
}
nodes[nodes.length - 1].next = null;
return nodes[0];
}
这种写法有两个细节值得说。
第一,排序的比较函数必须写 (a, b) => a.val - b.val。如果直接调 nodes.sort(),JavaScript 的默认排序会把元素转成字符串后按字典序排,10 会排在 2 前面,结果完全错误。这是新手特别容易踩的坑。
第二,链表中节点总数如果记为 N,这个解法的时间复杂度是 O(N log N),空间复杂度是 O(N)。如果 k 个链表每个平均长度是 n,那么 N = k * n。你可能会想,O(N log N) 看上去也不是很差?但问题在于:题目给定的是 k 个已经排好序的链表,这本身就是一种很强的“部分有序”信息,你手动把它打散成数组再整体排序,等于把这份信息浪费掉了。真正的多路归并可以做到 O(N log k),在 k 远小于 N 的场景下,这个差距是实打实的。
所以我的建议是:如果只是本地调试、快速验证数据,暴力方法可以;但你要是想在牛客网上练出真东西,或者应付面试
