leetcode 兩數相加(add two numbers) Python程式設計實現

王平發表於2019-03-25

上次推出這個用Python刷題leetcode系列後,有人喜歡有人厭,畢竟眾口難調。這篇用Python實現兩數相加。

寫猿人學Python教程之餘,也寫一個leetcode刷題系列。

Python程式設計實現leetcode題 兩數之和

題目:兩數相加(中等難度)

給出兩個 非空 的連結串列用來表示兩個非負的整數。其中,它們各自的位數是按照 逆序 的方式儲存的,並且它們的每個節點只能儲存 一位 數字。

如果,我們將這兩個數相加起來,則會返回一個新的連結串列來表示它們的和。

您可以假設除了數字 0 之外,這兩個數都不會以 0 開頭。

示例:

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

 

兩數相加知識點:單向連結串列

這道題看似是做加法,其實是考察對單向連結串列的操作能力。首先,我們要看看什麼是單向連結串列。乍一看它跟Python的列表跟相似,其實它有很多不同於列表的特點:

  • 連結串列的連結方向是單向的;
  • 對連結串列的訪問要透過從頭部開始,依序往下讀取;
  • 不能像列表那樣透過角標訪問元素;
  • 不能向上訪問連結串列,只能單向向下讀取;列表前後訪問很方便。

連結串列的是由一個個節點連結在一起的,每個節點有兩部分:

  • 第一部分是節點本身的資訊;
  • 第二部分是下一個節點的資訊。

這個結構如下圖所示:

兩數相加連結串列結構示意圖

連結串列的結束就是next的值為空(NULL)。到此為止,我們就可以根據單向連結串列的結構來遍歷它了,虛擬碼如下:


p = head.next
while p:
    print(p.data)
    p = p.next

用一個節點指標p指向當前節點,如果p不為空則說明當前節點有值,列印該值,然後讓p指向當前節點的next,迴圈往復,直到p為空也就到了連結串列的尾部,整個連結串列就遍歷完。

 

兩數相加解題思路

瞭解完單向連結串列,我們再來看看題目。這道題其實就是實現我們小學學的加法運算:從個位開始,按位相加,滿十進一。題目的加數和被加數用連結串列表示,為什麼要用連結串列表示呢?我們知道,計算機中的整數位數是有限制的,比如C語言中無符號32位二進位制整數(unsigned int)最大是2的32次方,64位的是2的64次方,直接兩個int相加有可能出現溢位,比如32為整數就不能表示 (2**32 – 1) +  2 這兩個數的和。而用連結串列表示就可以實現任意位數的加法。

但是,Python裡面的整數可以很大,到底有多大呢?查了一圈得出結論,基本上確定跟你的記憶體有關係,你的記憶體足夠大,它的整數就足夠大。Python的大整數(bignum)就是C語言實現的突破了int_32, int_64限制的結構體。

大致估算一下 1GB 記憶體可以表示多大的整數呢? 1GB 也就是2**30位元組,每個位元組有8位,一共可以有 2**30 * 8 位(bit) 也就是 8589934592,除去結構體佔用些記憶體,去個零頭就算 8000000000,那麼這麼多二進位制位可以表示的整數就是 2**8000000000 這麼大!! 雖然你的記憶體遠遠超過了1GB,但是我勸你不要用Python執行下面這條語句:

print(2**8000000000)

在Python裡,你可以輕易得到 2**1000000 即 2 的100萬次方這個整數(有輕微卡頓,它要算一下),但是一屏佔不下,只好接了個 2 的一萬次方的圖給大家感受一下“大數”:

python表示大整數

既然Python可以支援“無限大”的整數,於是就有了這個思路:

 

思路一:(奇葩型思路)連結串列 -> 整數相加 -> 連結串列

(1)分別把兩個連結串列轉換成整數,無論連結串列多長都可以轉換為整數(別超過記憶體),這個轉換演算法也有兩種:

(a)把連結串列轉換為字串,字串再轉換成整數:

(1->2->3->4) -> ‘1234’ -> ‘4321’ -> 4321

(b)連結串列按位相加直接轉換為整數:

(1->2->3->4) -> 1 + 2*10 + 3*100 + 4*1000 -> 4321

(2)兩個整數相加,把和變為連結串列返回。

有興趣的童鞋可以實現一下這個思路,主要是練習連結串列的遍歷、字串反轉、字串轉換為連結串列這幾項。

這個思路利用了Python支援大整數的特點。其實這個題目也可以引申一下,“求兩個數字字串表示的整數的和,返回和的字串”,這個引申可以直接利用這個奇葩思路,當然出題者的初衷應該不在於此。

可能出題者認為下面的思路才算比較正常的:按位相加,滿十進一。

 

思路二:(正常型思路)按位相加,滿十進一

這個思路很正常,不做過多解釋,直接看程式碼:


# Definition for singly-linked list.
#class ListNode:
#    def __init__(self,x):
#        self.val = x
#        self.next = None

class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        root = ListNode(0)
        current = root
        carry = 0
        while l1 or l2:
            v1 = v2 = 0
            if l1:
                v1 = l1.val
                l1 = l1.next
            if l2:
                v2 = l2.val
                l2 = l2.next
            val = v1 + v2 + carry
            carry = val // 10
            val = val % 10
            tmp = ListNode(val)
            current.next = tmp
            current = current.next
        if carry:
            tmp = ListNode(carry)
            current.next = tmp
        return root.next

程式碼最佳化方面,20-22行可以替換為:

carry, val = divmod(v1+v2+carry, 10) 

後來看到別人寫的更簡短的程式碼,思路一樣,行數更少(9行):


# Definition for singly-linked list.
#class ListNode:
#    def __init__(self,x):
#        self.val = x
#        self.next = None

class Solution:
    def addTwoNumbers(self, li: ListNode, l2: ListNode) -> ListNode:
        p1,p2,dum,rem = l1, l2, ListNode(0),0
        p = dum
        while p1 or p2:
            cur = (p1.val if p1 else 0) + (p2.val if p2 else 0) + rem
            rem, cur = cur // 10, cur %10
            p.next = ListNode(cur)
            p,p1,p2 = p.next, p1.next if p1 else p1, p2.next if p2 else p2
        if rem: p.next = ListNode(rem)
        return dum.next

 

思路三: 思路二的進階,支援多個數(不止兩個)相加


# Definition for singly-linked list.
#class ListNode:
#    def __init__(self,x):
#        self.val = x
#        self.next = None

class Solution:
    def addTwoNumbers(self, l1:ListNode, l2: ListNode) -> ListNode:
        root = ListNode(0)
        current = root
        carry = 0
        nums = [l1, l2]
        while nums:
            val = sum(n.val, for n in nums) + carry
            carry = val // 10
            val = val % 10
            tmp = ListNode(val)
            current.next = tmp
            current = current.next
            nums = [n.next for n in nums if n.next]
        if carry:
            tmp = ListNode(carry)
            current.next = tmp
        return root.next

估計你已經看出它和思路二的不同了,它用來一個list把加數包裝起來,迴圈中,下一個節點為空就不再放入list,這樣當list為空時加數就都遍歷到尾部了,迴圈也就結束。這個“用list封裝”的點,可以支援多個連結串列相加,不管是10個還是100個,都放到list裡面去既可以了。

在leetcode上刷題,同一段程式碼,不同時間提交,測試結果有時候會相差很多,不知道有沒有童鞋遇到同樣的現象?有可能是伺服器的“忙”或“閒”導致的吧,這個不用太計較。要計較的是,不同實現方法帶來的效率的不同。

猿人學banner宣傳圖

我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。

***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***

相關文章