Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions 2_add_two_numbers_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 問題へのリンク
[Add Two Numbers - LeetCode](https://leetcode.com/problems/add-two-numbers/description/)

# 言語
Python

# 問題の概要
与えられた2つの非負整数を逆順で表現した連結リストとして受け取り、それらの合計を同様に連結リストで返す問題。

# 自分の解法
再帰関数を用いた解法。


連結リストl1, l2の長さをそれぞれ`m`, `n`とすると
- 時間計算量:`O(max(m,n))`
- 空間計算量:`O(max(m,n))`(再帰のスタック領域を含む)

## step2
再帰的な実装から反復的な実装に変えた。また、変数名もより明確なものに変更した。

- アルゴリズムの変更:再帰から反復へ
- step1は再帰関数を用いた実装であったが、step2はwhileループを用いた反復処理の実装である。これにより、非常に長い連結リストを扱う際のスタックオーバーフローのリスクが解消された。
- ダミーヘッドノードの導入
- step2では、連結リストを構築する際の一般的なテクニックである`dummy_head`ノードが導入された。これにより、リストの先頭ノードに関する特別な処理が不要になり、コードが単純化されている。
- `None`ノードの処理方法の改善
- step1では、入力リストのノードが`None`の場合に新しい`ListNode`を生成していた。step2では、`l1.val if l1 else 0`という三項演算子を用いることで、この処理をより効率的かつ簡潔に記述している。
- ループ条件の単純化
- step1の複雑な再帰終了条件は、step2では`while l1 or l2 or carry_of_digits:`という単一の明快なループ条件に集約された。これは、処理を継続すべき全てのケースを網羅している。


# 別解・模範解答
再帰関数ではなく、ループを用いてノードを1つずつ処理する解法。
最後に返すノードは連結リストの先頭ノードなので、ダミーノードを用意しておき、そこから結果の連結リストを構築する。その後、ダミーノードの次のノード(`dummy_head.next`)を返す。
ループの最中には、今自分がどのノードを処理しているかをきちんと把握しておく。コードの中でもそれが明示できるように心がける。今回ならば、while文の中で足し合わせたノードは`node.next`に保存する。

連結リストl1, l2の長さをそれぞれ`m`, `n`とすると

- 時間計算量:`O(max(m,n))`
- 空間計算量:`O(1)`

# 次に解く問題の予告

- [ ] [Evaluate Division - LeetCode](https://leetcode.com/problems/evaluate-division/description/)
51 changes: 51 additions & 0 deletions 2_add_two_numbers_medium/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#
# @lc app=leetcode id=2 lang=python3
#
# [2] Add Two Numbers
#


# @lc code=start
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next


from typing import Optional

# we consider addition in base 10
BASE_NUMBER = 10


class Solution:
def addTwoNumbers(
self, l1: Optional[ListNode], l2: Optional[ListNode]
) -> Optional[ListNode]:

def add_two_list_nodes(
l1: Optional[ListNode], l2: Optional[ListNode], carry_of_digits: int
Copy link

Choose a reason for hiding this comment

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

単に carry でも十分に通じると思いました。

) -> Optional[ListNode]:
if not l1 and not l2 and carry_of_digits == 0:
Copy link

Choose a reason for hiding this comment

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

if l1 is None and l2 is None and carry_of_digits == 0: と書いてもよいと思いました。

参考までにスタイルガイドへのリンクを貼ります。

https://google.github.io/styleguide/pyguide.html#2144-decision

Always use if foo is None: (or is not None) to check for a None value. E.g., when testing whether a variable or argument that defaults to None was set to some other value. The other value might be a value that’s false in a boolean context!

ただし、上記のスタイルガイドは唯一絶対のルールではなく、複数あるスタイルガイドの一つに過ぎないということを念頭に置くことをお勧めします。また、所属するチームにより何が良いとされているかは変わります。自分の中で良い書き方の基準を持ちつつ、チームの平均的な書き方で書くことをお勧めいたします。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。
なるほど、こういうオブジェクトのNoneの判定には基本的にはisを使うようにします。
リストのようなイテレータオブジェクトで、空の場合とNoneの場合も区別せずに処理したい場合には if not x:を使うようにします。

return None
elif not l1 and not l2:
Copy link

Choose a reason for hiding this comment

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

step 2 のように

            if not l1:
                l1 = ListNode()
            if not l2:
                l2 = ListNode()

と書いたほうがシンプルだと思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。step2で改善できたようで良かったです。

Copy link

Choose a reason for hiding this comment

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

直前が return None と関数の終わりを表しているため、 step 2 のように if のほうがよいと思いました。

l1 = ListNode()
l2 = ListNode()
elif not l1:
l1 = ListNode()
elif not l2:
l2 = ListNode()

added_list_node: ListNode = ListNode()
carry_of_digits, digit_sum = divmod(
l1.val + l2.val + carry_of_digits, BASE_NUMBER
)
added_list_node.val = digit_sum
added_list_node.next = add_two_list_nodes(l1.next, l2.next, carry_of_digits)
return added_list_node

return add_two_list_nodes(l1, l2, 0)


# @lc code=end
53 changes: 53 additions & 0 deletions 2_add_two_numbers_medium/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#
# @lc app=leetcode id=2 lang=python3
#
# [2] Add Two Numbers
#


# @lc code=start
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next


from typing import Optional

# we consider addition in base 10
BASE_NUMBER = 10


class Solution:
def addTwoNumbers(
self, l1: Optional[ListNode], l2: Optional[ListNode]
) -> Optional[ListNode]:
"""
addTwoNumbers iteratively creates nodes and set added value to each of them.
Created linked list look like this; dummy_head -> node0 -> node1 ->... -> last_node
return value is dummy_head.next (=node0)
"""
# save dummy head for returning head node
dummy_head: ListNode = ListNode()

# initialize first node and carry of digits
node = dummy_head
carry_of_digits = 0
while l1 or l2 or carry_of_digits:
# add two values and carry_of_digits
digit1 = l1.val if l1 else 0
digit2 = l2.val if l2 else 0
total = digit1 + digit2 + carry_of_digits
carry_of_digits, digit = divmod(total, BASE_NUMBER)

Choose a reason for hiding this comment

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

個人的には

carry = total // BASE_NUMBER
digit = total % BASE_NUMBER

の方が unpack より読み下しやすいと感じてしまうのですが、単に私が divmod をあまり使わないので見慣れていないだけかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。
結論、可読性とパフォーマンスとのトレードオフかと思います。
divmodの方が//%よりも演算単体のパフォーマンスが高い(後述)のですが、それが全体のパフォーマンスにどれほど影響するかといわれると状況によります。(というかむしろ、ほかの部分がボトルネックになることがほとんどかと思います。実際に計算したところ、相当巨大な数を何度も計算する場合などに限られると感じました。)

一方で、それ以上に可読性が下がってしまうのがまずいとすると、後者を優先するべきだと思います。
divmodが一般的かどうか、読みづらくなるかどうかもチームにはよるかと思うのですが、このあたりは他の方の所感も気になります。

【補足】

バイトコードレベルでみると、割り算の結果とその余りは同時に得られるので、divmodの方が//%を二度使うよりも単純に二倍パフォーマンスが高いです。

実際、以下のような結果が得られます。

>>> import timeit
>>> timeit.timeit('divmod(n, d)', 'n, d = 2**74207281 - 1, ', number=100)
0.8267046669998308
>>> timeit.timeit('n // d, n % d', 'n, d = 2**74207281 - 1, 26', number=100)
1.654150374999972

参考

Choose a reason for hiding this comment

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

私も divmod() に見慣れていないだけかもしれませんが、個人的には今回の問題の条件では性能よりも可読性を選択すると思いました。

node.next = ListNode(digit)

# move to next node
node = node.next
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None

return dummy_head.next


# @lc code=end
50 changes: 50 additions & 0 deletions 2_add_two_numbers_medium/step2_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# @lc app=leetcode id=2 lang=python3
#
# [2] Add Two Numbers
#


# @lc code=start
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next


from typing import Optional

# we consider addition in base 10
BASE_NUMBER = 10


class Solution:
def addTwoNumbers(
self, l1: Optional[ListNode], l2: Optional[ListNode]
) -> Optional[ListNode]:

def add_nodes_recursive(
l1: Optional[ListNode], l2: Optional[ListNode], carry_of_digits: int
) -> Optional[ListNode]:
if not l1 and not l2 and carry_of_digits == 0:
return None
if not l1:
l1 = ListNode()
if not l2:
l2 = ListNode()

added_list_node: ListNode = ListNode()
carry_of_digits, digit_sum = divmod(
l1.val + l2.val + carry_of_digits, BASE_NUMBER
)
added_list_node.val = digit_sum
added_list_node.next = add_nodes_recursive(
l1.next, l2.next, carry_of_digits
)
return added_list_node

return add_nodes_recursive(l1, l2, 0)


# @lc code=end
49 changes: 49 additions & 0 deletions 2_add_two_numbers_medium/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#
# @lc app=leetcode id=2 lang=python3
#
# [2] Add Two Numbers
#


# @lc code=start
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next


from typing import Optional

BASE_NUMBER = 10


class Solution:
def addTwoNumbers(
self, l1: Optional[ListNode], l2: Optional[ListNode]
) -> Optional[ListNode]:
# to save head node, we preapare head_dummy
head_dummy = ListNode()

node: ListNode = head_dummy
carry_of_digits = 0

# visit each node iteratively and create added digit one by one
# break the loop if two nodes l1 and l2 are None and no carry of digits
Comment on lines +31 to +32

Choose a reason for hiding this comment

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

(自分用のメモ、というだけであれば恐縮ですが) このコメントはコードを読めば明らかにわかることで、特段複雑な処理をしているわけでもありませんし、冗長なように感じます。コメントは何をしているかよりも、なぜそうしているのか、を言及することが多いかと思います。
たとえば "10" というマジックナンバーが出てきたとき、BASE_NUMBER = 10 のように変数として宣言して命名するか、もしくは一度しか使わないようなものなら当該箇所に # Using base number 10 などつけると思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにそのコメントは不要に感じました。ありがとうございます。

while l1 or l2 or carry_of_digits:
# add two nodes
digit1 = l1.val if l1 else 0
digit2 = l2.val if l2 else 0
total = digit1 + digit2 + carry_of_digits
carry_of_digits, digit = divmod(total, BASE_NUMBER)
node.next = ListNode(digit)

# move to next node
node = node.next
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None

return head_dummy.next


# @lc code=end