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
41 changes: 41 additions & 0 deletions 235_lowest_common_ancestor_of_a_binary_search_tree/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 235. Lowest Common Ancestor of a Binary Search Tree

https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/

## Comments

### step1

* どこかで解いた気はするがぱっと解き方わからなかった。
* さっと答え見たが、BST であることを失念していた。p, q が同じ側にあれば探索を続け、別の側にあるとわかればそれが LCA
* 再帰での実装は簡単。特にコメントもなし。
* `p_val` や `q_val` を変数に置くべきかは微妙なところ。別にアロー演算子でアクセスしてもいい気がする。ちょっとだけタイプするのが楽。

### step2
* `Solution1`
* 最初 `//Unreachable` のコメントだけ書いていたが、最後の `return root;` ないと怒られる。コードパスの中で全部 return しましょうということだと思うが、たぶんコンパイラが到達するか判断できないからだろう。
* > Line 35: Char 5: error: non-void function does not return a value in all control paths [-Werror,-Wreturn-type]
* `Solution2`
* https://cpprefjp.github.io/reference/utility/unreachable.html
* `std::unreachable` というのがあるらしい。
* C++ だとこれを書いておくのがいいのかな
* https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.magvbqix7jf7 は Python の話。
* https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.atvjch3as46b
* C++ に関する話まとまってた
* `while (1)` でも回避できる。なるほど…読みにくい気がする?
* そもそも無限ループのコード書いたことないかも。この練習会だと時々出てくるが。たぶんプロダクションで加工というよりは、分岐をわかりやすくしたり、選択肢のひとつとして書いてみている、という気がする。
* https://github.com/seal-azarashi/leetcode/pull/21#discussion_r1777314992
* > (別のスレッドからこの変数をどうにかしていじるなどがなければ)到達しないと思うのですが「コンパイラが到達しないことを理解してくれるか」は別問題です。一般に、一重ループの停止性問題は決定不能なので、コンパイラが停止するかしないかを100%の精度で当てることはありません。あと、コードを読んでいる人にも、停止性問題を解かせないで欲しいのでコメントを残すなりしましょうか。私は無限ループへの書き換えを推します。
* https://github.com/olsen-blue/Arai60/pull/59#discussion_r2030548887
* > まず、原理的に、コンピュータがコードが到達可能か不可能かを判定することは不可能です。これはチューリングマシンの停止性問題は判定できないことからいえます。一方で、Java や Rust などは、コンパイル時に型のチェックを真面目にしていて、到達不能なコードかはある程度判定します。しかし、不完全です。このため、正確に考えると到達不可能な箇所であったとしても、コンパイラには分からない場合が多々あります。こういったときに、そこの行に例外を書くことでコンパイル可能になります。何を言っているかというと、上のような事情ならば、例外を書くのはいいですが、そうでなければ、私はデッドコードは基本的に書かないほうがよいと思っています。(せいぜいコメントでいいでしょう。)
* 結局 `std::unreachable` はありなのかな?個人的には無限ループより良さそうに思うのだけど…。
* あ、これめっちゃ新しい (C++23)。なるほどそれ以前の環境を考えると無限ループなどが選択肢に入ってくるのか。
* 例外はなんか重いから C++ ではあまり使わないというのも聞いたことがある。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

「例外は重い」というのは、return と比べれば重いです。
ここをみると char++ * 100万回が 4 ms で、3906回の exception で1秒くらいかかっています。200マイクロ秒かかるということですね。
https://stackoverflow.com/questions/13835817/are-exceptions-in-c-really-slow
私の直感よりもだいぶ遅いのですが、Exception オブジェクトを作ってスタックを巻き戻しながら誰がキャッチするかを探すということになるので、かなり遅いことは確かです。

ただ、Google 社内で使わない理由は、それよりも大域脱出なのでコードが追えなくなるほうが大きいと私は思っています。Chrome の C++ ソースコードは数万ファイルありますから、読んでいて例外が投げてあったら行き先探すのが辛いですね。

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://google.github.io/styleguide/cppguide.html#Exceptions

Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.

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.

Exception オブジェクトを作ってスタックを巻き戻しながら誰がキャッチするかを探すということになるので、かなり遅いことは確かです。

Exception が (原理的に) なぜ遅いのか、というのを考えたことがありませんでした。なるほど。

大域脱出なのでコードが追えなくなるほうが大きいと私は思っています。Chrome の C++ ソースコードは数万ファイルありますから、読んでいて例外が投げてあったら行き先探すのが辛いですね。

なるほど、確かにこれは辛いですね。


しかしこうした点は C++ 以外の言語 (Java や Python など) ではそこまで忌避されていないように思います。Java はわかりませんが Python だと例外を返すのはごく普通のことだと思うのですが、それはどういった差があるのでしょう…?

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.

@nodchip さんが引用くださったスタイルガイドなんかを参照していると、「思いから使うな」というよりは、「まあ多少重いのは重いんだけどそれ自体は許容範囲で、どちらかというと大規模コードベースを前提にした時の不都合が大きくなるから使うな」という気がしました。
そういう論理であれば、小さいプロジェクトであれば普通に使ってよい気もしましたが、初心者なのでそのあたりの感覚はずれているかもしれません。。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Java や Python はガーベージコレクションがあるのでスタックの解析が不要で、言語がそもそも遅いのです。あと、言語仕様上書かざるを得ないです。

「まあ多少重いのは重いんだけどそれ自体は許容範囲で、どちらかというと大規模コードベースを前提にした時の不都合が大きくなるから使うな」
だいたいそう思います。あと Google 社内はそれを前提として環境構築(例えばコードサーチからして)しているので、不利益は大きくなります。

* https://www.reddit.com/r/cpp/comments/ikv9kv/should_i_use_c_exceptions/
* この辺見ると別に良さそうな気もするけど
* `Solution3`
* 無限ループは `while (1)` と `while (true)` どっちがいいんだろう。個人的にはちゃんと bool を渡す方が好きだけど

### step3

* `std::unreachable` がかっこいい (無限ループを避けたい気持ち)
27 changes: 27 additions & 0 deletions 235_lowest_common_ancestor_of_a_binary_search_tree/step1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/

class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
int root_val = root->val;
int p_val = p->val;
int q_val = q->val;
if (p_val > root_val && q_val > root_val) {
// Both p and q are in the right subtree.
return lowestCommonAncestor(root->right, p, q);
}
if (p_val < root_val && q_val < root_val) {
// Both p and q are in the left subtree.
return lowestCommonAncestor(root->left, p, q);
}
return root;
}
};
93 changes: 93 additions & 0 deletions 235_lowest_common_ancestor_of_a_binary_search_tree/step2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/

// return
# include <stack>

class Solution1 {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
int p_val = p->val;
int q_val = q->val;
std::stack<TreeNode*> nodes;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nodes には高々 1 個までしか要素が詰まれないため、単に TreeNode* で良いように思いました。

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.

これ気づいていませんでした。stack で積んでいく、とぃうよりは単一の node を動かして走査している、と理解すべきでした。

nodes.push(root);
while (!nodes.empty()) {
TreeNode* node = nodes.top();
nodes.pop();
int node_val = node->val;
if (p_val > node_val && q_val > node_val) {
nodes.push(node->right);
continue;
}
if (p_val < node_val && q_val < node_val) {
nodes.push(node->left);
continue;
}
return node;
}
// Unreachable
return root;
}
};


// unreachable
# include <stack>

class Solution2 {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
int p_val = p->val;
int q_val = q->val;
std::stack<TreeNode*> nodes;
nodes.push(root);
while (!nodes.empty()) {
TreeNode* node = nodes.top();
nodes.pop();
int node_val = node->val;
if (p_val > node_val && q_val > node_val) {
nodes.push(node->right);
continue;
}
if (p_val < node_val && q_val < node_val) {
nodes.push(node->left);
continue;
}
return node;
}
std::unreachable();
}
};


// 無限ループ
class Solution3 {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
int p_val = p->val;
int q_val = q->val;
std::stack<TreeNode*> nodes;
nodes.push(root);
while (true) {
TreeNode* node = nodes.top();
nodes.pop();
int node_val = node->val;
if (p_val > node_val && q_val > node_val) {
nodes.push(node->right);
continue;
}
if (p_val < node_val && q_val < node_val) {
nodes.push(node->left);
continue;
}
return node;
}
}
};
26 changes: 26 additions & 0 deletions 235_lowest_common_ancestor_of_a_binary_search_tree/step3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# include <stack>

class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
int p_val = p->val;
int q_val = q->val;
std::stack<TreeNode*> nodes;
nodes.push(root);
while (!nodes.empty()) {
TreeNode* node = nodes.top();
nodes.pop();
int node_val = node->val;
if (p_val > node_val && q_val > node_val) {
nodes.push(node->right);
continue;
}
if (p_val < node_val && q_val < node_val) {
nodes.push(node->left);
continue;
}
return node;
}
std::unreachable();
}
};