-
Notifications
You must be signed in to change notification settings - Fork 0
617. Merge Two Binary Trees #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# 617. Merge Two Binary Trees | ||
|
||
https://leetcode.com/problems/merge-two-binary-trees/description/ | ||
|
||
## Comments | ||
|
||
### step1 | ||
|
||
* 最初 iterative DFS (`nodes_pairs = [(root1, root2)]` のような stack) で書こうとしたが、結局、今の node だけに注目していると、どの node の child なのか管理しないといけない気がして、再帰に方針変更 | ||
* limit が 2000 なので、まあ recursionlimit を変更するかは環境次第か | ||
* 10:00 くらいで `Solution1` まで書いてみた。`new_node.left = merge_trees_helper(node1.left, node2.left)` のあたりで None に left / right をやって runtime error になった。この時点で、node1 が None、node2 が None、両方 None でないの 3 パターンがあり、これごとに条件分岐するの非常に面倒だなと思う | ||
* get_left もしくは三項演算子を使って node1_left みたいなのを定義はできるが、node1_left, node2_left. node1_right, node2_right すべてやるの面倒だな… | ||
* ここまでで 13:00 以上経過していたので一旦打ち切り | ||
* -> あとで一応ベタ書きしてみた…これはひどい (`Solution2`)。二分木ではなく四分木だったらもっとひどい。 | ||
|
||
### step2 | ||
|
||
* https://github.com/hayashi-ay/leetcode/pull/12/files | ||
* なるほど既存の木を編集していく想定だった? | ||
* ああそうか片方が None のときは merge する必要がなく、もう片方を返せばいいだけか。 | ||
* そうすると merge するのは、いずれも None でないケースに限定されるので書きやすい | ||
* ここなんとなく書きたくはなるが確かに `if root1 is None` でカバーされているから不要ではある | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 条件式を明確にできるので、ある方がわかりやすいと思いました。 |
||
* `çopy.deepcopy` で非破壊的にできる。コピーに時間かかるがいいかは微妙だけど。 | ||
|
||
```python | ||
if root1 is None and root2 is None: | ||
return None | ||
``` | ||
|
||
* https://github.com/nittoco/leetcode/pull/30/files#r1693985989 | ||
* これでシンプルに書ける | ||
* https://github.com/nittoco/leetcode/pull/30/files#r1693945861 | ||
* 仕事を押し付ける、押し付けないの議論がよく理解できていないのでまた読む。 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C++ だと分かりやすいんですが string f() {
// 色々な操作
return result;
} を void f(string& return_value) {
// 色々な操作
return_value = result;
} と書き換えたとしましょう。 この変形は、状況によっては、再帰を末尾再帰の形に変形できる可能性があります。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 前者は関数の引数としてそのまま返していて、後者は、return_value という参照?ポインタ? (すみません、C++ を書かないので違いが怪しいです…GitHub のどこかで議論を見たかも) に、result を書き込んでいますよね。つまり Python でいうと引数として mutable なコンテナなんかを渡して、そこに書き込ませるということですよね。 とここまでは理解しましたが、ここに対する答えはよくわからなかったので、Gemini に手伝ってもらいつつ調べてみました (内容は記録として続くコメントに載せます)。
結局調べたことも踏まえて改めて質問と元のコード (https://github.com/nittoco/leetcode/pull/30/files#r1693945861) に戻ると
と理解しました。 ただここと、例示いただいたコード、特に「a = f(); を f(a); とすることができます。この変形は…」のところが繋がりません。 ちょっと混乱していると思います。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 上のコメントをするために使った情報を記録しておきます。 調査元のコードの読み直しここの実装を読み直してみました。要は、 https://github.com/nittoco/leetcode/pull/30/files#r1693945861 元のコードの解説 (by Gemini)なぜ「実用性があまりない」のか?
"go" と "back" というフラグを使って、木を「下っていく処理」と「上がってくる処理」を1つのスタックで管理しています。 まずスタックに ("go", ...) を積んでおき、これを取り出したら、今度は逆の処理をするために ("back", ...) を積み直します。その間に子ノードの処理をスタックに追加します。 これにより、子ノードの処理がすべて完了してから、親ノードの結合処理(merged_current[0] = ...)が行われる、という再帰と同じ流れを実現しています。 このロジックはアルゴリズムの学習としては面白いですが、コードが長くなり、一見して何をしているのか理解するのが困難です。
# C言語などではポインタを渡して、関数内でその指す先を書き換える
# change_value(&my_pointer);
# このコードはそれをリストで模倣している
merged_current[0] = TreeNode(node1.val + node2.val) これは、C言語などでポインタ(アドレス)を渡して、関数の中でそのポインタが指す先の値を書き換える、というテクニックを模倣したものです。Pythonでは、このように参照を渡して中身を書き換えるために、リストのようなミュータブル(変更可能)なオブジェクトが使われることがあります。 しかし、これはPythonでは一般的ではなく、トリッキーな書き方と見なされます。もっとシンプルでPythonらしい書き方があります。 末尾最適についての解説 (by Gemini)A. 変換しやすい再帰 = 末尾再帰 (Tail Recursion) 例: def factorial_tail(n, accumulator):
if n == 0:
return accumulator
# 再帰呼び出しが最後の処理
return factorial_tail(n - 1, n * accumulator) なぜ変換しやすいか: 戻ってきた後にやるべきことが何もないので、関数の状態を保存しておく必要がありません。単純なループ(goto文のようなジャンプ)に置き換えることができます。あなたの2つ目の実装**「部下に仕事を押し付けるstack」**は、この考え方に近いです。親ノードの処理を終えたら、子ノードの処理をスタックに積むだけで、親ノードはもう何もする必要がありません。 B. 変換しにくい再帰 = 末尾再帰でない再帰 例: def factorial_normal(n):
if n == 0:
return 1
# 再帰呼び出しの結果を使って、さらに計算(n *)を行っている
return n * factorial_normal(n - 1) なぜ変換しにくいか: 再帰から戻ってきた後に n * ... という計算をする必要があります。ループに変換するには、この「後でやるべき処理」の内容をどこかに保存しておかなければなりません。その保存場所として、通常は明示的なスタックが必要になります。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
逆です。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. あれ、https://github.com/nittoco/leetcode/pull/30/files#r1693945861 を改めて読んでいたんですが、「部下に仕事を押し付けないstack」の方ではこんな感じで nodes_stack = [(root1, root2, merged_root, [None], [None], "go")] もうしばらく考えてみます… There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 再帰の呼び出し元がする返ってきた値の代入を呼び出し先で行うことを、部下に仕事を押し付けるといっているだけなので、あまり言葉尻にとらわれずに考えてください。 |
||
|
||
### step3 | ||
|
||
* 2:00 前後。とりあえずこれが一番シンプルな解法だと思われる。 | ||
* ここは却って読みにくい気もするので、あまり使わないほうがいいかな。 | ||
|
||
```python | ||
# if root1 is None or root2 is None: | ||
# return root1 or root2 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Definition for a binary tree node. | ||
# class TreeNode: | ||
# def __init__(self, val=0, left=None, right=None): | ||
# self.val = val | ||
# self.left = left | ||
# self.right = right | ||
# Runtime error | ||
class Solution1: | ||
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]: | ||
def merge_trees_helper(node1, node2): | ||
if node1 is None and node2 is None: | ||
return None | ||
node1_val = node1.val if node1 is not None else 0 | ||
node2_val = node2.val if node2 is not None else 0 | ||
new_node = TreeNode(node1_val + node2_val) | ||
new_node.left = merge_trees_helper(node1.left, node2.left) | ||
new_node.right = merge_trees_helper(node1.right, node2.right) | ||
return new_node | ||
|
||
return merge_trees_helper(root1, root2) | ||
|
||
|
||
# ベタ書き。これはひどい | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 私も最初似たようなコードを書いてました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これはいいですね。全て先に 0 や None で初期化してしまうんですね。
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not (root1 or root2):
return None
merged = TreeNode()
merged.val = 0
root1_left = None
root1_right = None
root2_left = None
root2_right = None
if root1:
merged.val += root1.val
root1_left = root1.left
root1_right = root1.right
if root2:
merged.val += root2.val
root2_left = root2.left
root2_right = root2.right
merged.left = self.mergeTrees(root1_left, root2_left)
merged.right = self.mergeTrees(root1_right, root2_right)
return merged
|
||
# Accepted | ||
class Solution2: | ||
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]: | ||
def merge_trees_helper(node1, node2): | ||
if node1 is None and node2 is None: | ||
return None | ||
node1_val = node1.val if node1 is not None else 0 | ||
node2_val = node2.val if node2 is not None else 0 | ||
new_node = TreeNode(node1_val + node2_val) | ||
if node1 is not None and node2 is not None: | ||
node1_left = node1.left | ||
node1_right = node1.right | ||
node2_left = node2.left | ||
node2_right = node2.right | ||
elif node1 is None: | ||
node1_left = None | ||
node1_right = None | ||
node2_left = node2.left | ||
node2_right = node2.right | ||
elif node2 is None: | ||
node1_left = node1.left | ||
node1_right = node1.right | ||
node2_left = None | ||
node2_right = None | ||
|
||
new_node.left = merge_trees_helper(node1_left, node2_left) | ||
new_node.right = merge_trees_helper(node1_right, node2_right) | ||
return new_node | ||
|
||
return merge_trees_helper(root1, root2) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Definition for a binary tree node. | ||
# class TreeNode: | ||
# def __init__(self, val=0, left=None, right=None): | ||
# self.val = val | ||
# self.left = left | ||
# self.right = right | ||
class Solution: | ||
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]: | ||
if root1 is None and root2 is None: | ||
return None | ||
if root1 is None: | ||
return root2 | ||
if root2 is None: | ||
return root1 | ||
|
||
new_root = TreeNode(root1.val + root2.val) | ||
new_root.left = self.mergeTrees(root1.left, root2.left) | ||
new_root.right = self.mergeTrees(root1.right, root2.right) | ||
return new_root |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Definition for a binary tree node. | ||
# class TreeNode: | ||
# def __init__(self, val=0, left=None, right=None): | ||
# self.val = val | ||
# self.left = left | ||
# self.right = right | ||
class Solution: | ||
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]: | ||
# if root1 is None or root2 is None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここをコメントにするのいいですね。 |
||
# return root1 or root2 | ||
if root1 is None: | ||
return root2 | ||
if root2 is None: | ||
return root1 | ||
|
||
new_root = TreeNode(root1.val + root2.val) | ||
new_root.left = self.mergeTrees(root1.left, root2.left) | ||
new_root.right = self.mergeTrees(root1.right, root2.right) | ||
return new_root | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 読みやすいです。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 最初よくわかっていませんでしたが、reference いただいたコメントで理解しました。ありがとうございます!
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
再帰で書くときは、再帰の深さについて忘れないようにしたいです。