Skip to content
Open
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
87 changes: 87 additions & 0 deletions 617/617. Merge Two Binary Trees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# [617. Merge Two Binary Trees](https://leetcode.com/problems/merge-two-binary-trees/description/)
## Step1
/*答えを見て理解できたと思ったら、答えを隠して書いて下さい。筆が進まず、5分迷ったら答えを見て下さい。そして、見ちゃったら一回全部消してやり直しです。答えを送信して、正解になったら、まず1段落目です。*/
### 問題意図の考察
- 問題文の確認
- 二分木:roo1, roo2が与えられる。
- root1とroot2をマージして下さい、
- ルールがある
- 2つのノードが重なる場合、その値を合計し新しいノードの値する
- どちらか一方がnullの場合、新しい木のノードとして採用する

- 制約の確認
- The number of nodes in both trees is in the range [0, 2000].
- ノードの数は最大:2000
- O(N), O(n log n)がベター、O(n^2)だと効率という点では微妙かな
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ここを読んで少なくとも自分の感覚とはズレていると感じました。

自分はこの問題を見たとき、rootからトップダウンに作業してO(n)で行けるのはほぼ自明で、速度の議論については定数倍でしか起きないと感じました。

それよりも重要なのは、Step 1で書かれたコードはほぼ最速ですが、入力を破壊している点が気になります。返り値のある関数で入力が破壊される場合、呼び出した人はびっくりすると思います。
実務ならAPIドキュメントに仕様として明記しておいたり、入力を破壊するかどうか引数で選択できるなどの配慮が必要だと思います。

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.

@docto-rin
ご指摘ありがとうございます。
root1 を上書きする破壊的なマージになっている点認識しました。LeetCode上では問題ないように見えますが、実務的なAPIであれば「入力を破壊すること」をドキュメントで明示するか、非破壊的を用意するのが望ましいという点大変勉強になりました。
また実務経験はないのですが、この点他の方のコードを読んだ時に気づけたはずだったのでどんな違いがあるのか注意深く意識してみます。

- -10^4 <= Node.val <= 10^4
- ノードの値:-10,000~10,000
- 足しても +- 2 * 10^4

- 問題意図
- 根から走査して、順番にマージできるか

### 解法を考える。
- 再帰の書き方が自然な流れのように感じる
- null の扱いができるか
- root1->nullの時、root2を返す
- root2->nullの時、root1を返す
- root1->val += root2->val
- root1の二分木にまとめる感覚

初回の回答
```cpp
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if (root1 == nullptr) {
return root2;
}
if (root2 == nullptr) {
return root1;
}

root1->val += root2->val;

root1->left = mergeTrees(root1->left, root2->left);
root1->right = mergeTrees(root1->right, root2->right);

return root1;
}
};

```

## Step2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

step2とStep3のコードが無い気がします。アップいただけますでしょうか。

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.

@Ryotaro25

今回の問題に関してはstep1が自然に感じるのでそのままstep3へ
という形を取りましたが、他の方からのご指摘あります通りroot1に合わせるような破壊的なマージ以外のものに関しても今後書いていきたいと思います。

/*次にコードを読みやすくなるようにできるだけ整えましょう。これで動くコードになったら完成です。*/
- 改善点
- root1に足していくというのがシンプルだと思ったが、結構違うやり方をしている人が多い。
- 最近シンプルに書けることによってしまってるかも知れない。選択肢を増やして正しい判断ができるようにしたい。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

進め方は人それぞれなのでお任せしたいですが、全体的に拝見し、個人的にはもう少し選択肢とそれらのトレードオフについて広く検討されても良いかもしれないなと感じました。例えばですが:

再帰

https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.deivkzaqvetb

再帰をすると、少し面倒になるのがデバッグのためのログ関数を挟むときなどです。
なので、単純なループに近いときには再帰でわざわざ書くことはあまりないとは思います。ただ結構考えるときには使っています。

https://discord.com/channels/1084280443945353267/1350090869390311494/1354492544049877134

例えばですけれども、1万回くらい呼ぶと1回くらいおかしな動きをする機能があって、乱数などが絡んでいるから再現も難しいので、ある特定の if 文を通ったときにログを出力したいとします。再帰で書いていると、そもそもどういう状況でそこに到達したのかの関数の呼び出し元の情報などを出力するのが大変です。

https://discord.com/channels/1084280443945353267/1350090869390311494/1355224179720458261

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.

@docto-rin
アドバイスありがとうございます。
ご指摘の通りだと思いました。Arai60に取り組んだ初めの方、選択肢をどんどん増やそうと出てきたものを全部調べてると時間をかなり使ってしまい結果的に前に進まないという事になりました。これは避けたいですが、同時に引き出しを増やす時間がすごく大事ですので折り合いをつけながら学習を進めたいと思います。

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.

@oda
アドバイスありがとうございます。
まずは前に進めていこうと思います。

- 今回の問題に関してはstep1が自然に感じるのでそのままstep3へ

- 他の方のコードを読む
- https://github.com/5103246/LeetCode_Arai60/pull/22/commits/f2ddfbc88c74bb14d0f640f9b2a0d9f9f110003d
- https://github.com/nktr-cp/leetcode/pull/24/commits/1b6c1d4db51b3a4e22797b82da4be99b91b8e130#diff-d16010dc25fbb50a03680c436bbb0d90f462bf27b3aa26421f1df94d5f15aa15
- 新しい二分木:new TreeNode()を作成する。root1/root2を壊さずに作成
- node1, node2, merged_nodeと3つの組み合わせをキューに入れる
- dummyを用意
- 関数内でメモリを確保した場合、呼び出し元で解放しないとメモリリークとなる。ので面倒で選択してない。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

リンク先のコメントが参考になりました。C++の経験がないといつもの癖でnewとしそうなので、今後C++を書く際に気をつけられそうです。

- メモリ
- 動的メモリ:https://en.cppreference.com/w/cpp/language/new.html
- https://en.cppreference.com/w/cpp/memory.html
- Effective C++ 確認

- 他の解法
- python
- https://github.com/tshimosake/arai60/pull/14/commits/253f33c504c73c4709dafa575c47a9a0f3f0b5ab
-
- C++
- https://github.com/irohafternoon/LeetCode/pull/26/commits/d67d4b4a3959fb02d773249be55806988155817b
- https://github.com/Ryotaro25/leetcode_first60/pull/25/commits/b67018715bbcb6efaa049d09590460cbb23774b1#diff-1296319066657d7ffa73cc35ea8dab0b8bb061399fa3382e925bf62f940038e3
Comment on lines +73 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

URLだけでなく読んだ感想をペアにして列挙していただくとレビュワーの助けになると思います。

例:Step Xの実装が良い、読みやすい、シンプルだと思った。ここの部分は自分ならこう書きたい。ここの部分で複数の選択肢を検討していて良いと思った。など



## Step3
/*そしたら、また全部消しましょう。今度は時間を測りながら、もう一回、書きましょう。書いてアクセプトされたら文字を消して再度書きましょう。これを10分以内に一回もエラーを出さずに書ける状態になるまで続けて下さい。3回続けてそれができたらその問題はひとまず丸です。*/
1. 8min
2. 7min
3. 7min