【算法-LeetCode】2. 两数相加(双指针/同步指针;链表)

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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的题解(双指针)

其实这道题的思路还是比较简单的,但是需要考虑许多细节,所以编码工作其实并不是非常地顺利,我也是经过两次错误的提交后才发现正解的。

大体过程可看下图:

在这里插入图片描述

【图1】l1和l2都还没到头,p和q始终是同步运动的,而r指针则始终慢一步

在这里插入图片描述

【图2】l1已经到头,long指针接替q指针完成后面的遍历

总体思路就是先让 result 链表承担接收逐位计算所得的结果(当然这个结果是 l1l2 相应位上的节点值相加并取余之后的结果),并将这个结果生成节点往 result 链表上加,result 始终指向辅助头结点。而 r 指针则始终指向尾结点的前驱节点。

carrier 用于记录当前位的计算结果的进位(要么是 0,要么是 1),要注意的是,这个 carrier 是服务于下一位的计算的,所以需要用一个 temp 变量来做临时中转(或者说是暂存)。

这个 temp 的用法和之前的【【算法-LeetCode】1143. 最长公共子序列(动态规划;滚动数组;通用的空间优化)_赖念安的博客-CSDN博客】中的 leftTop 的用法是一样的。

如果 l1l2 的长度不一样, pq 指针开始时同步往后移动(这部分操作就相当于是下面程序中的第一个 while 循环),那 pq 指针总归会有一个先到达尾部,此时,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

参考:两数相加 - 两数相加 - 力扣(LeetCode)

【更新结束】

有关参考

暂无

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值