Solved 2_Add_Two_Numbers#5
Conversation
| n1_next, n2_next, digit_sum // 10 | ||
| ) | ||
| return node | ||
| return add_two_nodes(l1, l2, 0) |
| @@ -0,0 +1,121 @@ | |||
| ## Step 1 | |||
| ### 考えたこと | |||
| 新しい連結リストを作り、そこに値を格納していく。繰り上がりを格納する変数 `carry_over` を作り、各桁の合計値に都度足して、次の桁の繰り上がりを計算する。2つの連結リスト走査のための `while` の条件はすぐには思いつかないので、実装しながら考える。 | |||
There was a problem hiding this comment.
carryだけで桁上げを意味する専門用語ですので、carryだけでも十分だと思います
There was a problem hiding this comment.
レビューありがとうございます。
文脈で意味が変わる動詞だと判断し、少し説明的な命名にしました。
carry で通じるのとのことで、次からは簡潔に書きます。
|
|
||
| # while l1 or l2: <- Test case 3 でエラー | ||
| while l1 or l2 or carry_over: | ||
| sum = carry_over |
There was a problem hiding this comment.
There was a problem hiding this comment.
その観点が抜けていました。
Python の予約語・組み込み関数を確認し、無意識で避けられるようにします。
| ## 再帰で書く | ||
|
|
||
| - もう少し整える余地はありそうだが、再帰は読みにくい気がする。これは自分が慣れていないからなのか、SWE 共通なのか? | ||
| - Runtime が 0 ms になり、先ほどの 7ms から結構短くなったが、これはなぜだろうか?メモリは再帰 18 MB、非再帰 17.8 MB なので、メモリは再帰の方が使用しているらしい。 |
There was a problem hiding this comment.
LeetCode の時間は結構いい加減です。真面目にやるならば、時間を測る関数を使いましょう。
| n1_next = n1.next if n1 else None | ||
| n2_next = n2.next if n2 else None |
There was a problem hiding this comment.
私は上とくっつけます。
n1_next = None
if n1:
digit_sum += n1.val
n1_next = n1.nextThere was a problem hiding this comment.
(私の)非再帰のコードと同じロジックで書けるので、その方が読みやすいです。
まだ、頭のなかで柔軟に構造を組みかえることができないので、他の方のコードを参考にしながら訓練したいと思います。
| ## 再帰で書く | ||
|
|
||
| - もう少し整える余地はありそうだが、再帰は読みにくい気がする。これは自分が慣れていないからなのか、SWE 共通なのか? | ||
| - Runtime が 0 ms になり、先ほどの 7ms から結構短くなったが、これはなぜだろうか?メモリは再帰 18 MB、非再帰 17.8 MB なので、メモリは再帰の方が使用しているらしい。 |
There was a problem hiding this comment.
メモリ使用量が多い原因の一つとして、今回の再帰の書き方だと、子の再帰関数の計算が終わるまで、スタックフレームに呼び出し元のローカル変数などが保持されたままとなるので、メモリ使用量が多いのだと思います。
末尾再帰の最適化と呼ばれるテクニックを使えば、メモリ使用量は減るのではと思います。末尾再帰最適化はコンパイラ等の言語処理系が行うので、末尾再帰で書いても意味ないこともありますが、Pythonは3.11以降なら末尾再帰最適化が行われるみたいです(参考)。
There was a problem hiding this comment.
丁寧な説明、そして参考リンクまでありがとうございます!
末尾再帰という概念そのものや、末尾再帰の最適化、関数型言語との比較など、すべて理解できたわけではないですが、知らなかった世界を知ることができました。
末尾再帰でコードを書き直してみました。
引数に結果を格納する連結リストの head, tail を入れると上手く実装できたように思います。
(結構頭がこんがらがったので時間がかかりました)
class Solution:
def addTwoNumbers(
self, l1: Optional[ListNode],
l2: Optional[ListNode]
) -> Optional[ListNode]:
def _add_two_nodes(
n1: Optional[ListNode],
n2: Optional[ListNode],
carry: int,
acc_tail: Optional[ListNode],
acc_head: Optional[ListNode]
) -> Optional[ListNode]:
if not (n1 or n2 or carry):
return acc_head
digit_sum = carry
n1_next = None
n2_next = None
if n1:
digit_sum += n1.val
n1_next = n1.next
if n2:
digit_sum += n2.val
n2_next = n2.next
node = ListNode(digit_sum % 10)
if acc_tail:
acc_tail.next = node
else:
acc_head = node # executed only once at the beginning
return _add_two_nodes(n1_next, n2_next, digit_sum // 10, node, acc_head)
return _add_two_nodes(l1, l2, 0, None, None)There was a problem hiding this comment.
番兵を使うこともできるでしょう。
addTwoNumbers の直下に dummy を作っておいて、return dummy.next するということです。手作業だったらどうするかを考えてみましょう。
setrecursionlimit を触っていいとは限らないので、メモリーが溢れないとしても、再帰は選択肢の一つになっても別の選択肢をもう一つはもっておきたいものかと思いますね。
|
|
||
| ## 再帰で書く | ||
|
|
||
| - もう少し整える余地はありそうだが、再帰は読みにくい気がする。これは自分が慣れていないからなのか、SWE 共通なのか? |
There was a problem hiding this comment.
個人的な感想ですが、再帰も読みやすく感じます。加算器では入力がn1,n2,carryの三つであることが普通(だと思う)なので、この問題の再帰関数の引数が(n1,n2,carry)となるのも、しっくりきて好きです。
There was a problem hiding this comment.
再帰関数でも読みやすいんですね!
私の慣れの問題だと思いましたので、今後は「これは再帰では書けないだろうか?」と立ち止まって考えてみて、再帰関数の読み書きの頻度を増やそうと思います。
加算器の入力が n1, n2, carry の組み合わせがしっくりくるという感覚も持ち合わせていなかったので、参考になりました。これを自然と思えるようになると、最初から綺麗な再帰がかけるようになりそうです。
There was a problem hiding this comment.
今回の再帰関数もしっくりきて好きとは言いましたが、再帰については、考えやすい・読みやすい 以外のメリットは少なく感じます
irohafternoon/LeetCode#6 (comment)
再帰で考える→ループで書く みたいな流れは多いみたいなので、再起に慣れてきたらその練習も良さそうだと思いました。
There was a problem hiding this comment.
ありがとうございます!
現状私は、再帰「を」考えている状態なので、再帰「で」考えるというのが想像できないですが、再帰だとデバッグが難しいというのは確かにと思いました。何回目の再帰呼び出しか出力しようとすると結局ループで書くのと同じことになりそうですし。
This problem
-> 2. Add Two Numbers
Next
-> 20. Valid Parentheses
Language
-> Python 3