2. 两数相加 - 力扣(LeetCode)
发布:2021年9月28日14:57:18
问题描述及示例
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
提示:
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/add-two-numbers
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
我的题解(双指针)
其实这道题的思路还是比较简单的,但是需要考虑许多细节,所以编码工作其实并不是非常地顺利,我也是经过两次错误的提交后才发现正解的。
大体过程可看下图:
总体思路就是先让 result
链表承担接收逐位计算所得的结果(当然这个结果是 l1
和 l2
相应位上的节点值相加并取余之后的结果),并将这个结果生成节点往 result
链表上加,result
始终指向辅助头结点。而 r
指针则始终指向尾结点的前驱节点。
carrier
用于记录当前位的计算结果的进位(要么是 0
,要么是 1
),要注意的是,这个 carrier
是服务于下一位的计算的,所以需要用一个 temp
变量来做临时中转(或者说是暂存)。
这个
temp
的用法和之前的【【算法-LeetCode】1143. 最长公共子序列(动态规划;滚动数组;通用的空间优化)_赖念安的博客-CSDN博客】中的leftTop
的用法是一样的。
如果 l1
和 l2
的长度不一样, p
和 q
指针开始时同步往后移动(这部分操作就相当于是下面程序中的第一个 while
循环),那 p
和 q
指针总归会有一个先到达尾部,此时,long
指针就是用于接替那个还没到达尾部的指针的工作的(这部分操作就相当于是下面程序中的第二个 while
循环)。这两部分的逻辑大体相同。
详细解释请看下方注释:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
// p、q指针先指向两个链表的头部
let p = l1;
let q = l2;
// carrier用于记录当前位产生的进位以服务于下一位的计算
let carrier = 0;
// temp是服务于carrier的中转变量,用来暂存来自上一位计算时的进位
let temp = 0;
// result链表用于存储最终结果,每一位的计算结果都将作为节点追加到其尾部
// r指针用于指示result链表尾节点的前驱节点,方便我们操作result链表的尾部
let result = r = new ListNode(0, null);
// 开始时,p和q同步运动,这是第一阶段的链表遍历
while(p && q) {
// 在当前位的进位被覆盖前,先将其暂存在temp中
temp = carrier;
// 计算当前位产生的进位,如果p和q指针所指向的节点值之和
// 再加上前一位所产生的的进位大于10则产生一个进位,否则进位为0
carrier = p.val + q.val + temp >= 10 ? 1 : 0;
// 生成一个节点,节点值为p和q指针所指向的节点值之和加上前一位所产生的的进位再取余的结果
let node = new ListNode((p.val + q.val + temp) % 10, null);
// 将生成的该节点添加到result尾部
r.next = node;
// 分别让p、q、r三个指针向前走一步
p = p.next;
q = q.next;
r = r.next;
}
// 只要p、q指针中有一个指针到达了尾部,上面的循环就会停止,也就意味着第一阶段的工作完成了
// 此时让long指针来接替剩下那个还没到达链表(也就是更长的那个链表)结尾的指针的工作
let long = p ? p : q;
// 同时,为了节省节点空间,直接复用较长链表的后面一段
r.next = long;
// 开始遍历较长节点的剩余节点
while(long) {
// 下面两句和之前的逻辑是一样的
temp = carrier;
carrier = long.val + temp >= 10 ? 1 : 0;
// 更新当前节点的val值,因为我们复用了原有链表的节点,所以不用再创建新的节点对象
long.val = (long.val + temp) % 10;
// 让long和r指针往后走一步
long = long.next;
r = r.next;
}
// 下面这句是为了防止出现最高位9,然后加上前一位的进位后又产生进位,
// 此时就要创建一个新节点并设置节点值为1
r.next = carrier ? new ListNode(1, null) : null;
// 注意result指向的是辅助头结点,所以返回值不能直接返回result
return result.next;
};
提交记录
1568 / 1568 个通过测试用例
状态:通过
执行用时:120 ms, 在所有 JavaScript 提交中击败了62.04%的用户
内存消耗:42.7 MB, 在所有 JavaScript 提交中击败了92.67%的用户
时间:2021/09/28 15:02
提交之后,发现时间表现和空间表现都意外地不错,一开始还以为这种做法会因为消耗比较多的节点空间而表现较差。
空间优化(理论上)
既然上面的程序中复用了原本的节点,那么为什么一不做二不休,直接把复用其中某一条链表的全部节点呢?于是就有了下面的版本。下面程序的大体思路还是和上面的那种一样,只不过没有特意创建一个 result
链表来存储结果,而是选用了 l1
这条链表作为最终结果的载体(当然选 l2
也行,只要把相应的地方改一下即可)。
var addTwoNumbers = function(l1, l2) {
let p = l1;
let q = l2;
let carrier = 0;
let temp = 0;
while(p && q) {
temp = carrier;
carrier = p.val + q.val + temp >= 10 ? 1 : 0;
p.val = (p.val + q.val + temp) % 10;
// 注意这里我们提前判断了p、q的下一个节点是否为空,是为了之后的long指针和p指针的对接
if(!p.next || !q.next){
break;
}
p = p.next;
q = q.next;
}
let long = p.next ? p.next : q.next;
p.next = long;
while(long) {
temp = carrier;
carrier = long.val + temp >= 10 ? 1 : 0;
long.val = (long.val + temp) % 10;
long = long.next;
p = p.next;
}
p.next = carrier ? new ListNode(1, null) : null;
// 注意返回结果不是p,因为p指针会移动,而l1则始终指向链表头部
return l1;
};
提交记录
1568 / 1568 个通过测试用例
状态:通过
执行用时:116 ms, 在所有 JavaScript 提交中击败了74.20%的用户
内存消耗:43.3 MB, 在所有 JavaScript 提交中击败了24.72%的用户
时间:2021/09/28 17:42
理论上来说,因为上面的程序没有除了末尾的那个进位节点可能会被创建,其他的节点都是复用 l1
的节点,所以空间表现应该会更好,但是奇怪的是,提交之后发现内存消耗似乎还更多了?真是魔幻……😂,不过我向来也不是很相信LeetCode的性能判断,应该有些题目的结果真的是让人摸不着头脑,只要理论分析过关应该就行了吧……
官方题解
更新:2021年7月29日18:43:21
因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。
更新:2021年9月28日15:03:57
【更新结束】
有关参考
暂无