这个博客系列记录我刷LeetCode过程中的一些循序渐进的思路和想法,希望能坚持下去。如果读者老爷觉得有帮助,就点个赞吧。
题目描述
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/add-two-numbers著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
题目解读及思路
这个题考的是最简单的单链表的使用。题目已经说明了链表是非空的且表示的整数非负,那么在考虑过程中就可以少考虑这两点了。
需要注意的几点:
1.两数的位数相同,最后结果不用进位;
2.位数相同,最后结果需进位;
3.位数不同,最后结果需进位。
当然,算法好的话,实现时根本不用单独考虑这几点。
思路1
创建一个结果链表头指针result,一个指向新结点的指针pr,两个数据指针p1,p2分别指向两个单链表。
在循环体里,先判断p1和p2是否都到表尾了,是则退出循环,否则执行加法操作,创建结果结点,设置进位标志。
python代码:
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
result = ListNode(0)
pr = result
p1, p2 = l1, l2
carryIn = 0 #进位标志
preNode = pr #前导结点指针
while True:
if p1 == None and p2 == None: #p1和p2都完了,退出循环
break
if pr == None: #pr为空,先创建结果结点
pr = ListNode(0)
preNode.next = pr
preNode = pr
if p1:
pr.val += p1.val
if p2:
pr.val += p2.val
pr.val += carryIn
carryIn = 1 if pr.val > 9 else 0
pr.val %= 10
if p1: # 后移p1、p2
p1 = p1.next
if p2:
p2 = p2.next
preNode = pr
pr = pr.next
if carryIn: #都加完了后,还有进位信号
pr = ListNode(1)
preNode.next = pr
return result
这个代码的运行时间是100ms,只超过了11.03%的人,肯定还有改进的地方。
思路1改进
上面的代码中,使用了pr、preNode两个指针来生成结果链表,这是因为一开始我想的是第一个结点就开始存结果,而且因为python对象的特性,不能只用一个pr来创建这个链表。这样做的话,就带来了很多麻烦,循环体中的
if pr == None: #pr为空,先创建结果结点
pr = ListNode(0)
preNode.next = pr
preNode = pr
除了第一次循环,pr为假,后面一直都是真,这就导致效率低下。所以,将结果链表第一个结点作“头结点”,这样每次循环都不用判断pr了,直接为pr创建一个新的结点。
其次,再观察代码,循环体中前后有两处判断p1和p2是否为空,在移动p1、p2前,同一个循环体中判断结果肯定相同,理所当然的应该将它们分别合并。
再次,再观察代码,carryIn信号只取01是不是效率很低?为什么不直接把这个信号同时作进位和加法结果用?同时,循环体完后的那个if代码块,也可以加进循环体中。于是,改进后的代码如下:
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
pr = result = ListNode(0)
carryIn = 0
while l1 or l2 or carryIn:
carryIn += (l1.val if l1 else 0) + (l2.val if l2 else 0)
pr.next = ListNode(carryIn % 10)
pr = pr.next
carryIn //= 10 # 除以10,再向下取整,0或1
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
return result.next
这个代码的运行时间是68ms,战胜了90.67%的人,勉强还行吧(强行鼓励自己一下),之余更快的40ms和50ms档的人,可能是他们的代码写的更贴合硬件,所以更快。
C语言代码
温故而知新,好久没用过C了,用C实现了一下,也是折腾了好久,才改完错误。
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){
struct ListNode *head, *ph, *pre;
pre = (struct ListNode*) malloc (sizeof(struct ListNode));
head = pre;
head->next = NULL;
int carryIn = 0;
while(l1 != NULL || l2 != NULL || carryIn)
{
if (l1 != NULL)
{
carryIn += l1->val;
l1 = l1->next;
}
if (l2 != NULL)
{
carryIn += l2->val;
l2 = l2->next;
}
ph = (struct ListNode*) malloc (sizeof(struct ListNode));
ph->val = carryIn % 10;
ph->next = NULL;
pre->next = ph;
pre = ph;
carryIn = floor(carryIn / 10.0f);
}
return head->next;
}
这个C代码运行时间是36ms,确实比python快多了,但也是垫底,再观察代码,肯定还有改进的地方。
1.循环体中最后一句用了floor函数,可以直接除以10,即
carryIn /= 10,反正整数相除也是类似向下取整的机制。
2.循环倒数第三句,没必要每次循环都执行,反正下一次循环这个next会被赋值,所以可以把这一句拿出来在循环完之后执行,收尾。
改进之后:
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){
struct ListNode *head, *ph, *pre;
pre = (struct ListNode*) malloc (sizeof(struct ListNode));
head = pre;
head->next = NULL;
int carryIn = 0;
while(l1 != NULL || l2 != NULL || carryIn)
{
if (l1 != NULL)
{
carryIn += l1->val;
l1 = l1->next;
}
if (l2 != NULL)
{
carryIn += l2->val;
l2 = l2->next;
}
ph = (struct ListNode*) malloc (sizeof(struct ListNode));
ph->val = carryIn % 10;
pre->next = ph;
pre = ph;
carryIn /= 10;
}
ph->next = NULL;
return head->next;
}
改进后执行时间是16ms,提升了20ms。
如果链表不是逆序,是正着来怎么办?
这样的话,我的初步想法是先把链表中的元素分别按序入栈,然后在循环体中用这两个栈来进行操作。这样其实也就多了前面入栈的开销,循环体中出栈和指针后移开销应该差不多。