描述
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
数据范围:节点总数 0≤n≤50000≤n≤5000,每个节点的val满足 ∣val∣<=1000∣val∣<=1000
要求:时间复杂度 O(nlogn)O(nlogn)
示例1
输入:
[{1,2,3},{4,5,6,7}]
复制返回值:
{1,2,3,4,5,6,7}
解题思路:
整体高层思路
- 如果只合并两个链表,用 Merge(list1, list2):用一个哑节点(dummy)和尾指针 cur,迭代比较两个链表当前节点,把较小的节点直接接到结果链表后,直到某个链表遍历完,把剩余整段接上即可(原地,不创建新节点)。
- 如果合并 k 个链表(lists),用分治:把数组区间 [left, right] 对半分,分别递归合并左右两半得到两条有序链表,再把这两条按 Merge 合并;递归直到区间为空或只有一个链表。
- 这种分治把合并过程变成类似归并排序的合并,能把多链表合并的复杂度降到较优。
下面逐段解释代码与关键点
- Merge 方法(合并两个有序链表,非递归、就地)
签名:public ListNode Merge(ListNode list1, ListNode list2)
-
边界处理:
- if (list1 == null) return list2;
- if (list2 == null) return list1;
这两句处理其中一个输入为空的情况,直接返回另一个链表(可能为 null 或非空)。
-
创建虚拟头结点和尾指针:
- ListNode head = new ListNode(0);
- ListNode cur = head;
使用虚拟头可以避免单独处理第一个节点的连接逻辑,统一操作更简洁。
-
主循环(同时遍历两个链表):
- while (list1 != null && list2 != null) {
if (list1.val <= list2.val) { cur.next = list1; list1 = list1.next; }
else { cur.next = list2; list2 = list2.next; }
cur = cur.next;
}
解释: - 每次比较 list1 和 list2 的当前节点值,把较小(或相等时取 list1)的节点直接接到 cur.next,
- 然后把对应链表前进一位,并把 cur 指向新接上的节点。
- 注意这里没有创建新节点,是把原节点“摘出来”接到结果链表上,所以是原地合并。
- while (list1 != null && list2 != null) {
-
处理剩余节点:
- if (list1 == null) cur.next = list2; else cur.next = list1;
解释: - 循环结束说明至少一个链表遍历完,另一个链表剩余的节点本身是有序的,把剩余整段直接接上即可。
- if (list1 == null) cur.next = list2; else cur.next = list1;
-
返回结果:
- return head.next; // 跳过虚拟头,返回真实头
复杂度:
- 时间 O(n + m),空间 O(1)(只用了常数个指针,原地操作)。
- divideMerge 方法(分治合并 lists[left..right])
签名:ListNode divideMerge(ArrayList lists, int left, int right)
-
基本情况:
- if (left > right) return null; // 空区间
- else if (left == right) return lists.get(left); // 区间只有一个链表,直接返回
-
分治合并:
- int mid = left + (right - left) / 2; // 防止溢出(常用写法)
- return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
解释: - 先递归合并左半段和右半段,得到两条有序链表,再把两条链表用 Merge 合并。
- 这种做法等价于把 k 条链表两两合并(类似归并排序的合并阶段),递归深度约为 O(log k)。
时间复杂度直观分析(k 条链表,总结点数为 N):
- 每一层分治把所有节点参与合并一次,总开销与节点数成线性关系。分治共有 O(log k) 层,因此总时间复杂度为 O(N log k)(这是多链表合并的经典下界并且是高效实现之一)。
- 空间复杂度:递归栈占 O(log k)(分治递归深度),除此之外常数额外空间(Merge 是原地的 O(1))。
- mergeKLists(入口)
签名:public ListNode mergeKLists (ArrayList lists)
- 边界处理:
- if (lists == null || lists.size() == 0) return null;
- 调用分治:
- return divideMerge(lists, 0, lists.size() - 1);
这样就能把 lists 中所有链表合并成一个。
示例帮助理解
- 假设有 4 条链表 L0, L1, L2, L3:
- divideMerge([0,3]) 会把区间分为 [0,1] 与 [2,3] 两部分,
- 递归分别合并 [0,1] -> M01,合并 [2,3] -> M23,
- 最后 Merge(M01, M23) 得到最终结果。
- 每次 Merge 的成本与参与合并的节点数成线性关系,分层合并使总复杂度为 O(N log k)。
一句话总结
- 用一个 O(1) 空间的原地 Merge 两条有序链表作为基本操作,配合分治把 k 条链表递归两两合并,最终以 O(N log k) 时间、O(log k) 额外栈空间把所有链表合并为一条有序链表。
import java.util.*;
/*
* 假设已有的节点类(平台通常已定义),保留示例以便参考:
* public class ListNode {
* int val;
* ListNode next = null;
* public ListNode(int val) {
* this.val = val;
* }
* }
*/
public class Solution {
/**
* 合并两个有序链表为一个有序链表(非递归、就地合并)
*
* @param list1 有序链表1 的头结点
* @param list2 有序链表2 的头结点
* @return 合并后的有序链表头结点
*/
public ListNode Merge(ListNode list1, ListNode list2) {
// 如果其中一个链表为空,直接返回另一个(边界情况)
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
// 构造一个虚拟头节点,方便统一处理头节点连接逻辑
ListNode head = new ListNode(0);
// cur 指向结果链表的当前尾部
ListNode cur = head;
// 同时遍历两个链表,每次把较小的节点接到结果链表后面
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
// list1 当前节点值更小或相等,把它接到 cur 后面
cur.next = list1;
// list1 前进
list1 = list1.next;
} else {
// list2 当前节点值更小,把它接到 cur 后面
cur.next = list2;
// list2 前进
list2 = list2.next;
}
// cur 前进到新接上的节点
cur = cur.next;
}
// 退出循环表示至少有一个链表已经遍历完
// 把剩余的非空链表直接接到结果链表后面(注意不要接 null)
if (list1 == null) {
cur.next = list2; // list2 可能为非空或 null(如果都为空则接 null 也没问题)
} else {
cur.next = list1;
}
// 返回虚拟头节点的下一个节点,即真正的合并后链表头
return head.next;
}
/**
* 使用分治(递归)合并 lists 数组中索引区间 [left, right] 的链表
*
* 思路:将区间一分为二,分别合并左右两边,然后再把两个结果合并(两两合并)
*
* @param lists 存放链表头结点的 ArrayList
* @param left 区间左端点(包含)
* @param right 区间右端点(包含)
* @return 合并后的链表头结点,若 left > right 返回 null(空区间)
*/
ListNode divideMerge(ArrayList<ListNode> lists, int left, int right) {
// 空区间,返回 null
if (left > right) {
return null;
}
// 区间只有一个链表,直接返回该链表
else if (left == right) {
return lists.get(left);
}
// 取中点,使用更稳健的写法防止极端整型溢出(虽然索引通常不会那么大)
int mid = left + (right - left) / 2;
// 递归合并左半区间和右半区间,然后把两个结果合并并返回
return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
}
/**
* 合并 k 个有序链表(入口函数)
*
* @param lists 存放链表头结点的 ArrayList(可能为 null 或 空)
* @return 合并后的有序链表头结点;若输入为空返回 null
*/
public ListNode mergeKLists (ArrayList<ListNode> lists) {
// 边界处理:lists 为 null 或者没有任何链表,直接返回 null
if (lists == null || lists.size() == 0) return null;
// 利用分治合并所有链表,区间为 [0, lists.size() - 1]
return divideMerge(lists, 0, lists.size() - 1);
}
}