题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 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.学习巧妙运用递归
递归和遍历都是解决问题的有效方法,它们各自适用于不同的场景和问题。下面是它们的一些使用场景:
递归:
-
问题具有递归结构: 当问题的解决方案可以通过重复应用相同的方法来构建,且每一步都是同一问题的简化版本时,递归是一个很自然的选择。例如,树的遍历、图的深度优先搜索等都可以用递归来实现。
-
代码简洁清晰: 在某些情况下,递归可以使代码更加简洁和清晰。特别是对于一些数学问题或者具有明显递归性质的问题,递归代码往往更容易理解和实现。
-
问题规模可控: 在使用递归时,需要确保问题的规模可以逐步减小,否则可能导致栈溢出或者无限递归的问题。因此,在使用递归时,需要谨慎设计递归终止条件,确保问题最终能够得到解决。
遍历:
-
线性结构或者迭代操作: 当问题的解决方案可以通过迭代遍历数据结构来实现时,遍历往往是一个更好的选择。例如,对于数组、链表等线性结构,通常使用迭代来实现各种操作,如查找、排序、过滤等。
-
节省空间: 在一些情况下,遍历比递归更节省空间。递归往往需要使用额外的栈空间来存储每一层的调用信息,而遍历则可以通过循环来实现,节省了额外的空间开销。
-
性能要求较高: 一些场景下,对性能要求较高,遍历往往比递归更高效。递归可能会因为函数调用的开销而导致性能下降,而遍历则可以通过迭代一次性完成所有操作,提高效率。
综上所述,递归和遍历各有适用的场景。在选择使用哪种方法时,需要考虑问题的特点、数据结构的类型、性能要求以及代码的可读性等因素。
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++指针用法:
-
声明指针:要声明指针,您需要在变量名前面放置星号(*)。例如:
int *ptr;
声明了一个指向整数的指针。 -
初始化指针:可以将指针初始化为另一个变量的地址,或者将其初始化为nullptr(空指针,即指向任何有效对象的地址都没有)。例如:
int *ptr = nullptr;
或者int x = 10; int *ptr = &x;
(将ptr指向变量x的地址)。 -
访问指针所指向的值:使用解引用操作符(*)可以访问指针所指向的值。例如:
int x = *ptr;
将会把指针ptr指向的值赋给变量x。 -
指针算术:可以对指针执行算术运算,如加法和减法。例如:
ptr++
将会使指针ptr指向下一个内存位置。 -
数组和指针:数组名本身就是一个指向数组第一个元素的指针。因此,可以通过指针来访问数组的元素。例如:
int arr[5]; int *ptr = arr;
现在ptr指向了arr数组的第一个元素。 -
指针和函数:可以将指针作为参数传递给函数,从而在函数内部操作指针所指向的值。这可以用于实现函数修改调用者的变量值,而不是创建副本。例如:
void modifyValue(int *ptr) { *ptr = 20; }
-
动态内存分配:使用
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。