Skip to content

Solved 2_Add_Two_Numbers#5

Open
rieuky wants to merge 7 commits into
mainfrom
2_add_two_numbers
Open

Solved 2_Add_Two_Numbers#5
rieuky wants to merge 7 commits into
mainfrom
2_add_two_numbers

Conversation

@rieuky

@rieuky rieuky commented May 4, 2025

Copy link
Copy Markdown
Owner

This problem
-> 2. Add Two Numbers

Next
-> 20. Valid Parentheses

Language
-> Python 3

Comment thread solution_2.md
n1_next, n2_next, digit_sum // 10
)
return node
return add_two_nodes(l1, l2, 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一行空いていると読みやすいと思いました。

Comment thread solution_2.md
@@ -0,0 +1,121 @@
## Step 1
### 考えたこと
新しい連結リストを作り、そこに値を格納していく。繰り上がりを格納する変数 `carry_over` を作り、各桁の合計値に都度足して、次の桁の繰り上がりを計算する。2つの連結リスト走査のための `while` の条件はすぐには思いつかないので、実装しながら考える。

@Miyamoto-tryk Miyamoto-tryk May 4, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

carryだけで桁上げを意味する専門用語ですので、carryだけでも十分だと思います

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。
文脈で意味が変わる動詞だと判断し、少し説明的な命名にしました。
carry で通じるのとのことで、次からは簡潔に書きます。

Comment thread solution_2.md

# while l1 or l2: <- Test case 3 でエラー
while l1 or l2 or carry_over:
sum = carry_over

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

その観点が抜けていました。
Python の予約語・組み込み関数を確認し、無意識で避けられるようにします。

Comment thread solution_2.md
## 再帰で書く

- もう少し整える余地はありそうだが、再帰は読みにくい気がする。これは自分が慣れていないからなのか、SWE 共通なのか?
- Runtime が 0 ms になり、先ほどの 7ms から結構短くなったが、これはなぜだろうか?メモリは再帰 18 MB、非再帰 17.8 MB なので、メモリは再帰の方が使用しているらしい。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LeetCode の時間は結構いい加減です。真面目にやるならば、時間を測る関数を使いましょう。

Comment thread solution_2.md
Comment on lines +112 to +113
n1_next = n1.next if n1 else None
n2_next = n2.next if n2 else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

私は上とくっつけます。

n1_next = None
if n1:
    digit_sum += n1.val
    n1_next = n1.next

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(私の)非再帰のコードと同じロジックで書けるので、その方が読みやすいです。
まだ、頭のなかで柔軟に構造を組みかえることができないので、他の方のコードを参考にしながら訓練したいと思います。

Comment thread solution_2.md
## 再帰で書く

- もう少し整える余地はありそうだが、再帰は読みにくい気がする。これは自分が慣れていないからなのか、SWE 共通なのか?
- Runtime が 0 ms になり、先ほどの 7ms から結構短くなったが、これはなぜだろうか?メモリは再帰 18 MB、非再帰 17.8 MB なので、メモリは再帰の方が使用しているらしい。

@Miyamoto-tryk Miyamoto-tryk May 4, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

メモリ使用量が多い原因の一つとして、今回の再帰の書き方だと、子の再帰関数の計算が終わるまで、スタックフレームに呼び出し元のローカル変数などが保持されたままとなるので、メモリ使用量が多いのだと思います。
末尾再帰の最適化と呼ばれるテクニックを使えば、メモリ使用量は減るのではと思います。末尾再帰最適化はコンパイラ等の言語処理系が行うので、末尾再帰で書いても意味ないこともありますが、Pythonは3.11以降なら末尾再帰最適化が行われるみたいです(参考)。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

丁寧な説明、そして参考リンクまでありがとうございます!
末尾再帰という概念そのものや、末尾再帰の最適化、関数型言語との比較など、すべて理解できたわけではないですが、知らなかった世界を知ることができました。

末尾再帰でコードを書き直してみました。
引数に結果を格納する連結リストの 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

番兵を使うこともできるでしょう。
addTwoNumbers の直下に dummy を作っておいて、return dummy.next するということです。手作業だったらどうするかを考えてみましょう。

setrecursionlimit を触っていいとは限らないので、メモリーが溢れないとしても、再帰は選択肢の一つになっても別の選択肢をもう一つはもっておきたいものかと思いますね。

Comment thread solution_2.md

## 再帰で書く

- もう少し整える余地はありそうだが、再帰は読みにくい気がする。これは自分が慣れていないからなのか、SWE 共通なのか?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的な感想ですが、再帰も読みやすく感じます。加算器では入力がn1,n2,carryの三つであることが普通(だと思う)なので、この問題の再帰関数の引数が(n1,n2,carry)となるのも、しっくりきて好きです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再帰関数でも読みやすいんですね!
私の慣れの問題だと思いましたので、今後は「これは再帰では書けないだろうか?」と立ち止まって考えてみて、再帰関数の読み書きの頻度を増やそうと思います。

加算器の入力が n1, n2, carry の組み合わせがしっくりくるという感覚も持ち合わせていなかったので、参考になりました。これを自然と思えるようになると、最初から綺麗な再帰がかけるようになりそうです。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今回の再帰関数もしっくりきて好きとは言いましたが、再帰については、考えやすい・読みやすい 以外のメリットは少なく感じます
irohafternoon/LeetCode#6 (comment)

再帰で考える→ループで書く みたいな流れは多いみたいなので、再起に慣れてきたらその練習も良さそうだと思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!
現状私は、再帰「を」考えている状態なので、再帰「で」考えるというのが想像できないですが、再帰だとデバッグが難しいというのは確かにと思いました。何回目の再帰呼び出しか出力しようとすると結局ループで書くのと同じことになりそうですし。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants