Problem: 2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
整体思路
这段代码旨在解决一个经典的链表问题:两数相加 (Add Two Numbers)。问题要求将两个由链表表示的非负整数相加,其中链表的每个节点包含一个数字,并且数字是逆序存储的。例如,链表 2 -> 4 -> 3
代表数字 342。
该实现采用了一种非常优雅的 递归 方法来模拟逐位相加的过程。其核心逻辑如下:
-
递归函数设计:
- 定义一个辅助的递归函数
addTwo(l1, l2, carry)
,它接收两个链表节点和上一位的进位carry
作为参数。 - 该函数的作用是计算当前位的和,并递归地调用自身来处理下一位的相加,最终返回构建好的结果链表的当前节点。
- 定义一个辅助的递归函数
-
递归的终止条件 (Base Case):
if (null == l1 && null == l2 && 0 == carry)
:这是递归的最终出口。当两个输入链表都已遍历完毕(都为null
),并且没有来自上一位的进位时,说明加法过程彻底结束,应该返回null
,表示结果链表的末尾。
-
递归的递推关系 (Recursive Step):
- 计算当前位的和:
int s = carry;
。首先将和s
初始化为上一位的进位。然后,如果l1
或l2
不为空,就将其节点值加到s
中,并将其指针后移一位。 - 创建新节点:计算出的总和
s
可能大于9。当前位的值应该是s % 10
。因此,创建一个新节点new ListNode(s % 10, ...)
。 - 递归调用:新节点的
next
指针应该指向下一位加法的结果。这个结果通过递归调用addTwo(l1, l2, s / 10)
来获得。这里的s / 10
就是要传递给下一位的新的进位。 - 返回当前节点:函数返回新创建的、已连接好后续部分的节点。
- 计算当前位的和:
通过这种自顶向下的递归方式,代码从链表的头部(个位)开始,逐位向高位处理,自然地构建出结果链表。
完整代码
/**
* 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 {
/**
* 主函数,启动两数相加的过程。
* @param l1 第一个数对应的链表
* @param l2 第二个数对应的链表
* @return 相加结果对应的链表
*/
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 初始调用递归函数,进位为 0
return addTwo(l1, l2, 0);
}
/**
* 递归辅助函数,执行一位的相加并链接后续结果。
* @param l1 当前 l1 的节点
* @param l2 当前 l2 的节点
* @param carry 来自上一位的进位
* @return 构建好的当前节点
*/
private ListNode addTwo(ListNode l1, ListNode l2, int carry) {
// 递归终止条件:两个链表都已遍历完,且没有进位
if (null == l1 && null == l2 && 0 == carry) {
return null;
}
// 计算当前位的总和,初始为上一位的进位
int s = carry;
// 如果 l1 链表还有节点,加上其值,并移动指针
if (null != l1) {
s += l1.val;
l1 = l1.next;
}
// 如果 l2 链表还有节点,加上其值,并移动指针
if (null != l2) {
s += l2.val;
l2 = l2.next;
}
// 创建新节点:
// 节点的值是和的个位数 (s % 10)
// 节点的 next 指针是递归调用处理下一位的结果,
// 将新的进位 (s / 10) 传递下去。
return new ListNode(s % 10, addTwo(l1, l2, s / 10));
}
}
时空复杂度
时间复杂度:O(max(M, N))
- 递归深度:递归函数
addTwo
的调用次数由较长的链表决定。如果l1
的长度是M
,l2
的长度是N
,那么递归的深度最多是max(M, N)
次(如果最后还有进位,则再多一次)。 - 每次操作:在每次递归调用中,执行的都是常数时间的加法、取模、除法和指针移动操作。
综合分析:
算法的时间复杂度与两个链表中较长者的长度成线性关系。因此,时间复杂度为 O(max(M, N))。
空间复杂度:O(max(M, N))
- 递归栈空间:由于使用了递归,函数调用会占用调用栈的空间。递归的深度是
max(M, N)
,所以递归栈所需的空间复杂度是 O(max(M, N))。 - 结果链表空间:算法创建了一个新的链表来存储结果。这个新链表的长度也是
max(M, N)
(可能加1)。这部分空间通常被视为输出,不计入辅助空间,但它确实是算法使用的内存。
综合分析:
算法的额外空间主要由递归调用栈决定。因此,空间复杂度为 O(max(M, N))。
参考灵神