LeetCode 热题 HOT 100(01,两数相加)

LeetCode 热题 HOT 100(01,两数相加)

不够优秀,发量尚多,千锤百炼,方可成佛。

算法的重要性不言而喻,无论你是研究者,还是最近比较火热的IT 打工人,都理应需要一定的算法能力,这也是面试的必备环节,算法功底的展示往往能让面试官眼前一亮,这也是在大多数竞争者中脱颖而出的重要影响因素。

然而往往大多数人比较注重自身的实操能力,着重于对功能的实现,却忽视了对算法能力的提高。有的时候采用不同的算法来解决同一个问题,运行效率相差还是挺大的,毕竟我们最终还是需要站在客户的角度思考问题嘛,能给用户带来更加极致的体验当然再好不过了。

万法皆空,因果不空。Taoye之前也不怎么情愿花费太多的时间放在算法上,算法功底也是相当的薄弱。这不,进入到了一个新的学习阶段,面对导师的各种“严刑拷打”和与身边人的对比,才开始意识到自己“菜”的事实。

讲到这,流下了没技术的眼泪!!!

LeetCode 热题 HOT 100(01,两数相加)

这次的题目是LeeTCode 热题 HOT 100的第二题,难度属于中等,涉及到了链表的知识。

自打接触Python以来,都没有从中用到过链表,也无法通过指针来操作链表。曾经也只是在备考408,学习C的过程中刷过一些链表相关算法,一开始拿到这道题的时候,不知道Python如何下手,不知道怎么操作链表,菜是原罪(ノへ ̄、)

查找资料之后才发现,我们操作链表的时候其实就是将其封装成一个实例对象,而实例对象中存储了节点的相关信息(其实和C中差不多)。但与C或C++中不同的是,Python没有指针,也就没有什么指向操作,所以从对链表的整体结构理解上,Python和C、C++还是有点区别的。意思类似下图:

LeetCode 热题 HOT 100(01,两数相加)

可以理解成一种俄罗斯套娃的形式。

注意:只是理解上有所区别,其实表达的意思还是一样的。

下面,我们就来看看这道题吧。

题目:两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

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

示例

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

思路

前面也提到了,在Python中,链表可以理解成一种套娃的形式,通过输入两个链表,输出一个新的链表,新链表中的每个节点的产生过程基本一致,所以我们初步判断通过递归的方法来解决该问题。

当然了,递归能解决的问题,非递归同样可以,只是将一次递归函数的调用替换成一次循环。所以下面将分别介绍两种方法。

  • 方法一:递归

题中给出的例子里的两个链表的长度是一样的,但题意所表明的意思是任意长度的两个链表。所以,为了提高算法的“泛化性能”,在输入两个长度不同链表的前提下,我们需要对其进行一个统一,就是对长度短的链表我们需要做一个补0处理。补0并不影响求和操作,但又方便了我们对截止递归的判断。

这种处理方式在数学上还是挺常见的,比如说在求最值的时候,我们常常会用到均值不等式,但题目中给出的数值维数可能并不会明显地满足均值不能式,所以常常会用到一个扩维的操作,很巧妙的将题目向均值不等式靠近,从而简化了对题目的解答,比如下面这道题的巧妙解答:

LeetCode 热题 HOT 100(01,两数相加)

LeetCode 热题 HOT 100(01,两数相加)

最后,左右同开5次方,再同乘27即可证毕。

回到算法题吧。

题目既然要我们求链表所对应的单位数之和,那么肯定会涉及进位,我们不妨将进位表达为carry。至此,我们不难想到,每次的递归需要的参数有三个,分别是第一个链表中的值、第二个链表中的值、上一次进位的值(非0即1)。

前面也有说到,在Python中,链表其实就是一个对象,而该对象中又有俩个属性,分别是valnext,而next本身有代表一个链表对象。所以我们递归方法传入的第一、第二个参数可以用当前链表节点所获取到。

判断跳出递归的条件:前面,我们已经对链表进行处理过了,也就是说无论你输入的两个链表长度是否一致,我们都对其进行了统一长度的处理。所以说当我们所传入的三个参数同时为False的时候即可跳出递归。举例说明: 1、输入两个空链表和一个carry为1的参数,说明已经产生了进位,此时我们需要对其进行处理,而非跳出递归。2、输入的一个链表节点value值为5,而另一个节点为None,carry为0,此时我们依然需要进行求和处理,即使另一个节点为None,但我们已经对其进行了补0处理。3、至于其他可能性,各位看官可自行思考。

求和处理:

  1. 求出两个链表当前value值和carry的和,并对10进行一个divmod操作。说明: Python中divmod会返回一个元组,第一个值为进位,第二个值为取余,比如divmod(13, 10) = (1, 3)。 注意:在求和的过程中,有可能当前节点为None,这个时候则需要将节点的value值作为0进行求和。
  2. 实例化一个链表,也就是我们的目标返回链表,其value值为上述的取余。注意:这里一定理解清楚题意,链表中的值是实际值的逆序。
  3. result的next属性所对应的值本身又是一个链表,而对其进行赋值的时候,我们需要调用递归方法。传递的三个参数:经过上述过程,链表中的当前值已经处理完毕,这个时候需要处理链表的下一个值,也就是将下一个节点作为参数进行一次递归。但这里需要考虑的是,假如我们的当前节点为空,也就是说当前链表已经遍历结束,则需要传递None给递归函数。

相关代码:

class Solution:
    def recursion(self, list_node1, list_node2, carry):
        # 判断跳出递归的条件
        if (list_node1 == None) and (list_node2 == None) and (carry == 0): return None
        # 两个节点值和进位值的求和操作,若传入的节点为None,则需要将value赋值为0进行求和,也就是补0
        sum_number = (list_node1.val if list_node1 else 0) + (list_node2.val if list_node2 else 0) + carry
        # 产生新的carry进位值,作为下一次递归的判断。
        carry, value = divmod(sum_number, 10)
        # 实例新的节点(result,为返回的目标节点的),其val属性为value
        result = ListNode(value)    
        # 进行下一次递归
        result.next = self.recursion(list_node1.next if list_node1 else None, list_node2.next if list_node2 else None, carry)
        return result

    # 主函数
    def addTwoNumbers(selfl1: ListNode, l2: ListNode) -> ListNode:
        return self.recursion(l1, l2, 0)    # 执行入口,调用递归函数,传入初试的两个节点,默认进位为0
  • 方法二:非递归

非递归的思想和递归差不多,只是除了返回result链表之外,还需要额外创建一个新的链表用于循环。

class Solution:
    def addTwoNumbers(selfl1: ListNode, l2: ListNode) -> ListNode:
        # 初始node,result为返回结果链表,temp_node用作循环遍历,链接产生新的节点
        result = temp_node = ListNode(0)    
        carry = 0   # 初始进位为0
        while l1 or l2 or carry:    # 跳出循环的条件和递归一样,三者同时为False的时候跳出循环
            # 两个节点值和进位值的求和操作,若传入的节点为None,则需要将value赋值为0进行求和,也就是补0
            sum_number = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
            # l1和l2当前节点操作完毕,指向下一个节点,准备进入下一次循环
            l1 = l1.next if l1 else None; l2 = l2.next if l2 else None
            # 产生新的carry进位值,作为下一次递归的判断。
            carry, value = divmod(sum_number, 10)
            # 重新赋值temp_node节点,不断为result链表进行延伸补充节点
            temp_node.next = ListNode(value); temp_node = temp_node.next
        return result.next

总的来说,非递归表达的思想和递归差不多。

不过这里有一点值得说明下: result节点只在初始和最后返回时用到过,而在算法的核心while循环中并没有用到,为什么还能返回正常的result链表呢?

这主要是因为,我们初始化创建的result已经和temp_node都赋值给新的链表了,其中val为0,next属性值为None,其内存位置是固定的。而初试的result和temp_node是相等的,所指向的内存地址是一样的,所以我们只需要的延伸temp_node节点即可,而result地址不变,而result指向的下一个节点的地址则会随着temp_node的变化而变化。

注意: result的内存地址从始至终都是不变的,初试的时候和temp_node的地址是一样,只不过之后变化的是temp_node,而result不变。我们可以在通过如下操作简单测试下这个过程:

LeetCode 热题 HOT 100(01,两数相加)

也不知道自己表述清楚了没有?

也不知道各位看官理解了没有?

如果没看懂的话,强烈建议重新看一遍,细细琢磨下,这个地方还是挺重要的。

  • 最后

在逛讨论区的时候,看到了另外一种解法,感觉这算法写的挺优雅的,在这里分享下。

"""
    Author:学废了的Kai
"""

class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        head = curr = ListNode()
        carry = val = 0
        while carry or l1 or l2:
            val = carry
            if l1: l1, val = l1.next, l1.val + val
            if l2: l2, val = l2.next, l2.val + val
            carry, val = divmod(val, 10)
            curr.next = curr = ListNode(val)
        return head.next

这种解法和上面非递归表达的是同种思想,只不过在求sum的时候这个是叠加的形式,而非一次性求和。上面算法还有一点值得学习的是,在最后curr.next = curr = ListNode(val)这行代码中,其表达的意思是如下:

curr.next =ListNode(val)
curr = curr.next

Taoye也不知道为什么是这种赋值顺序,按道理讲应该是先赋值curr = ListNode(val),再赋值curr.next = curr?但经过测试之后,上述两行代码块才是正确的,暂时不知道为什么,之后有机会再回过头看看吧。

我是Taoye,研究生在读。爱专研,爱分享,热衷于各种技术,学习之余喜欢下象棋、听音乐、聊动漫,希望借此一亩三分地记录自己的成长过程以及生活点滴,也希望能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder

推荐阅读:

LeetCode 热题 HOT 100(00,两数之和)
Taoye渗透到一家黑平台总部,背后的真相细思极恐
《大话数据库》-SQL语句执行时,底层究竟做了什么小动作?
那些年,我们玩过的Git,真香
基于Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度学习环境
网络爬虫之页面花式解析
手握手带你了解Docker容器技术
一文详解Hexo+Github小白建站
​打开ElasticSearch、kibana、logstash的正确方式

分类:

技术点:

相关文章: