Problem: 23. 合并 K 个升序链表
题目:给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
整体思路
这段代码旨在解决 “合并 K 个排序链表” 的问题,它采用了一种非常高效且空间优化的 自底向上(或称迭代式)归并排序 策略。与之前的递归分治法相比,这种方法避免了递归调用,从而消除了对调用栈的依赖,实现了真正的 O(1) 额外空间复杂度。
算法的核心思想是从最小的单位(单个链表)开始,以轮次(pass)的形式,逐步将它们两两合并成更大的有序链表,直到所有链表都合并成一个。
其核心逻辑步骤如下:
-
处理边界情况:
- 检查输入的链表数组
lists
是否为空,如果是,则直接返回null
。
- 检查输入的链表数组
-
迭代合并轮次:
- 算法的核心是一个外层
for
循环,由step
变量驱动。step
代表了当前轮次中,被合并的“子单元”的大小。它从 1 开始,每次翻倍(1, 2, 4, 8, ...
)。 step = 1
时:算法将lists[0]
与lists[1]
合并,lists[2]
与lists[3]
合并,以此类推,将所有链表两两配对进行合并。合并的结果存回配对的第一个位置(例如,merge(lists[0], lists[1])
的结果存回lists[0]
)。step = 2
时:现在,lists[0]
是一个包含两个原始链表的有序链表,lists[2]
也是。算法将lists[0]
与lists[2]
合并,lists[4]
与lists[6]
合并…- 持续进行:这个过程不断重复,每次合并的链表“块”都越来越大,直到
step
的大小超过了链表数组的长度,此时所有链表都已被合并到lists[0]
中。
- 算法的核心是一个外层
-
配对合并:
- 内层的
for
循环负责在每一轮中找到要合并的配对。 i += step * 2
这个跳跃方式确保了我们每次都能找到下一对独立的“块”来进行合并。例如,当step=1
时,我们处理(0, 1)
,然后跳到(2, 3)
。当step=2
时,我们处理(0, 2)
,然后跳到(4, 6)
。lists[i] = mergeTwo(lists[i], lists[i + step]);
调用标准的“合并两个有序链表”的辅助函数来完成实际的合并工作。
- 内层的
-
返回结果:
- 所有轮次结束后,
lists[0]
中就包含了最终的、完全合并好的链表,将其返回。
- 所有轮次结束后,
完整代码
/**
* Definition for singly-linked list.
*/
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
/**
* 合并 K 个排序链表(自底向上归并排序法)。
* @param lists 一个包含 K 个排序链表的数组
* @return 合并后的单一排序链表
*/
public ListNode mergeKLists(ListNode[] lists) {
int n = lists.length;
// 边界情况:没有链表需要合并
if (n == 0) {
return null;
}
// 外层循环控制合并的轮次。step 代表当前合并的“块”的大小。
// step 从 1 开始,每次翻倍,直到能覆盖所有链表。
for (int step = 1; step < n; step *= 2) {
// 内层循环负责在当前轮次中,找到所有需要合并的配对。
// 每次跳跃 2*step,以找到下一对独立的块。
for (int i = 0; i < n - step; i += step * 2) {
// 将 lists[i] 和 lists[i + step] 这两个块进行合并,
// 结果存回 lists[i]。
lists[i] = mergeTwo(lists[i], lists[i + step]);
}
}
// 所有轮次结束后,最终的合并结果存储在 lists[0] 中。
return lists[0];
}
/**
* 辅助函数:合并两个有序链表。
* @param l1 第一个有序链表
* @param l2 第二个有序链表
* @return 合并后的有序链表
*/
private ListNode mergeTwo(ListNode l1, ListNode l2) {
// 使用哨兵节点简化合并逻辑
ListNode dummy = new ListNode();
ListNode cur = dummy;
// 迭代比较,将较小的节点链接到结果链表
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
// 将剩余的链表部分直接追加到末尾
cur.next = l1 == null ? l2 : l1;
return dummy.next;
}
}
时空复杂度
时间复杂度:O(N log K)
- 外层循环:
step
变量从 1 开始,每次乘以 2,直到step >= K
(K
是链表数量n
)。这个循环的执行次数是 O(log K)。 - 内层循环与
mergeTwo
:这是分析的关键。对于外层循环的每一轮(即一个固定的step
值),内层循环和它调用的mergeTwo
函数合起来,会完整地遍历所有N
个节点一次。例如,当step=1
时,merge(0,1)
,merge(2,3)
… 这些操作的总和是遍历所有节点。当step=2
时,merge(0,2)
,merge(4,6)
… 这些操作的总和还是遍历所有节点。因此,每一轮的代价是 O(N)。
综合分析:
总的时间复杂度 = (轮次) * (每轮的代价) = O(log K) * O(N)
。因此,最终时间复杂度为 O(N log K),其中 N
是总节点数,K
是链表数。
空间复杂度:O(1)
- 主要存储开销:该算法是迭代的,没有使用递归,因此没有递归栈的开销。
- 辅助数据结构:算法没有使用优先队列或任何其他与输入规模成比例的数据结构。
mergeTwo
函数只使用了dummy
和cur
等常数个指针。 - 原地操作:算法在输入的
lists
数组上原地进行修改。
综合分析:
算法所需的额外辅助空间是常数级别的。因此,其空间复杂度为 O(1)。这是该解法相比于递归分治法和优先队列法的最大优势,也是此问题的最优空间复杂度解法。
参考灵神