LeetCode题目笔记--2.两数相加

这个博客系列记录我刷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,两个数据指针p1p2分别指向两个单链表。
在循环体里,先判断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。

如果链表不是逆序,是正着来怎么办?

这样的话,我的初步想法是先把链表中的元素分别按序入栈,然后在循环体中用这两个栈来进行操作。这样其实也就多了前面入栈的开销,循环体中出栈和指针后移开销应该差不多。

猜你喜欢

转载自blog.csdn.net/weixin_44801799/article/details/107405873