前言
合并多条有序链表,暴力逐个合并效率很低,分治是最优解之一,时间复杂度 O(nlogk),代码简洁好记,下面逐行拆解这段 JS 代码。
链表节点定义(题目自带)
function ListNode(val, next) {
this.val = val === undefined ? 0 : val
this.next = next === undefined ? null : next
}
完整代码
var mergeKLists = function(lists) {
// 合并两个有序链表(LeetCode21逻辑)
const mergeTwo = (a, b) => {
const dummy = new ListNode(0);
let cur = dummy;
while (a && b) {
if (a.val < b.val) {
cur.next = a;
a = a.next;
} else {
cur.next = b;
b = b.next;
}
cur = cur.next;
}
// 拼接剩余未遍历完的链表
cur.next = a || b;
return dummy.next;
}
// 分治递归:合并区间 [l, r] 的所有链表
const merge = (l, r) => {
// 区间只有一条链表,直接返回
if (l === r) return lists[l];
// 左边界大于右边界,无链表返回null
if (l > r) return null;
// 二分取中点
const mid = (l + r) >> 1;
// 递归合并左半区、右半区
const left = merge(l, mid);
const right = merge(mid + 1, r);
// 将左右合并后的两条链表合并
return mergeTwo(left, right);
}
// 从整个数组区间 0 ~ lists.length-1 开始分治
return merge(0, lists.length - 1);
};
代码逐段解析
1. mergeTwo(a, b):合并两条有序链表
核心是虚拟头节点 dummy,规避链表头节点判断逻辑:
- 用
cur指针遍历拼接; - 每次把更小值的节点接到
cur.next; - 循环结束后,直接拼接剩下未走完的链表;
- 返回
dummy.next即为合并后的链表头部。
2. merge(l, r):分治递归函数
采用二分拆分思想,类似归并排序:
- 终止条件1:
l === r,当前区间只剩一条链表,直接返回; - 终止条件2:
l > r,区间不存在链表,返回空; (l + r) >> 1等价Math.floor((l+r)/2),快速取中间下标;- 递归拆分左区间
[l,mid]、右区间[mid+1,r]; - 左右区间各自合并完成后,调用
mergeTwo合并两条结果链表。
3. 入口调用
merge(0, lists.length - 1) 代表对整个链表数组做分治合并。
思路总结
- 分治拆分:把 k 条链表不断二分,直到每组只剩 1 条链表;
- 逐层向上合并:每两层合并为一条有序链表;
- 底层复用「合并两有序链表」基础逻辑,代码复用性强。
复杂度
- 时间:
O(nlogk),n 所有节点总数,k 链表数量; - 空间:
O(logk)递归栈开销。
适用场景
面试首选解法,代码短、逻辑清晰,相比最小堆不需要手动实现堆结构,上手更快。