本题采用双指针迭代算法(又称“哑节点辅助归并法”)解决两个有序单链表的合并问题。其核心本质是利用单调递增的先验条件,通过双指针同步线性扫描,在常数阶额外空间内将两个空间独立的拓扑序列融合成全新的全局有序链表。当前提供的源码实现了在时间复杂度 O(M + N) 和额外空间复杂度 O(1) 条件下的全局最优线性检索,最终走向是高效拼接所有节点并返回新链表的头节点。
一、 问题本质与数据模型
对于给定的两个单链表list1和list2,它们在物理拓扑结构上彼此独立,但在数据分布上满足相同的单调性约束:均按非递减顺序排列(即node.val <= node.next.val)。
若采用常规的随机构建或重新排序,将破坏原有的局部有序性,导致算力浪费。算法通过引入两个核心机制破除信息无序困局:
“哑节点(Dummy Node)定位模型”:在合并初期,由于无法预知两链表的首节点孰小,直接确定新链表的头节点会引入复杂的边界判定。通过开辟一个虚拟的
dummy节点作为新链表的常驻起点,可以将“头节点的初始化”抽象为常规的“追加后续节点”操作,从而实现控制流的归一化。“双指针线性归并机制”:设置两个指针分别指向两个链表的未处理头部,每次比较当前节点的数值。由于两链表局部有序,当前较小者必然是全局未合并元素中的极小值,将其拼接到新链表尾部能绝对保障新链表的升序物理形态。
二、 算法演进对比
在解决有序链表合并问题时,迭代归并法在时空资源的利用效率上达到了理论极限:
| 解法名称 | 时间复杂度 | 空间复杂度 | 核心原理 | 物理瓶颈 / 缺陷 |
|---|---|---|---|---|
| 暴力重组排序法 | O((M+N) log(M+N)) | O(M+N) | 将两个链表的所有节点值全部复制到数组中,通过快速排序后重新物理构建一条新链表 | 完全忽略了原链表有序的先验条件,产生了高昂的排序时间损耗与外部存储空间开销 |
| 递归归并法 | O(M + N) | O(M + N) | 将问题拆解为头节点的比较,剩余部分交由递归函数处理,利用隐含的系统栈实现挂载 | 递归调用会在系统栈中产生深度为 M + N 的栈帧开销,在超长链表下有引发栈溢出的物理风险 |
| 迭代归并法(当前解法) | O(M + N) | O(1) | 利用哑节点和双指针进行原地指针重定向,单遍线性扫描即完成物理拼接 | 指针交替推进,需要严密防控链表触底时的空指针判定逻辑 |
三、 核心分支控制逻辑与决策证明
当前源码的控制流完全依赖于while (list1 != null && list2 != null)边界防护网以及后续的尾部残留挂载,其内部决策分支证明如下:
1. 局部极小值命中分支:if (list1.val < list2.val)
执行:
cur.next = list1; list1 = list1.next;数学证明:已知两链表当前剩余部分皆为升序。若
list1.val < list2.val,则list1.val严格小于list2集合中的所有剩余元素;同时,list1.val也小于或等于list1后续的所有元素。结论:
list1当前节点是所有未合并节点中的绝对极小值,将其作为新链表的下一节点在数学上完全合规。执行步进,将list1指针后移。
2. 补集极小值命中分支:else(即list1.val >= list2.val)
执行:
cur.next = list2; list2 = list2.next;数学证明:同理,当
list2.val小于或等于list1.val时,list2.val必然小于或等于两个链表剩余集合中的任意元素。结论:
list2当前节点满足全局极小值条件,将其拼接到cur.next,并将list2指针后移。
3. 残留段物理挂载:cur.next = (list1 != null) ? list1 : list2;
执行:当
while循环因其中一个链表触底(变为null)而终止时,直接将另一非空链表挂载至cur.next。数学证明:由于两个链表本身严格升序,当其中一个链表被完全耗尽时,另一个链表中的所有剩余节点必然都大于或等于已合并链表中的最大元素,且其内部已维持升序拓扑结构。
结论:无需继续遍历对比,直接执行 O(1) 级别的指针挂载即可保障全局有序,流程提前终结。
四、 算法执行状态机步进示例
以输入链表list1 = [1, 2, 4],list2 = [1, 3, 4]为例,指针状态机的演进过程如下表所示(dummy初始值设为 0):
| 步骤 | list1当前指向 | list2当前指向 | 比较状态与分支 | 执行的核心剪枝与挂载动作 | 新链表当前拓扑状态 (dummy.next开始) |
|---|---|---|---|---|---|
| 初始 | 1 (->2->4) | 1 (->3->4) | 进入循环边界 | 初始化cur指向dummy | dummy |
| 1 | 1 (->2->4) | 1 (->3->4) | 1 >= 1 (进入 else) | cur.next指向list2的 1;list2后移 | 1 (来自list2) |
| 2 | 1 (->2->4) | 3 (->4) | 1 < 3 (进入 if) | cur.next指向list1的 1;list1后移 | 1 -> 1 |
| 3 | 2 (->4) | 3 (->4) | 2 < 3 (进入 if) | cur.next指向list1的 2;list1后移 | 1 -> 1 -> 2 |
| 4 | 4 (->null) | 3 (->4) | 4 >= 3 (进入 else) | cur.next指向list2的 3;list2后移 | 1 -> 1 -> 2 -> 3 |
| 5 | 4 (->null) | 4 (->null) | 4 >= 4 (进入 else) | cur.next指向list2的 4;list2变为null | 1 -> 1 -> 2 -> 3 -> 4 (来自list2) |
| 6 | 4 (->null) | null | 循环退出判定 | 触发残留挂载,cur.next直接指向list1的 4 | 1 -> 1 -> 2 -> 3 -> 4 -> 4 |
五、 源码实现
/** * Definition for singly-linked list. * public 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 { public ListNode mergeTwoLists(ListNode list1, ListNode list2) { // 创建独立哑节点 dummy,用作新链表的固定的控制流哨兵 ListNode dummy = new ListNode(0); // 初始化当前工作指针 cur,指向哑节点 ListNode cur = dummy; // 双指针同步推进:当两个链表均未触底时执行线性比对 while (list1 != null && list2 != null) { // 分支 1:list1 节点值更小,挂载 list1 并将指针后移 if (list1.val < list2.val) { cur.next = list1; list1 = list1.next; } // 分支 2:list2 节点值更小或相等,挂载 list2 并将指针后移 else { cur.next = list2; list2 = list2.next; } // 新链表工作指针向后步进 1 位 cur = cur.next; } // 边界处理:将尚未遍历完毕的非空链表段直接挂载到新链表尾部 cur.next = (list1 != null) ? list1 : list2; // 返回新链表的物理真实头节点 return dummy.next; } }六、 复杂度分析
1. 时间复杂度:O(M + N)
分析:设
list1的长度为 M,list2的长度为 N。在while循环的每次迭代中,list1或list2的指针必定会向后推进 1 个节点,同时新链表的控制指针cur也会同步推进。因此循环的最大迭代次数为 M + N。当循环结束后,残留段的挂载操作耗时为常数阶 O(1)。结论:总的节点比较与指针重定向操作次数与两个输入链表的长度总和呈严格的线性正比关系。
2. 空间复杂度:O(1)
分析:算法在运行期间,除了创建了一个用于统一控制流边界的哑节点
dummy之外,仅在栈内存中开辟了cur一个用于节点迭代定位的引用变量。整个重组过程是通过直接修改输入链表节点的next指针域来实现的原地(In-place)拼接。结论:没有申请任何与输入链表规模相关的外部或临时数据结构,内存空间损耗恒定为常数阶。