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
240 changes: 240 additions & 0 deletions arai60/Tree_BT_BST/minimum-depth-of-binary-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# 111. Minimum Depth of Binary Tree

LeetCode URL: https://leetcode.com/problems/minimum-depth-of-binary-tree/description/

この問題は Java で解いています。
各解法において、メソッドが属するクラスとして `Solution` を定義していますが、これは Java の言語仕様に従い、コードを実行可能にするために必要なものです。このクラス自体には特定の意味はなく、単にメソッドを組織化し、実行可能にするためのものです。

## Step 1

```java
// 解いた時間: 5分ぐらい
// 時間計算量: O(n)
// 空間計算量: O(n)
class Solution {
private static final int MIN_DEPTH_NOT_CALCULATED = -1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

calculate には、計算式を用いて何らかの計算をするという意味合いがあるように思います。このプログラムは特に計算をするわけではないため、 calculate という単語を使うのは不適切なように思います。別の単語を使うことをお勧めいたします。

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
ありがとうございます。確かにそうですね。
MIN_DEPTH_NOT_FOUND が適切なのかなと考えましたが、こちらはいかがでしょうか?

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.

ありがとうございます!こちら Step 4 の方に書いておきます。


public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}

Queue<TreeNode> treeNodes = new ArrayDeque<>();
treeNodes.offer(root);
int depth = 1;
while (!treeNodes.isEmpty()) {
int currentLevelNodesCount = treeNodes.size();
for (int i = 0; i < currentLevelNodesCount; i++) {
TreeNode node = treeNodes.poll();
if (node.left == null && node.right == null) {
return depth;
}
if (node.left != null) {
treeNodes.offer(node.left);
}
if (node.right != null) {
treeNodes.offer(node.right);
}
}

depth++;
}

// 処理に異常が無い限りここには到達しません。
return MIN_DEPTH_NOT_CALCULATED;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!treeNodes.isEmpty になることがないはずなので、無限ループにしてこの行消したほうがいいのかなと思いました。

}
}
```

次のようなことを考えながら実装していました:

- Queue を用いた BFS と再帰関数、スタックによる DFS の計3つのアプローチを思いつく
- 1つめのアプローチはスタックオーバーフローのリスクが無く、リーフノードを見つけた時点で処理が終了できて効率がいい、また実装も他2つと比べ理解しやすい内容になると考え、そちらで書いてみた
- Leetcode の constraints 上はスタックオーバーフローのリスクはほぼ無いと考えらるが、 [oda さんのコメント](https://github.com/kazukiii/leetcode/pull/22/files#r1667746480)にある通り「この環境では大丈夫」なコードはいつ自分の足を撃ち抜いて来るかわからないので避けた
- 次の理由でキュー (ArrayDeque) へは null を挿入していない:
- そもそも ArrayDeque の各メソッドは null を挿入することを許容しない
- null チェックをイテレーションごとに都度実施しなくていいなら、そうする越したことはないと思う
- [ArrayDeque はドキュメント冒頭にある通り早い](https://docs.oracle.com/en%2Fjava%2Fjavase%2F22%2Fdocs%2Fapi%2F%2F/java.base/java/util/ArrayDeque.html)ので、その利点を捨ててまで、例えば null 許容する LinkedList を implementing class にすることもないかなと考える
- minDepth() の最下部は次のようなことを考えて実装した:
- 処理に異常がない限りは到達しないことを明記
- Depth の値として期待されない -1 を返す代わりに Exception を throw することも考えたが、後続処理にどのような影響を与えるべきかの議論なしにそこまでやるのは憚られたのでこのようにした
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分としてはException を throwすることは憚られるが-1を返すのは良いという理屈があまり分かりませんでした。

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.

別のスレッドに返信していたので一応こちらにもリンク貼っておきます: #21 (comment)

- 以前同じような議論がされていたので参考まで: https://github.com/seal-azarashi/leetcode/pull/9/files#r1667365546

## Step 2

### スタックによる DFS

```java
// 時間計算量: O(n)
// 空間計算量: O(n)
class Solution {
private record TreeNodeDepth(TreeNode node, int depth) {};

public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}

Deque<TreeNodeDepth> treeNodeDepths = new ArrayDeque<>();
treeNodeDepths.push(new TreeNodeDepth(root, 1));
int minDepth = Integer.MAX_VALUE;
while (!treeNodeDepths.isEmpty()) {
TreeNodeDepth nodeDepth = treeNodeDepths.pop();
if (nodeDepth.node.left == null && nodeDepth.node.right == null) {
minDepth = Math.min(minDepth, nodeDepth.depth);
}
if (nodeDepth.node.left != null) {
treeNodeDepths.push(new TreeNodeDepth(nodeDepth.node.left, nodeDepth.depth + 1));
}
if (nodeDepth.node.right != null) {
treeNodeDepths.push(new TreeNodeDepth(nodeDepth.node.right, nodeDepth.depth + 1));
}
Comment on lines +86 to +91
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

細かいですが、stack-dfsは右>左の順に書いたほうが自然な処理の流れで好みです。

}
return minDepth;
}
}
```

### 再帰関数による DFS

```java
// 時間計算量: O(n)
// 空間計算量: O(n)
class Solution {
public int minDepth(TreeNode root) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

minDepth自体を再帰させることもできそうですか?

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.

はい、次のように出来ます

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        if (root.left == null && root.right == null) {
            return 1;
        }

        int leftNodeMinDepth = Integer.MAX_VALUE;
        if (root.left != null) {
            leftNodeMinDepth = minDepth(root.left);
        }
        int rightNodeMinDepth = Integer.MAX_VALUE;
        if (root.right != null) {
            rightNodeMinDepth = minDepth(root.right);
        }
        return Math.min(leftNodeMinDepth, rightNodeMinDepth) + 1;
    }
}

if (root == null) {
return 0;
}

return traverseMinDepthRecursively(root);
}

private int traverseMinDepthRecursively(TreeNode node) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

traverse は他動詞としては

https://eow.alc.co.jp/search?q=traverse

〔山などを〕越える
〔場所を〕横切る、横断する
〔~に〕矛盾する、反対する、抗弁する、否認する
〔~を〕詳しく検討する

という意味があり、目的語を min depth にすると意味が通らなくなってしまうように思います。別の表現は思いつきますか?

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
なるほどありがとうございます。木に対する走査を tree traversal と言うのでその動詞形を選んでいましたが、引用していただいた他の意味を考えると適切でないように思えました。あと tree を traverse することは手段であって目的ではないのでやはり不適切ですね。

別の表現は思いつきますか?

findMinDepthRecursively を思いつきました。問題文に "Given a binary tree, find its minimum depth" とあるので、自然な命名になっていると感じますがいかがでしょうか。

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.

ありがとうございます!

if (node.left == null && node.right == null) {
return 1;
}

int leftNodeMinDepth = Integer.MAX_VALUE;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

int minDepth = Integer.MAX_VALUE;
if (node.left != null) {
    minDepth = Math.min(minDepth , traverseMinDepthRecursively(node.left));
}

if (node.right != null) {
    minDepth = Math.min(minDepth , traverseMinDepthRecursively(node.right));;
}

return minDepth + 1;

とすると、少しシンプルになると思います。

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.

ありがとうございます!Step 4 に書いておきます。

if (node.left != null) {
leftNodeMinDepth = traverseMinDepthRecursively(node.left);
}
int rightNodeMinDepth = Integer.MAX_VALUE;
if (node.right != null) {
rightNodeMinDepth = traverseMinDepthRecursively(node.right);
}
return Math.min(leftNodeMinDepth, rightNodeMinDepth) + 1;
}
}
```

## Step 3

```java
// 解いた時間: 5分ぐらい
// 時間計算量: O(n)
// 空間計算量: O(n)
class Solution {
private static final int MIN_DEPTH_NOT_CALCULATED = -1;

public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}

Queue<TreeNode> treeNodes = new ArrayDeque<>();
treeNodes.offer(root);
int minDepth = 1;
while (!treeNodes.isEmpty()) {
int currentLevelNodeCount = treeNodes.size();
for (int i = 0; i < currentLevelNodeCount; i++) {
TreeNode node = treeNodes.poll();
if (node.left == null && node.right == null) {
return minDepth;
}
if (node.left != null) {
treeNodes.offer(node.left);
}
if (node.right != null) {
treeNodes.offer(node.right);
}
}
minDepth++;
}

return MIN_DEPTH_NOT_CALCULATED;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Depth の値として期待されない -1 を返す代わりに Exception を throw することも考えたが、後続処理にどのような影響を与えるべきかの議論なしにそこまでやるのは憚られたのでこのようにした

期待されない値というか、ここって到達しないので、throw Exception("unreachable")が正しいんじゃないでしょうか??

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が書いてあったらどこかで実装ミスをしたのかな、と思って混乱するかもなと思いました

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 を throw してしまうと後続処理が止まってしまいます。例えば呼び出し元が次のようになっている場合、この関数で Exception が発生したら logics.somethingVeryImportantMustBeReached(valCanBeTrivial) は実行されません。

public class Driver {
    public static void main(String[] args) {
        TreeNode root = /* 木の構築 */;
        Solution solution = new Solution();
        OtherLogics logics = new OtherLogics();
        try {
            int minDepth = solution.minDepth(root);
            int valCanBeTrivial = logics.somethingTrivial(minDepth);
            logics.somethingVeryImportantMustBeReached(valCanBeTrivial);
        } catch (Exception e) {
            System.err.println("エラーが発生しました: " + e.getMessage());
        }
    }
}

特に確認が出来ていない状況であれば、とりあえず選ぶならば、後続処理を止める可能性のある実装より、そうでない実装の方にした方がまだいいのかなと考えていました。

Copy link
Copy Markdown
Owner Author

@seal-azarashi seal-azarashi Sep 10, 2024

Choose a reason for hiding this comment

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

とは言うものの、そもそもチェック例外のある関数を呼ぶなら呼ぶ側が気をつければいいという話ではあるかもしれないですね... と上記書いてて思いました。
上記の後続処理も、絶対実行させたいなら次のように書くことも出来ますし。

public class Driver {
    public static void main(String[] args) {
        TreeNode root = /* 木の構築 */;
        Solution solution = new Solution();
        OtherLogics logics = new OtherLogics();
        int valCanBeTrivial = 0; // デフォルト値
        try {
            int minDepth = solution.minDepth(root);
            valCanBeTrivial = logics.somethingTrivial(minDepth);
        } catch (Exception e) {
            System.err.println("エラーが発生しました: " + e.getMessage());
        } finally { // 👈 例外の発生有無に関わらずこのブロック内の処理は必ず実行される
            logics.somethingVeryImportantMustBeReached(valCanBeTrivial);
        }
    }
}

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

Choose a reason for hiding this comment

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

私がよく分かっていない可能性があるのですが、

到達しないつもりで書いてはいるのですが、コンパイラによってチェックされているといった保証が無いので、到達する可能性は排除出来ない認識でいます。

とあり、どのような入力を与えると(若しくは事故?が起こると)return MIN_DEPTH_NOT_CALCULATED;に到達することがあるのか気になっております。

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にするのもあるかもなという気持ちになりました

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.

@TORUS0818

私がよく分かっていない可能性があるのですが、

到達しないつもりで書いてはいるのですが、コンパイラによってチェックされているといった保証が無いので、到達する可能性は排除出来ない認識でいます。

とあり、どのような入力を与えると(若しくは事故?が起こると)return MIN_DEPTH_NOT_CALCULATED;に到達することがあるのか気になっております。

すいません、返信がかなり遅くなってしまいました。
私が認識してる限りですと、この関数に関してはこの行に到達するケースは無いです。ただそれはあくまで人間 (今回の場合は実装者の私) がチェックした結果そう認識しているだけで、型システムなどで保証されているわけではありませんので、到達する可能性は排除できないと書いていました。

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.

これだけ多くの人が見ても見つからないのでこの関数に関してはもうこの行に到達することはないと考えていいと思います。ただそれはそれとして、システムによる保証がない状況で強いて選ぶなら... と考えてこのようにした次第です。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、これなんですが、(別のスレッドからこの変数をどうにかしていじるなどがなければ)到達しないと思うのですが「コンパイラが到達しないことを理解してくれるか」は別問題です。一般に、一重ループの停止性問題は決定不能なので、コンパイラが停止するかしないかを100%の精度で当てることはありません。
あと、コードを読んでいる人にも、停止性問題を解かせないで欲しいのでコメントを残すなりしましょうか。
私は無限ループへの書き換えを推します。

}
}
```

## Step 4

### キューによる BFS

```java
// 時間計算量: O(n)
// 空間計算量: O(n)
class Solution {
private static final int MIN_DEPTH_NOT_FOUND = -1;

public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}

Queue<TreeNode> treeNodes = new ArrayDeque<>();
treeNodes.offer(root);
int depth = 1;
while (!treeNodes.isEmpty()) {
int currentLevelNodesCount = treeNodes.size();
for (int i = 0; i < currentLevelNodesCount; i++) {
TreeNode node = treeNodes.poll();
if (node.left == null && node.right == null) {
return depth;
}
if (node.left != null) {
treeNodes.offer(node.left);
}
if (node.right != null) {
treeNodes.offer(node.right);
}
}

depth++;
}

// 処理に異常が無い限りここには到達しません。
return MIN_DEPTH_NOT_FOUND;
}
}
```

### 再帰関数による DFS

```java
// 時間計算量: O(n)
// 空間計算量: O(n)
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}

return findMinDepthRecursively(root);
}

private int findMinDepthRecursively(TreeNode node) {
if (node.left == null && node.right == null) {
return 1;
}

int minDepth = Integer.MAX_VALUE;
if (node.left != null) {
minDepth = Math.min(minDepth , findMinDepthRecursively(node.left));
}
if (node.right != null) {
minDepth = Math.min(minDepth , findMinDepthRecursively(node.right));
}
return minDepth + 1;
}
}
```