【刷透Leetcode热题100】2.两数相加

题目描述

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

 一、Java解法

/**
 * 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 addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(0); // 创建一个虚拟头节点
        ListNode current = dummyHead; // 初始化当前节点为虚拟头节点

        int carry = 0; // 进位

        // 遍历链表,直到两个链表都为空且没有进位
        while (l1 != null || l2 != null || carry != 0) {
            int sum = carry;
            if (l1 != null) {
                sum += l1.val;
                l1 = l1.next;
            }
            if (l2 != null) {
                sum += l2.val;
                l2 = l2.next;
            }
            carry = sum / 10; // 更新进位
            current.next = new ListNode(sum % 10); // 将当前位的值添加到结果链表中
            current = current.next; // 更新当前节点为下一个节点
        }

        return dummyHead.next; // 返回结果链表的头节点
    }
}

二、Java复盘

1.利用sentinel(虚拟头节点)的概念构建空链表

        本题,我们新建了一个ListNode来存储答案。在一开始我们创建一个虚拟头节点,等待后续增加答案值。并为其创建一个指针current,通过current.next来进行更新。

        这给了我们一个启示,即做题可以从创建返回值的数据结构入手。思考如何对其进行添加、遍历,用哪些条件来判断。

2.进位更新

        在本题中,我们学会了如何用程序进行进位的更新,以及从不同位数字求整体sum值。其核心在于每次循环中sum/10是进位数(sum是整数,这里会舍去余数),sum%10是位数上的值(将其存储在链表里)。

        每次遍历中,sum➗10 = carry ……num(限制sum每次遍历最多是两位数,carry不是0就是1)

3.循环条件

        此处循环条件也很巧妙地考虑到了edge case——如果最后把两个链表都遍历完了,还有余数,需要加一个node存储。

        所以循环条件不仅是要把两个链表都循环到,还要保证我们的进位数carry归零。这给了我们启示,即遍历时要特别考虑两头(开始和结束)的特殊情况如何处理积累常用的解决问题的方法,提升编程素质和代码直觉。

三、Python解法

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):
    def addTwoNumbers(self, l1, l2,carry = 0):
        """
        :type l1: ListNode
        :type l2: ListNode
        :rtype: ListNode
        """
        if l1 is None and l2 is None:
            return ListNode(carry) if carry else None
        if l1 is None:
            l1,l2 = l2,l1
        carry += l1.val + (l2.val if l2 else 0)       
        l1.val = carry % 10
        l1.next = self.addTwoNumbers(l1.next,l2.next if l2 else None,carry // 10)
        return l1   

 四、Python复盘

1.学习巧妙运用递归

        递归和遍历都是解决问题的有效方法,它们各自适用于不同的场景和问题。下面是它们的一些使用场景:

        递归:

  1. 问题具有递归结构: 当问题的解决方案可以通过重复应用相同的方法来构建,且每一步都是同一问题的简化版本时,递归是一个很自然的选择。例如,树的遍历、图的深度优先搜索等都可以用递归来实现。

  2. 代码简洁清晰: 在某些情况下,递归可以使代码更加简洁和清晰。特别是对于一些数学问题或者具有明显递归性质的问题,递归代码往往更容易理解和实现。

  3. 问题规模可控: 在使用递归时,需要确保问题的规模可以逐步减小,否则可能导致栈溢出或者无限递归的问题。因此,在使用递归时,需要谨慎设计递归终止条件,确保问题最终能够得到解决。

        遍历:

  1. 线性结构或者迭代操作: 当问题的解决方案可以通过迭代遍历数据结构来实现时,遍历往往是一个更好的选择。例如,对于数组、链表等线性结构,通常使用迭代来实现各种操作,如查找、排序、过滤等。

  2. 节省空间: 在一些情况下,遍历比递归更节省空间。递归往往需要使用额外的栈空间来存储每一层的调用信息,而遍历则可以通过循环来实现,节省了额外的空间开销。

  3. 性能要求较高: 一些场景下,对性能要求较高,遍历往往比递归更高效。递归可能会因为函数调用的开销而导致性能下降,而遍历则可以通过迭代一次性完成所有操作,提高效率。

        综上所述,递归和遍历各有适用的场景。在选择使用哪种方法时,需要考虑问题的特点、数据结构的类型、性能要求以及代码的可读性等因素。

2.条件:两个链表都不为空,返回的链表不为空

3.交换语句:l1,l2 =  l2,l1

4.直接把l1当作返回的链表并赋值

5.辅助参数carry的使用

        注意edge case:l2 == 0。

6.尝试使用return a if 条件 else b

五、C++解法

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        int carry = 0;
        ListNode *head = nullptr, *tail = nullptr;
        while (l1 || l2) {
            int n1 = l1 ? l1->val : 0;
            int n2 = l2 ? l2->val : 0;
            int sum = n1 + n2 + carry;
            // 第一次计算,头节点和尾节点是同一个
            if (!head) {
                head = tail = new ListNode(sum % 10);
            } else { // 后面的计算,只移动尾节点,头节点不动
                tail->next = new ListNode(sum % 10);
                tail = tail->next;
            }

            carry = sum / 10;
            if (l1) {
                l1 = l1->next;
            }
            if (l2) {
                l2 = l2->next;
            }
        }
        if (carry > 0) {
            tail->next = new ListNode(carry);
        }
        return head;
    }
};

六、C++复盘

1.指针

当谈论C++指针时,我们实际上是在谈论一种非常强大的工具,它允许我们直接访问内存中的位置。这是一种基本的数据类型,可以存储其他数据类型的内存地址。以下是一些常见的C++指针用法:

  1. 声明指针:要声明指针,您需要在变量名前面放置星号(*)。例如:int *ptr; 声明了一个指向整数的指针。

  2. 初始化指针:可以将指针初始化为另一个变量的地址,或者将其初始化为nullptr(空指针,即指向任何有效对象的地址都没有)。例如:int *ptr = nullptr; 或者 int x = 10; int *ptr = &x;(将ptr指向变量x的地址)。

  3. 访问指针所指向的值:使用解引用操作符(*)可以访问指针所指向的值。例如:int x = *ptr; 将会把指针ptr指向的值赋给变量x。

  4. 指针算术:可以对指针执行算术运算,如加法和减法。例如:ptr++ 将会使指针ptr指向下一个内存位置。

  5. 数组和指针:数组名本身就是一个指向数组第一个元素的指针。因此,可以通过指针来访问数组的元素。例如:int arr[5]; int *ptr = arr; 现在ptr指向了arr数组的第一个元素。

  6. 指针和函数:可以将指针作为参数传递给函数,从而在函数内部操作指针所指向的值。这可以用于实现函数修改调用者的变量值,而不是创建副本。例如:void modifyValue(int *ptr) { *ptr = 20; }

  7. 动态内存分配:使用new关键字可以在堆上动态分配内存,并返回指向分配内存的指针。例如:int *ptr = new int; 创建了一个int类型的动态分配内存,并将其地址赋给指针ptr。记得用delete释放这块内存,以避免内存泄漏。

        这些是C++中指针的基本用法。使用指针时需要小心,因为它们直接操作内存,可能导致一些难以追踪的错误。

        本题中,利用了c++空指针概念,更强大的指针操作空间。

2.链表指针:l1 -> next 

3.赋值连等式 head = tail = ListNode(sum%10)

七、思路与问题

1.复习int carry 代表进位的使用方法。sum/10 是进位,sum%10是没进位的数值。

2.两数之和操作中,巧用递归。

3. 熟悉三元运算符。 

        java,c++ : int result = (condition) ? value_if_true : value_if_false;
        python:       result = value_if_true if condition else value_if_false


4.判断条件:两个列表都不为空,且进位满足。

5.思考方法总结

        三个问题:用什么数据结构来存储答案并返回?使用什么判断条件如何使用遍历or递归?遍历的最后一项到哪里?

        每一个题都可以从返回值入手考虑。先建立存储答案的数据结构,然后思考如何通过递归/遍历来进行逐步操作。

        本题中,返回值是一个链表或者一个指向链表的指针。求两数之和,存在进位和余数的问题,用carry= sum/10代表进位,用sum%10代表留下的数。遍历/递归时要考虑两个链表都要完整的遍历,并且最后一位进位要考虑到,故判断条件就是两个链表都非空并且进位carry == 0.

        c++的做法最为简单粗暴,直接设立两个nullptr,然后从头开始遍历,利用独特的语言优势轻松解决问题。python做法最为优雅简洁,调用递归,没有指针参与,复杂度较小。java很系统,几乎是c++版本的去指针操作,比较tricky的是建立sentinel并且返回sentinel.next。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值