上次推出這個用Python刷題leetcode系列後,有人喜歡有人厭,畢竟眾口難調。這篇用Python實現兩數相加。
寫猿人學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可以支援“無限大”的整數,於是就有了這個思路:
思路一:(奇葩型思路)連結串列 -> 整數相加 -> 連結串列
(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上刷題,同一段程式碼,不同時間提交,測試結果有時候會相差很多,不知道有沒有童鞋遇到同樣的現象?有可能是伺服器的“忙”或“閒”導致的吧,這個不用太計較。要計較的是,不同實現方法帶來的效率的不同。
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***