diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Python.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Python.md new file mode 100644 index 0000000..e432bcb --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Python.md @@ -0,0 +1,319 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Python(CPython 3.11.10 / LeetCode class形式) +> 適用ルールセット: 共通5ルール + Python固有4ルール +> 参照ファイル: references/common.md + references/python.md + +--- + +# 104. Maximum Depth of Binary Tree(Python版) + +--- + +## 1. 問題の分析 + +> 💡 **一言で言うと**:「二分木(=各ノードが最大2つの子を持つ木構造データ)の根(root)から一番遠い葉(末端ノード)まで、何段あるかを数える問題」です。 + +### Pythonで解く際の特有の注意点 + +PythonのLeetCode環境では`TreeNode`クラスは定義済みで使えます。Pythonは動的型付け言語(=変数の型を実行時に決める言語)なので、型ヒントがないと`root`が`None`なのか`TreeNode`なのかをpylanceが判断できません。`Optional[TreeNode]`(=`TreeNode`または`None`のどちらか)という型ヒントを明示することで、pylanceが実行前にバグを検出できるようになります。再帰(=関数が自分自身を呼び出す仕組み)はPythonではデフォルトで再帰深度が1000に制限されていますが、制約のノード数最大10^4でも木が完全に偏った場合(一本道)にはこの制限に引っかかる可能性があります。競技版ではこの点も補足します。 + +### 競技プログラミング視点 + +- **制約分析**:ノード数 ≤ 10^4。O(n)で十分間に合う +- **最速手法**:再帰DFS(深さ優先探索)。Pythonの`max()`はC実装なので比較処理がネイティブ速度 +- **メモリ最小化**:再帰コールスタックのみ(追加データ構造不要)。ただし偏った木では深さ最大10^4のスタックが積まれる + +### 業務開発視点 + +- **型安全設計**:`Optional[TreeNode]`で`None`の可能性を型レベルで明示。pylanceが`root.left`への誤アクセスを事前検出 +- **エラーハンドリング**:制約範囲内(ノード数0〜10^4・値-100〜100)の入力しか来ないためバリデーションは最小限に。空の木(`root=None`)はアルゴリズムのベースケースで自然に処理 + +### Python特有の分析 + +- **`max()` の活用**:`max(left, right)`はC実装の組み込み関数なので、`if left > right: return left`よりも高速かつ可読性が高い +- **再帰vs反復**:CPythonのデフォルト再帰制限(1000)を考慮し、巨大入力には`collections.deque`を使ったBFS版も提供する + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、`max()`や`min()`などの組み込み関数はC言語レベルで動作するため高速 +> - **`Optional[T]`**:「`T`型またはNone」を表す型ヒント。`from typing import Optional`でインポートする。Python 3.10以降は`T | None`と書ける +> - **動的型付け**:変数の型を実行時に決める仕組み。Pythonはこれを採用しており、型ヒントを書かないとpylanceが型エラーを検出できない +> - **再帰深度制限**:CPythonのデフォルトでは再帰は約1000回まで。`sys.setrecursionlimit()`で変更可能 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも解き方は複数あります。Pythonでは「C実装の組み込み関数を使えるか」「追加のデータ構造が必要か」もパフォーマンス上の重要な判断基準です。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ----------------------- | ---------- | ---------- | ---------------- | ------ | -------------------- | ------------- | ------------------ | +| **① DFS 再帰** | O(n) | O(h) | 低 | ★★★ | `max()`(C実装) | ◎ | 最もシンプル | +| **② BFS 反復(deque)** | O(n) | O(w) | 中 | ★★☆ | `collections.deque` | ○ | 再帰制限を回避 | +| **③ DFS 反復(stack)** | O(n) | O(h) | 中 | ★☆☆ | `list`をスタック代用 | △ | 型が複雑になりがち | + +> 💡 **各アプローチのPython固有の観点** +> +> - **① DFS再帰**:`max()`がC実装なので比較が最速。ただしCPythonの再帰制限に注意 +> - **② BFS(deque)**:`collections.deque`の`popleft()`はO(1)(`list.pop(0)`はO(n)なので使ってはいけない)。再帰制限を完全に回避できる +> - **③ DFS反復**:`list`をスタック代わりに使うが、`append()/pop()`がO(1)なので性能は問題なし。ただし可読性が最も低い +> 📖 **このセクションで登場した用語** +> - **`collections.deque`**:「両端開きの箱」のようなデータ構造。`list.pop(0)`(先頭削除)はO(n)かかるが、`deque.popleft()`はO(1)で済む +> - **BFS(Breadth-First Search=幅優先探索)**:木を段ごとに横方向に探索する手法。「何段あるか=何回レベルを処理したか」で深さを数えられる +> - **h(木の高さ)**:根から葉までの最長パスのノード数。再帰のスタックは最大h段積まれる + +--- + +## 3. 実装パターン + +> 💡 **コードの骨格(全体の流れ)** +> +> 1. `root`が`None`(空の木・または葉ノードの先)なら深さ`0`を返す(再帰の終了条件) +> 2. 左の部分木の深さを再帰で求める +> 3. 右の部分木の深さを再帰で求める +> 4. 左右の深さの大きい方に`1`(現在ノード分)を加えて返す + +--- + +### 【業務開発版を使う場面】 + +チームで長期間メンテナンスするプロダクションコードや、コードレビューが行われる現場に向きます。型ヒントとdocstringが充実しているため、後から読んだ人が「この関数は何をするのか・何を受け取るのか」を一目で理解できます。BFSを採用することでCPythonの再帰深度制限を完全に回避しており、本番環境での予期しないクラッシュを防ぎます。 + +```python +# Runtime 1 ms +# Beats 31.87% +# Memory 20.27 MB +# Beats 70.25% + +from typing import Optional +from collections import deque + +# 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 maxDepth(self, root: Optional[TreeNode]) -> int: + """ + 二分木の最大深さを返す(BFS反復版・業務開発向け)。 + + 再帰版よりコードは長くなるが、CPythonの再帰深度制限(デフォルト1000)を + 回避できるため、本番環境での安全性が高い。 + + Args: + root: 二分木の根ノード。None は空の木を意味する。 + + Returns: + 根から最も遠い葉ノードまでのノード数。空の木は 0 を返す。 + + Time Complexity: O(n) 全ノードを1回ずつ訪問するため + Space Complexity: O(w) w は木の最大幅(同じ深さのノード数の最大値) + """ + + # ── エッジケース:空の木 ────────────────────────────────────────── + # root が None の場合はノードが1つもないため、深さは 0。 + # 後続の deque 処理に None を入れないための早期リターン。 + if root is None: + return 0 + + # ── BFS のためのキューを初期化 ──────────────────────────────────── + # collections.deque を使う理由: + # list.pop(0) は先頭削除で O(n) かかる(全要素をずらすため)が、 + # deque.popleft() は O(1) で済む。 + # 根ノードをキューに入れてBFSを開始する。 + queue: deque[TreeNode] = deque([root]) + + # ── 深さカウンター ──────────────────────────────────────────────── + # 各「レベル(段)」を処理するたびに 1 ずつ増やす。 + # BFS は「同じ深さのノードを全部処理してから次の深さへ進む」 + # という特性があるため、この方法で深さを正確に数えられる。 + depth: int = 0 + + # ── BFS メインループ ────────────────────────────────────────────── + # キューが空になるまで「1段分のノードをまとめて処理」を繰り返す。 + while queue: + + # この時点での queue の長さ = 現在の深さにいるノードの数。 + # この level_size 個のノードを全部処理したら、1段下に進む。 + level_size: int = len(queue) + + # 現在の深さのノードを全て処理する。 + for _ in range(level_size): + # deque の先頭からノードを取り出す(O(1))。 + # list.pop(0) は O(n) なので絶対に使ってはいけない。 + node: TreeNode = queue.popleft() + + # 左の子が存在すれば、次のレベルとしてキューに追加する。 + # None チェックを先に行うことで、None ノードをキューに入れない。 + if node.left is not None: + queue.append(node.left) + + # 右の子が存在すれば、同じく次のレベルとしてキューに追加する。 + if node.right is not None: + queue.append(node.right) + + # 現在のレベルを全部処理し終えたので、深さを 1 増やす。 + depth += 1 + + return depth +``` + +--- + +### 動作トレース(業務開発版・入力例1) + +`root = [3, 9, 20, null, null, 15, 7]` でBFSがどう動くかを追います。 + +``` +木の構造: + 3 ← 深さ1 + / \ + 9 20 ← 深さ2 + / \ + 15 7 ← 深さ3 + +───────────────────────────────────────────────────────────────── +初期状態: root=TreeNode(3), queue=deque([3]), depth=0 + +【レベル1の処理】 + level_size = 1 + popleft() → node=TreeNode(3) + node.left = TreeNode(9) → append → queue=deque([9]) + node.right = TreeNode(20) → append → queue=deque([9, 20]) + レベル完了 → depth = 1 + +【レベル2の処理】 + level_size = 2 + popleft() → node=TreeNode(9) + node.left = None → スキップ + node.right = None → スキップ + popleft() → node=TreeNode(20) + node.left = TreeNode(15) → append → queue=deque([15]) + node.right = TreeNode(7) → append → queue=deque([15, 7]) + レベル完了 → depth = 2 + +【レベル3の処理】 + level_size = 2 + popleft() → node=TreeNode(15) + node.left = None, node.right = None → 両方スキップ + popleft() → node=TreeNode(7) + node.left = None, node.right = None → 両方スキップ + レベル完了 → depth = 3 + +while queue → queue=deque([]) → 空なのでループ終了 +───────────────────────────────────────────────────────────────── +return 3 ✅ +``` + +--- + +### 【競技プログラミング版を使う場面】 + +LeetCodeなど制限時間内に正解を出すことが目的の場合に向きます。再帰DFSで実装しており、コードが非常に短く、アルゴリズムの本質(「深さ=1+左右の大きい方の深さ」)が一目で分かります。完全二分木であれば深さは約log2(10^4) ≈ 14程度で安全ですが、左右どちらかに偏った退化木(skewed tree)の場合は深さが最大10^4に達する可能性があります。そのため、そのようなエッジケースでは呼び出し側で`sys.setrecursionlimit(...)`を設定するか、反復アプローチを用いる必要があります。 + +```python +from typing import Optional + +class Solution: + def maxDepth(self, root: Optional[TreeNode]) -> int: + # ── ベースケース ────────────────────────────────────────────────── + # root が None = 「この方向には木がない」を意味する。 + # 存在しない木の深さは 0 なので 0 を返して再帰を終了させる。 + # Python の "if not root:" は None と TreeNode(0) の両方でTrueになるため + # 明示的に "is None" で書くほうがpylanceの型推論上も安全。 + if root is None: + return 0 + + # ── 再帰ステップ ────────────────────────────────────────────────── + # max() は C実装の組み込み関数なので、if文での比較よりも高速。 + # 「左の深さ」と「右の深さ」を再帰で求め、大きい方を選んで +1 する。 + # +1 は「今いるこのノード自身」のカウント分。 + return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right)) +``` + +--- + +### 動作トレース(競技プログラミング版・入力例2) + +`root = [1, null, 2]` で再帰がどう展開・収束するかを追います。 + +``` +木の構造: + 1 ← 深さ1 + \ + 2 ← 深さ2 + +───────────────────────────────────────────────────────────────── +Call 1: maxDepth(TreeNode(1)) + ├─ root is not None → 再帰ステップへ + ├─ Call 2: maxDepth(None) ← root.left + │ └─ root is None → return 0 + └─ Call 3: maxDepth(TreeNode(2)) ← root.right + ├─ root is not None → 再帰ステップへ + ├─ Call 4: maxDepth(None) ← TreeNode(2).left + │ └─ return 0 + └─ Call 5: maxDepth(None) ← TreeNode(2).right + └─ return 0 + └─ 1 + max(0, 0) = 1 + +Call 1 の最終結果: 1 + max(0, 1) = 2 ✅ +───────────────────────────────────────────────────────────────── +return 2 +``` + +> 💡 **`max()` がなぜ高速か(最適化前→後→理由)** +> +> ```python +> # 最適化前:if文での比較 +> if left_depth > right_depth: +> return 1 + left_depth +> else: +> return 1 + right_depth +> +> # 最適化後:組み込み関数 max() を使う +> return 1 + max(left_depth, right_depth) +> +> # なぜ速いか:max() はC言語実装。Pythonインタープリタを介さずに +> # C言語レベルで比較するため、if文より高速かつコードが短くなる。 +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **`deque.popleft()`**:dequeの先頭から要素を取り出す操作。O(1)で動作する。`list.pop(0)`はO(n)なので木の幅優先探索には必ず`deque`を使う +> - **レベル(BFSの文脈で)**:木の同じ深さにいるノードの集合。BFSは1レベルずつ処理するため、処理したレベル数=深さになる +> - **ベースケース**:再帰を止める条件。「これ以上分割できない最小の状態」を定義する。Pythonでは`None`チェックがこれにあたる +> - **`Optional[TreeNode]`**:`TreeNode | None`と同じ意味。pylanceに「この引数はNoneかもしれない」と伝えることで、`root.left`への無条件アクセスを実行前に警告してくれる + +--- + +## 4. エッジケース検証 + +> 💡 エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +``` +【ケース1】空の木: root = None + → 業務版: root is None → return 0 ✅ + → 競技版: root is None → return 0 ✅ + +【ケース2】ノードが1つだけ: root = [1] + → 業務版: queue=deque([1]) → level処理 → depth=1 → return 1 ✅ + → 競技版: 1 + max(maxDepth(None), maxDepth(None)) + = 1 + max(0, 0) = 1 ✅ + +【ケース3】右に偏った一本道(再帰深度の観点で最悪ケース): root = [1,null,2,null,3,...,null,10000] + → 業務版: BFS使用のため再帰制限なし。depth=10000 ✅ + → 競技版: 再帰深度10000 > CPythonデフォルト制限1000 → 注意が必要 + 対処: コード冒頭に import sys; sys.setrecursionlimit(20000) を追加 + +【ケース4】完全二分木(最大ノード数 10^4) + → 高さ h ≈ log2(10000) ≈ 14 なので再帰深度は約14。安全範囲内。✅ +``` + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空の木・ノード1つ・一本道など、境界的な条件の入力のこと +> - **`sys.setrecursionlimit(n)`**:CPythonの再帰深度制限をn回まで拡張する関数。デフォルトは約1000 +> - **完全二分木**:全ての内部ノードが2つの子を持ち、葉が全て同じ深さにある理想的な木。高さはlog₂(n)に収まる diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Rust.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Rust.md new file mode 100644 index 0000000..00dbfbb --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Rust.md @@ -0,0 +1,268 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Rust +> 適用ルールセット: 共通5ルール + Rust固有5ルール +> 参照ファイル: references/common.md + references/rust.md + +--- + +# 104. Maximum Depth of Binary Tree(Rust版) + +--- + +## 1. 問題の分析 + +> 💡 **一言で言うと**:「`Rc>`という多重ラッパーに包まれた木を再帰で降りていき、一番深い段数を数える問題」です。 + +### Rustで解く際に特に気をつけるべき点 + +このLeetCodeのRust用ノード定義は、TypeScript版と比べて**型が非常に複雑**です。なぜなら、Rustの所有権ルール(=値を同時に複数箇所から所有できない仕組み)のせいで、「左の子・右の子を複数箇所から参照したい」という木構造を素直に表現できないからです。その解決策として `Rc>` という3つの仕組みを組み合わせた型が使われます。これを最初に理解しておくことがRustの木問題の鍵です。 + +``` +Option>> + │ │ │ + │ │ └── RefCell:「実行時」に借用チェックを行う箱 + │ └─────── Rc:複数箇所からの「共有所有権」を可能にするスマートポインタ + └────────────── Option:子がいない(null)場合を安全に表現する型 +``` + +### 競技プログラミング視点での分析 + +- **実行速度**:全ノードを1回ずつ訪問するのが必須のため、O(n)が理論限界 +- **メモリ最小化**:再帰(=関数が自分自身を呼び出す仕組み)を使う場合、コールスタック(=関数の呼び出し記録が積み上がる高速なメモリ領域)の消費量は木の高さh分のみ。制約ではノード数最大10^4なので、スタックオーバーフロー(=再帰が深くなりすぎてスタックが溢れるエラー)のリスクはほぼない +- **`Rc::clone()`のコスト**:ポインタのコピーと参照カウントのインクリメントのみで、データの実コピーは発生しない。ゆえに非常に軽量 + +### 業務開発視点での分析 + +- **`Option` によるnull安全**:子ノードの有無を`Option`で表現するため、nullポインタ参照(=何もない場所へのアクセス)というクラッシュがコンパイル時に防がれる +- **`RefCell` の実行時借用チェック**:通常Rustは「コンパイル時」に借用ルールをチェックするが、木構造の複雑な共有には実行時チェックの`RefCell`が必要。`RefCell::borrow()` は複数の immutable borrow を同時に許可します。実行時パニックが発生するのは、`RefCell::borrow_mut()` が既存の borrow(immutable または mutable)と競合する場合のみです。今回は `Option` に包まれたノードに対して読み取りのみ行うため、そのリスクはありません。 + +### Rust特有の考慮点 + +- **`Rc` が必要な理由**:通常のRustでは値の所有者は1つだけ。しかし木のノードは「親から参照され、かつ自分の子を所有する」という複数の所有者が必要な構造のため、参照カウント(=何箇所から参照されているかを数える数字)で共有所有権を管理する`Rc`が使われる +- **借用とクローンの設計**:`.borrow()`で`RefCell`の中身を一時的に借り出した後、子ノードの`.clone()`(`Rc::clone`=参照カウントの増加のみ、データコピーなし)が必要 + +> 📖 **このセクションで登場した用語** +> +> - **所有権**:値を「誰が管理するか」をコンパイル時に決めるRust独自の仕組み。メモリ解放を自動かつ安全に行える +> - **`Rc`(Reference Counted)**:参照カウントで共有所有権を実現するスマートポインタ(=所有権管理機能付きのポインタ)。Java/Pythonの変数参照に近い挙動 +> - **`RefCell`**:コンパイル時ではなく「実行時」に借用ルールをチェックする箱。「内部可変性(=通常では変更できない場所を変更できるようにする仕組み)」パターンとも呼ばれる +> - **`Option`**:値があれば`Some(T)`、なければ`None`を返す型。JavaScriptの`null`と違い、使う前に必ず確認が強制される +> - **スタック**:関数呼び出しに使われる高速なメモリ領域。サイズが固定な値を置く場所 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも解き方は複数あります。Rustでは「所有権の移動が起きるか」「ヒープ(=動的にサイズが変わるデータを置くメモリ領域)のアロケーション(=メモリの確保)が何回起きるか」も重要な判断基準になります。 + +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | +| -------------------------- | ---------- | ---------- | -------------- | ------ | ------ | ----------------------------- | +| **① DFS 再帰** | O(n) | O(h) | 低 | 高 | ◎ | `.borrow()`+`Rc::clone()`のみ | +| **② BFS 反復(VecDeque)** | O(n) | O(w) | 中 | 高 | ○ | キューへの所有権移動が必要 | +| **③ DFS 反復(スタック)** | O(n) | O(h) | 中 | 高 | △ | `Vec`をスタック代わりに使用 | + +> 💡 **Rust固有の観点** +> +> - **① DFS再帰**:`Rc::clone()`は参照カウントのインクリメントのみなので、ヒープアロケーションはゼロ追加。最もRustの所有権モデルと相性が良い +> - **② BFS**:`VecDeque>>>` というキューへ都度`Rc::clone()`してenqueueする。追加アロケーションは最大幅w分 +> - **③ DFS反復**:`Vec`をスタックとして使うが、型が複雑になりやすく可読性が下がる +> 📖 **このセクションで登場した用語** +> - **`VecDeque`**:両端からの追加・取り出しが効率的なキュー(=行列)型。BFSの実装に使う +> - **アロケーション**:ヒープ上にメモリを確保する操作。頻繁に行うと速度が落ちる +> - **h(木の高さ)**:根から葉までの最長パスのノード数。平衡木ではO(log n)、一本道ではO(n) +> - **w(木の最大幅)**:同じ深さのノードの最大個数。完全二分木では最下段≈n/2個になる + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**:① DFS 再帰 + +- **理由**: + - **BFS(②)を選ばなかった理由**:`VecDeque`へのクローンと管理コードが増え、「深さの最大値を返す」というアルゴリズムの本質が見えにくくなるため + - **DFS反復(③)を選ばなかった理由**:再帰を手動の`Vec`スタックで模倣するため、`Option>>`の複雑な型と組み合わさるとコードが読みにくくなるため + - **DFS再帰(①)を選んだ理由**:「木の深さ = 1 + max(左の深さ, 右の深さ)」という構造が再帰でそのままコードに表現でき、**最も読みやすく保守しやすい** + +- **Rust特有の最適化ポイント**: + - `Rc::clone()`はポインタコピーと参照カウントのインクリメントのみ(O(1)・追加ヒープアロケーションなし)なので、ゼロコスト抽象化(=便利な書き方をしても手書きと同じ速さになる性質)の恩恵を最大限活かせる + - モノモーフィゼーション(=ジェネリクス関数が使われる型ごとに専用コードへ自動展開される仕組み)は今回不要だが、`match`による`Option`の展開はコンパイラが最適化しやすいパターン + - `.borrow()`で取得した`Ref`はスコープを抜けると自動で解放されるため、借用のリークが発生しない + +> 📖 **このセクションで登場した用語** +> +> - **ゼロコスト抽象化**:高レベルな書き方(`Rc`・`Option`・イテレータなど)をしても、手書きの低レベルコードと同等の速さになるRustの特性 +> - **`Ref`**:`RefCell::borrow()`が返す「一時的な読み取り専用の参照」。スコープを抜けると借用ロックが自動解除される +> - **モノモーフィゼーション**:`fn f(x: T)`のようなジェネリクス関数が、使われる型ごとに専用コードへ自動展開される仕組み + +--- + +## 4. 実装コード + +> 💡 **コードの骨格(全体の流れ)** +> +> 1. **ベースケース**:`root`が`None`(=子がいない)なら深さ`0`を返す +> 2. **RefCellを借り出す**:`node.borrow()`でノードの中身への読み取り参照を取得する +> 3. **左右の子を再帰探索**:左右の子をそれぞれ`Rc::clone()`して再帰呼び出しする +> 4. **結果の統合**:左右の深さの大きい方に`1`(現在のノード分)を加えて返す + +```rust +// Runtime 0 ms +// Beats 100.00% +// Memory 2.86 MB +// Beats 10.64% +use std::rc::Rc; +use std::cell::RefCell; + +impl Solution { + /// 二分木の最大深さを返す。 + /// + /// # Arguments + /// * `root` - 二分木の根ノード。`None` は空の木を意味する + /// + /// # Returns + /// 根から最も遠い葉ノードまでのノード数(空の木は 0) + /// + /// # Complexity + /// - Time: O(n) 全ノードを1回ずつ訪問するため + /// - Space: O(h) h は木の高さ(再帰のコールスタック分) + pub fn max_depth(root: Option>>) -> i32 { + + // ── ベースケース ──────────────────────────────────────────────── + // Option を match で展開する。 + // `None` はこの方向に子ノードが存在しないことを意味するため、 + // 深さ 0 を返して再帰を終了させる(ここが「底」)。 + // JavaやC++のnullチェックに相当するが、 + // Rustではコンパイラが`None`の処理忘れを検出してくれる。 + let node = match root { + None => return 0, + // `Some(n)` の場合、内部の `Rc>` を `node` に束縛する。 + // この時点では所有権はまだ `node` にある(消費されていない)。 + Some(n) => n, + }; + + // ── RefCell からノードの中身を借り出す ───────────────────────── + // `.borrow()` は RefCell の「実行時借用チェック」を通じて + // `Ref`(=読み取り専用の一時参照)を返す。 + // JavaやPythonでは参照を自由に渡せるが、Rustでは + // 「今誰が読んでいるか」を管理しているため、このステップが必要。 + // `borrowed` はこのスコープを抜けると自動で借用解放される。 + let borrowed = node.borrow(); + + // ── 左の部分木を再帰探索 ──────────────────────────────────────── + // `borrowed.left` の型は `Option>>` 。 + // `Ref` から left を直接 move(所有権移動)することはできないため、 + // `.clone()` を呼ぶ。`Rc::clone()` はポインタのコピーと + // 参照カウントのインクリメントのみで、データの実コピーは発生しない(O(1)・軽量)。 + let left_depth = Solution::max_depth(borrowed.left.clone()); + + // ── 右の部分木を再帰探索 ──────────────────────────────────────── + // 左と同じ理由で `.clone()` を使用。 + let right_depth = Solution::max_depth(borrowed.right.clone()); + + // ── 結果の統合 ───────────────────────────────────────────────── + // `i32::max()` で左右の深い方を選び、現在のノード分 +1 を加える。 + // なぜ +1 かというと、今 `borrowed` にいる「このノード自体」も + // 深さのカウントに含まれるため。 + 1 + left_depth.max(right_depth) + } +} +``` + +--- + +### 動作トレース(入力例1) + +`root = [3, 9, 20, null, null, 15, 7]` で再帰がどう展開・収束するかを追います。 + +``` +木の構造(再確認): + 3 + / \ + 9 20 + / \ + 15 7 + +────────────────────────────────────────────────────────────── +Call 1: max_depth(Some(3)) + ├─ root = Some(3) → match で node = Rc<3> + ├─ borrowed = node.borrow() → TreeNode { val:3, left:Some(9), right:Some(20) } + + ├─ Call 2: max_depth(Some(9)) ← left.clone() + │ ├─ borrowed = TreeNode { val:9, left:None, right:None } + │ ├─ Call 3: max_depth(None) → return 0 (9の左はNone) + │ ├─ Call 4: max_depth(None) → return 0 (9の右はNone) + │ └─ 1 + max(0, 0) = 1 + ├─ left_depth = 1 + + └─ Call 5: max_depth(Some(20)) ← right.clone() + ├─ borrowed = TreeNode { val:20, left:Some(15), right:Some(7) } + + ├─ Call 6: max_depth(Some(15)) ← left.clone() + │ ├─ borrowed = TreeNode { val:15, left:None, right:None } + │ ├─ Call 7: max_depth(None) → return 0 + │ ├─ Call 8: max_depth(None) → return 0 + │ └─ 1 + max(0, 0) = 1 + + └─ Call 9: max_depth(Some(7)) ← right.clone() + ├─ borrowed = TreeNode { val:7, left:None, right:None } + ├─ Call 10: max_depth(None) → return 0 + ├─ Call 11: max_depth(None) → return 0 + └─ 1 + max(0, 0) = 1 + + └─ right_depth: 1 + max(1, 1) = 2 + + └─ right_depth = 2 + +Call 1 の最終結果: 1 + max(1, 2) = 3 ✅ +────────────────────────────────────────────────────────────── +答え: 3 +``` + +### なぜ `.clone()` が必要か?図で理解する + +``` +通常のRustで子ノードをそのまま渡そうとした場合(コンパイルエラー): + + borrowed.left ──→ ここは Ref から値を「move(移動)」しようとしている + しかし borrowed は借用中なので、中身を move できない → ❌ エラー + +.clone() を使った場合(正しい方法): + + borrowed.left.clone() + │ + └──→ Rc のクローン = 参照カウント +1、ポインタのコピーのみ + ヒープ上のデータは一切コピーされない → ✅ 軽量・安全 +``` + +> 💡 **`match` による`Option`展開の仕組み** +> +> ``` +> match root { +> None => return 0, // 「箱が空」→ 深さ0を返して終了 +> Some(n) => n, // 「箱に値あり」→ 中身nを取り出す +> } +> ``` +> +> C++の`if (root == nullptr)`・JavaScriptの`if (!root)`に相当するが、 +> Rustでは`None`の処理を書き忘れるとコンパイルエラーになるため、 +> バグが実行前に検出される。 +> 📖 **このセクションで登場した用語** +> +> - **`match`**:Rustのパターンマッチ構文。`Option`や`Result`の全ケースを網羅的に処理でき、処理漏れがあるとコンパイルエラーになる +> - **`borrow()`**:`RefCell`の中身を「実行時借用チェック付き」で読み取り参照として取り出すメソッド。`.borrow_mut()`で書き込み可能参照になる +> - **`Rc::clone()`**:参照カウントを+1するだけで、ヒープ上のデータは複製しない軽量なクローン。Pythonの変数への参照追加と動作が似ている +> - **`move`(移動)**:値の所有権を別の変数・場所へ渡すこと。移動後は元の変数から使えなくなる。JavaやPythonにはない概念 +> - **`Ref`**:`RefCell::borrow()`が返す型。スコープを抜けると自動で借用ロックを解除する「スマートな一時参照」 +> - **パニック**:Rustで回復不能なエラーが起きたときの即時クラッシュ。`RefCell`で同時に複数箇所から`.borrow_mut()`を呼ぶと発生する(今回は読み取り専用なので安全) + +--- + +## Rust固有の最適化観点まとめ + +| 観点 | この問題での適用箇所 | 効果 | +| --------------------------------- | ------------------------ | ---------------------------------------------- | +| **`Option`によるnull安全** | `root`・左右の子の型 | nullポインタ参照クラッシュをコンパイル時に防止 | +| **`Rc::clone()`の軽量性** | 左右の子を再帰に渡す際 | ヒープアロケーションなし、参照カウントのみ | +| **`RefCell::borrow()`の自動解放** | `borrowed`のスコープ管理 | 借用ロックのリーク・パニックが起きない | +| **`match`による網羅性保証** | `None`/`Some`の分岐 | 処理漏れをコンパイラが検出 | +| **コールスタックの深さ制御** | 再帰の深さ = 木の高さh | 制約10^4ノードは安全範囲内(スタック溢れなし) | diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Typescript.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Typescript.md new file mode 100644 index 0000000..ed23f4a --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/Maximum_Depth_of_Binary_Tree_Typescript.md @@ -0,0 +1,240 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: TypeScript +> 適用ルールセット: 共通5ルール + TS固有5ルール +> 参照ファイル: references/common.md + references/typescript.md + +--- + +# 104. Maximum Depth of Binary Tree + +--- + +## 1. 問題の分析 + +> 💡 **一言で言うと**:「木(ツリー)の一番深いところまで何段あるかを数える問題」です。 + +### 問題を図で理解する + +まず「二分木(=各ノードが最大2つの子を持つ木構造のデータ)」を視覚的に理解しましょう。 + +``` +例1: root = [3,9,20,null,null,15,7] + + 3 ← 深さ1(根・root) + / \ + 9 20 ← 深さ2 + / \ + 15 7 ← 深さ3(葉・leaf) + +最大深さ = 3 + +例2: root = [1,null,2] + + 1 ← 深さ1 + \ + 2 ← 深さ2 + +最大深さ = 2 +``` + +### 競技プログラミング視点での分析 + +- **実行速度優先**:すべてのノードを1回は必ず訪問しなければならないため、O(n)が理論限界 +- **メモリ最小化**:再帰(=関数が自分自身を呼び出す仕組み)を使うと、コールスタック(=関数呼び出しの記録が積み上がる場所)の深さは木の高さh分だけ使用。最悪O(n)(一本道の木)、平均的な平衡木ではO(log n) + +### 業務開発視点での分析 + +- **型安全性**:`TreeNode | null`という型が既に与えられており、nullを明示的に扱う必要がある +- **可読性**:再帰DFSは問題の本質(「左右の深いほうを選ぶ」)を直接コードで表現できるため非常に読みやすい +- **エラーハンドリング**:制約により`-100 <= Node.val <= 100`、ノード数は最大10^4なので、スタックオーバーフロー(=再帰が深くなりすぎてメモリが枯渇するエラー)のリスクは極めて低い + +### TypeScript特有の考慮点 + +- `TreeNode | null`という**ユニオン型**(=複数の型のどれかであることを表す型)を型ガード(`if (root === null)`)で絞り込む +- LeetCode環境では`TreeNode`クラスは定義済みなので、再定義は不要 + +> 📖 **このセクションで登場した用語** +> +> - **二分木**:各ノード(節)が最大2つの子(左・右)を持つ木構造のデータ形式 +> - **葉(leaf)**:子を持たないノード。木の末端にあたる +> - **再帰**:関数が自分自身を呼び出す手法。木の探索と非常に相性が良い +> - **ユニオン型**:`A | B`と書き、「AまたはB」のどちらかであることを表すTypeScript固有の型 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも解き方は複数あります。「速さ」と「メモリの使いやすさ」を比べて最適なものを選びます。木の問題では主にDFS(深さ優先探索)とBFS(幅優先探索)という2つの大きなアプローチがあります。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| -------------------------- | ---------- | ---------- | ------------ | -------- | ------ | -------------------- | +| **① DFS 再帰** | O(n) | O(h) | 低 | 高 | ◎ | 最もシンプル | +| **② BFS 反復(キュー)** | O(n) | O(w) | 中 | 高 | ○ | レベル順探索 | +| **③ DFS 反復(スタック)** | O(n) | O(h) | 中 | 高 | △ | 再帰をスタックで模倣 | + +> 💡 **Big-O記法の読み方** +> +> - `O(n)`:ノード数nに比例した処理時間(全ノードを1回ずつ訪問するため) +> - `O(h)`:hは木の高さ(height)。平衡木ではO(log n)、最悪の一本道ではO(n) +> - `O(w)`:wは木の最大幅(width)。完全二分木では最下段のノード数≈n/2なのでO(n)になりうる +> 📖 **このセクションで登場した用語** +> - **DFS(Depth-First Search=深さ優先探索)**:根から一本道を葉まで探索してから戻り、次の道を探す方式。迷路を一本道ずつ進む探索に似ている +> - **BFS(Breadth-First Search=幅優先探索)**:根から同じ深さのノードを全て見てから、次の深さに進む方式。木を段ごとに横に見ていくイメージ +> - **キュー(Queue)**:「先に入れたものを先に出す」データ構造。行列(レジの並び)と同じ仕組み +> - **スタック(Stack)**:「後に入れたものを先に出す」データ構造。皿の積み重ねと同じ仕組み + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**:① DFS 再帰(Recursive DFS) + +- **理由**: + - **BFS(②)を選ばなかった理由**:BFSはキューを使って幅方向に探索するため、コード量が増え、この問題の本質(「深さの最大値を返す」)が見えにくくなります + - **DFS反復(③)を選ばなかった理由**:再帰を手動スタックで模倣するため、コードが複雑になりやすく、保守性が下がります + - **DFS再帰(①)を選んだ理由**:「木の深さ=1+左右の深い方の子の深さ」というアルゴリズムの本質を、コードがそのまま表現できるため、最も読みやすく保守しやすい + +- **TypeScript特有の最適化ポイント**: + - `root === null`の型ガードにより、コンパイル時にnull安全性(=nullへのアクセスによるクラッシュを防ぐ仕組み)が保証される + - `Math.max()`は型推論(=型を明示しなくてもTypeScriptが自動で型を判断する機能)により`number`型を返すことがコンパイル時に保証される + +> 📖 **このセクションで登場した用語** +> +> - **null安全性**:`null`や`undefined`への誤ったアクセスによるクラッシュを防ぐ仕組み +> - **型推論**:型を明示しなくてもTypeScriptが文脈から型を自動判断する機能。JavaScriptにはない + +--- + +## 4. 実装コード + +> 💡 **コードの骨格(全体の流れ)** +> +> 1. **ベースケース**:rootがnullなら深さ0を返す(再帰の止まる条件) +> 2. **再帰ステップ**:左の部分木と右の部分木それぞれの最大深さを再帰で求める +> 3. **結果の統合**:左右の深さの大きい方に1(現在のノード分)を加えて返す + +```typescript +// Runtime 0 ms +// Beats 100.00% +// Memory 58.95 MB +// Beats 54.36% +/** + * 二分木の最大深さを返す + * @param root - 二分木の根ノード(nullの場合は空の木) + * @returns 根から最も遠い葉ノードまでのノード数 + * @complexity Time: O(n), Space: O(h) ※hは木の高さ + */ +function maxDepth(root: TreeNode | null): number { + // ── ベースケース(再帰の終了条件)────────────────────────────── + // rootがnullということは「この方向には木がない」を意味する。 + // 存在しない木の深さは0なので0を返す。 + // ここが再帰を止める「底」になる。 + if (root === null) { + return 0; + } + + // ── 再帰ステップ ─────────────────────────────────────────────── + // 左の部分木(=左の子以下の木全体)の最大深さを再帰で求める。 + // root.leftがnullなら、次の呼び出しでベースケースに入り0が返る。 + const leftDepth: number = maxDepth(root.left); + + // 右の部分木(=右の子以下の木全体)の最大深さを再帰で求める。 + // leftDepthと同じ仕組みで、nullなら0が返る。 + const rightDepth: number = maxDepth(root.right); + + // ── 結果の統合 ───────────────────────────────────────────────── + // 左右の深さのうち「大きい方」を選び、現在のノード分(+1)を加える。 + // なぜ+1かというと、現在いるrootノード自体も深さの1カウントに含まれるため。 + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +--- + +### 動作トレース(入力例1) + +`root = [3, 9, 20, null, null, 15, 7]` を使って、再帰がどのように展開・収束するかをステップごとに追います。 + +``` +木の構造(再確認): + 3 + / \ + 9 20 + / \ + 15 7 + +──────────────────────────────────────────────── +Call 1: maxDepth(3) ← rootノード3で呼び出し開始 + └─ Call 2: maxDepth(9) ← 左の子ノード9へ + └─ Call 3: maxDepth(null) → return 0 (9の左はnull) + └─ Call 4: maxDepth(null) → return 0 (9の右はnull) + └─ Math.max(0, 0) + 1 = 1 + └─ leftDepth = 1 + + └─ Call 5: maxDepth(20) ← 右の子ノード20へ + └─ Call 6: maxDepth(15) ← 20の左の子ノード15へ + └─ Call 7: maxDepth(null) → return 0 (15の左はnull) + └─ Call 8: maxDepth(null) → return 0 (15の右はnull) + └─ Math.max(0, 0) + 1 = 1 + └─ Call 9: maxDepth(7) ← 20の右の子ノード7へ + └─ Call 10: maxDepth(null) → return 0 (7の左はnull) + └─ Call 11: maxDepth(null) → return 0 (7の右はnull) + └─ Math.max(0, 0) + 1 = 1 + └─ Math.max(1, 1) + 1 = 2 + └─ rightDepth = 2 + +Call 1 の最終結果: Math.max(1, 2) + 1 = 3 ✅ +──────────────────────────────────────────────── +答え: 3 +``` + +### 動作トレース(入力例2) + +`root = [1, null, 2]` + +``` +木の構造: + 1 + \ + 2 + +──────────────────────────────────────────────── +Call 1: maxDepth(1) + └─ Call 2: maxDepth(null) → return 0 (1の左はnull) + └─ leftDepth = 0 + + └─ Call 3: maxDepth(2) + └─ Call 4: maxDepth(null) → return 0 (2の左はnull) + └─ Call 5: maxDepth(null) → return 0 (2の右はnull) + └─ Math.max(0, 0) + 1 = 1 + └─ rightDepth = 1 + +Call 1 の最終結果: Math.max(0, 1) + 1 = 2 ✅ +──────────────────────────────────────────────── +答え: 2 +``` + +> 📖 **このセクションで登場した用語** +> +> - **ベースケース(base case)**:再帰呼び出しを止める条件。「これ以上分割できない最小の状態」を定義する。ベースケースがないと無限ループになる +> - **再帰(recursion)**:関数が自分自身を呼び出す手法。問題を「同じ形の小さな問題」に分割して解くのに最適 +> - **部分木(subtree)**:木のある節を根として見たときの、その節以下の木全体のこと +> - **コールスタック(call stack)**:関数呼び出しの記録が積み上がる場所。再帰の深さに比例してメモリを使う +> - **readonly(読み取り専用)**:変数の値を変更できないようにするTypeScript固有の修飾子。JavaScriptには存在せず、意図せぬ書き換えをコンパイル時に防ぐ + +--- + +## TypeScript固有の最適化観点まとめ + +| 観点 | この問題での適用箇所 | 効果 | +| --------------------------------- | ------------------------- | ------------------------------------------------ | +| **ユニオン型** `TreeNode \| null` | 引数・再帰呼び出しの型 | nullを忘れるとコンパイルエラーになるため安全 | +| **型推論** | `leftDepth`, `rightDepth` | `number`型が自動判定され、誤った型の演算を防ぐ | +| **型ガード** `=== null` | ベースケース | nullアクセスのクラッシュをコンパイル時に防止 | +| **戻り値型注釈** `: number` | 関数シグネチャ | 誤った型のreturnがあればコンパイルエラーで即発見 | + +> 📖 **TypeScript固有の最終用語集** +> +> - **型ガード**:`if (root === null)` のように実行時に型を絞り込む仕組み。JavaScriptにもある書き方だが、TypeScriptではこれで型情報が自動的に変わる(`null`が除外される) +> - **コンパイル時エラー**:TypeScriptコードをJavaScriptに変換する段階で検出するエラー。実行前にバグを発見できるため、業務開発での価値が非常に高い +> - **戻り値型注釈**:関数の返す値の型を明示する記法(例: `): number`)。JavaScriptにはなく、誤った値の返却をコンパイル時に防ぐ diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README.md new file mode 100644 index 0000000..0fc27a4 --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README.md @@ -0,0 +1,779 @@ +# Maximum Depth of Binary Tree — 木の最大深さを再帰・BFSで求める + +--- + +## 目次(Table of Contents) + +- [Overview](#overview) +- [Algorithm](#algorithm) + - [アルゴリズム要点 TL;DR](#tldr) + - [図解](#figures) + - [正しさのスケッチ](#correctness) +- [Complexity](#complexity) +- [Implementation](#implementation) + - [Python 実装](#impl) + - [エッジケースと検証観点](#edgecases) +- [Optimization](#optimization) + - [CPython 最適化ポイント](#cpython) + - [FAQ](#faq) + +--- + +

Overview

+ +> 💡 この問題は、一言で言うと「木(ツリー)の一番深いところまで何段あるかを数える問題」です。 + +### 問題の要約 + +与えられた二分木(=各ノードが最大2つの子「左・右」を持つ木構造データ)の根(root)から、 +一番遠い**葉ノード**(=子を持たない末端のノード)までのノード数を返してください。 + +``` +例1: + 3 ← 深さ 1(根・root) + / \ + 9 20 ← 深さ 2 + / \ + 15 7 ← 深さ 3(葉ノード) + +最大深さ = 3 + +例2: + 1 ← 深さ 1 + \ + 2 ← 深さ 2 + +最大深さ = 2 +``` + +### なぜこの問題が重要か + +「木の深さ」を求めるには、**すべてのノードを1回は必ず訪問しなければならない**という点がポイントです。 +単純に数を数えるだけでなく、「左の部分木」と「右の部分木」のどちらがより深いかを比較しながら +探索していく必要があります。この「比較しながら探索する」という考え方が、 +後述する再帰DFSやBFSの核心になります。 + +### 制約 + +| 項目 | 内容 | +| ---------- | ------------------ | +| ノード数 | 0 以上 10,000 以下 | +| ノードの値 | -100 以上 100 以下 | + +> 📖 **この章で登場した用語** +> +> - **二分木(Binary Tree)**:各ノードが最大2つの子(左・右)を持つ木構造のデータ形式 +> - **根(root)**:木の一番上にあるノード。木の入り口になる +> - **葉(leaf)**:子を持たないノード。木の末端にある +> - **制約**:入力として与えられる値の範囲や条件。例:「ノード数は0以上10,000以下」 +> - **部分木(subtree)**:木のあるノードを根として見たときの、そのノード以下の木全体 + +--- + +

Algorithm

+ +

アルゴリズム要点(TL;DR)

+ +> 💡 TL;DR(Too Long; Didn't Read)とは「長くて読めない人向けの要約」です。 +> ここではアルゴリズム全体の戦略を箇条書きでまとめます。詳細は後の章で説明するので、 +> **「なんとなくこういう手順で解くんだな」というイメージを掴む章**として読んでください。 + +### 戦略(2パターン) + +#### 競技プログラミング版:再帰 DFS(深さ優先探索) + +- **アルゴリズム**:「木の深さ = 1 + max(左の深さ, 右の深さ)」を再帰で表現する +- **データ構造**:追加のデータ構造は不要。CPython のコールスタック(関数呼び出しの記録)のみ使う +- **なぜ再帰か**:「左右に分岐しながら深く探索する」という木構造の性質と再帰は非常に相性が良いから +- **時間計算量**:O(n)(全ノードを1回ずつ訪問) +- **空間計算量**:O(h)(h = 木の高さ。再帰のスタックがh段分積まれる) + +#### 業務開発版:反復 BFS(幅優先探索)+ `collections.deque` + +- **アルゴリズム**:木を「段ごと(レベルごと)」に処理し、段数を数える +- **データ構造**:`collections.deque`(両端キュー)を使ってBFSを実装する +- **なぜ deque か**:`list.pop(0)` は先頭削除がO(n)かかるが、`deque.popleft()` はO(1)で済むから +- **なぜBFSか**:CPythonの再帰深度制限(デフォルト約1000)を完全に回避できるから +- **時間計算量**:O(n)(全ノードを1回ずつ訪問) +- **空間計算量**:O(w)(w = 木の最大幅。同じ深さのノード数の最大値) + +> 📖 **この章で登場した用語** +> +> - **DFS(Depth-First Search=深さ優先探索)**:根から一本道を葉まで探索してから戻り、次の道を探す方式。迷路を一本道ずつ進む探索に似ている +> - **BFS(Breadth-First Search=幅優先探索)**:根から同じ深さのノードを全て見てから次の深さへ進む方式。木を段ごとに横に見ていくイメージ +> - **コールスタック**:関数が呼び出されるたびにその記録が積み上がるメモリ領域。再帰の深さに比例して消費される +> - **`collections.deque`**:「両端開きの箱」。前からも後ろからも O(1) で出し入れできるデータ構造 + +--- + +

図解

+ +> 💡 この章では、アルゴリズムの「処理の流れ」を視覚的に示します。 +> Mermaidフローチャートの読み方: +> +> - **長方形(`[]`)**:処理ステップ(何かを実行する) +> - **ひし形(`{}`)**:条件分岐(Yes/Noで処理が分かれる) +> - **矢印(`-->`)**:処理の流れ + +--- + +### フローチャート①:競技プログラミング版(再帰 DFS) + +この図は `maxDepth(root)` が再帰的に呼び出され、ベースケースから結果を積み上げていく処理の流れを表しています。 +上から下へ読み進め、再帰呼び出しが「返ってくる流れ」を矢印で追ってください。 + +```mermaid +flowchart TD + Start[Start maxDepth root] + Base{root is None} + Ret0[Return 0] + CallL[Call maxDepth root.left] + CallR[Call maxDepth root.right] + Combine[Return 1 + max left_depth right_depth] + + Start --> Base + Base -- Yes --> Ret0 + Base -- No --> CallL + CallL --> CallR + CallR --> Combine +``` + +各ノードの意味: + +- `Start`:`maxDepth` 関数の入り口。`root`(ノードまたは`None`)を受け取る +- `Base`(ひし形):`root is None` かどうかを判定する条件分岐。再帰の**ベースケース**(終了条件) +- `Ret0`:`None`なので深さ0を返す。これ以上探索しない「底」 +- `CallL`:左の部分木に対して同じ関数を再帰呼び出し → `left_depth` が返ってくる +- `CallR`:右の部分木に対して同じ関数を再帰呼び出し → `right_depth` が返ってくる +- `Combine`:左右の深さを比較し、大きい方に現在のノード分(+1)を加えて返す + +--- + +### フローチャート②:業務開発版(反復 BFS) + +この図はキュー(`deque`)を使って木を段ごとに処理し、深さを数えていく処理の流れを表しています。 +「1レベル分全部処理してから次のレベルへ進む」という繰り返し構造に注目してください。 + +```mermaid +flowchart TD + Start2[Start maxDepth root] + Check{root is None} + Ret0B[Return 0] + Init[Init deque with root] + InitD[Set depth = 0] + LoopCheck{queue is empty} + RetD[Return depth] + LevelSize[level_size = len queue] + LevelLoop[Process level_size nodes] + Dequeue[node = queue.popleft] + AddLeft{node.left exists} + AppendL[Append node.left] + AddRight{node.right exists} + AppendR[Append node.right] + IncDepth[depth += 1] + + Start2 --> Check + Check -- Yes --> Ret0B + Check -- No --> Init + Init --> InitD + InitD --> LoopCheck + LoopCheck -- Yes --> RetD + LoopCheck -- No --> LevelSize + LevelSize --> LevelLoop + LevelLoop --> Dequeue + Dequeue --> AddLeft + AddLeft -- Yes --> AppendL + AddLeft -- No --> AddRight + AppendL --> AddRight + AddRight -- Yes --> AppendR + AddRight -- No --> LevelLoop + AppendR --> LevelLoop + LevelLoop --> IncDepth + IncDepth --> LoopCheck +``` + +各ノードの意味: + +- `Init`:根ノードをキューに入れてBFSの準備をする +- `LoopCheck`(ひし形):キューが空なら全ノードを処理済み → 深さを返す +- `LevelSize`:今のキューの長さ=今のレベルのノード数を記録する +- `LevelLoop`:`level_size` 個のノードをまとめて処理する(1段分の処理) +- `Dequeue`:キュー先頭のノードを取り出す(O(1) の `popleft()`) +- `AddLeft/AddRight`:左・右の子が存在すれば次のレベルとしてキューへ追加 +- `IncDepth`:1段分の処理が終わったので深さカウンターを+1 + +--- + +### データフロー図:入力から出力までの変換 + +この図は入力(`TreeNode`または`None`)が最終的に深さ(`int`)に変換されるまでのデータの流れを表しています。 + +```mermaid +graph LR + subgraph Precheck + A[Input root] --> B[None check] + B --> C[Init queue or recurse] + end + subgraph Core + C --> D[Visit each node] + D --> E[Collect children] + E --> F[Count levels or compare depths] + end + F --> G[Output depth int] +``` + +主要な流れの説明: + +- `Input → None check`:空の木(`root=None`)を早期に捕捉し、0を返す +- `Init → Visit`:BFSならキューへ、DFSなら再帰で各ノードを1回ずつ訪問する +- `Collect children → Count`:BFSは段数を、DFSは左右の深さの最大値を積み上げる + +--- + +### 代表例でのトレース(入力例1) + +`root = [3, 9, 20, null, null, 15, 7]`(業務開発版BFSで追跡) + +``` +初期状態: + queue = deque([TreeNode(3)]), depth = 0 + +【レベル1 の処理】 + level_size = 1 + popleft() → node = TreeNode(3) + left=TreeNode(9) → append → queue=[9] + right=TreeNode(20) → append → queue=[9, 20] + レベル完了 → depth = 1 + +【レベル2 の処理】 + level_size = 2 + popleft() → node = TreeNode(9) + left=None → スキップ + right=None → スキップ + popleft() → node = TreeNode(20) + left=TreeNode(15) → append → queue=[15] + right=TreeNode(7) → append → queue=[15, 7] + レベル完了 → depth = 2 + +【レベル3 の処理】 + level_size = 2 + popleft() → node = TreeNode(15) → 子なし → スキップ + popleft() → node = TreeNode(7) → 子なし → スキップ + レベル完了 → depth = 3 + +queue = deque([]) → 空 → ループ終了 +return 3 ✅ +``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理ステップ +> - **ベースケース(base case)**:再帰の終了条件。「これ以上分割できない最小の状態」 +> - **レベル(BFSの文脈)**:木の同じ深さにいるノードの集合。BFSは1レベルずつ処理する +> - **キュー(Queue)**:「先に入れたものを先に出す」データ構造。行列(レジの並び)と同じ仕組み + +--- + +

正しさのスケッチ

+ +> 💡 この章では「なぜこのアルゴリズムが常に正しい答えを返すと言えるか」の根拠を整理します。 +> 数学的な厳密な証明ではなく、「直感的に納得できる理由」のスケッチです。 + +### ①ベースケース(再帰の終了条件) + +`root is None` のとき `0` を返す。 +「存在しないノード」の深さは0であり、これは直感的にも正しい。 +また、すべての葉ノードの子は`None`なので、**全ての再帰呼び出しは必ずここで停止する**。 + +### ②不変条件(アルゴリズムが正しく動くために処理中ずっと成り立つべき条件) + +`maxDepth(node)` は「`node` を根とする部分木の最大深さ」を正しく返す、という性質が +すべての再帰呼び出しで成り立つ。 + +- 葉ノード(`node.left=None, node.right=None`)のとき:`1 + max(0, 0) = 1` → 深さ1 ✅ +- 子が1つのノードのとき:`1 + max(子の深さ, 0) = 1 + 子の深さ` → 正しく積み上がる ✅ +- 子が2つのノードのとき:`1 + max(左の深さ, 右の深さ)` → 深い方を選ぶ ✅ + +### ③網羅性(すべてのノードを処理できているか) + +- **DFS版**:`root.left` と `root.right` の両方を必ず再帰呼び出しするため、全ノードを1回ずつ訪問する +- **BFS版**:キューに入れたノードの左右の子を必ず全てキューへ追加するため、全ノードを1回ずつ訪問する + +### ④終了性(必ず有限ステップで終わるか) + +木のノード数は有限(制約:最大10,000)なので、再帰呼び出しの深さもBFSのループ回数も有限。 +どちらのアプローチも必ず終了する。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +> - **ベースケース**:再帰の終了条件。これがないと無限再帰になる + +--- + +

Complexity

+ +> 💡 計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| ---------- | ---------------------- | -------------------------- | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(log n)` | 入力の対数に比例 | 二分探索で半分ずつ絞る | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +--- + +### 計算量の比較表 + +| 実装 | 時間計算量 | 空間計算量 | 空間の詳細 | +| ------------------ | ---------- | ---------- | ---------------------------------------------- | +| 競技版(再帰 DFS) | **O(n)** | **O(h)** | h = 木の高さ。平衡木でO(log n)、一本道でO(n) | +| 業務版(反復 BFS) | **O(n)** | **O(w)** | w = 木の最大幅。完全二分木では最下段≈n/2でO(n) | + +- **n** = ノードの総数(最大10,000) +- **h(木の高さ)**:平衡な木ではlog₂(n)≈14段、最悪の一本道ではnに等しい +- **w(木の最大幅)**:完全二分木では最下段のノード数≈n/2。平均的にはhより大きくなることが多い + +### どちらの空間計算量が有利か? + +``` +木が「平衡に近い」場合: + DFS の空間: O(log n) ← 少ない ✅ + BFS の空間: O(n/2) ← 多い + +木が「一本道(最悪ケース)」の場合: + DFS の空間: O(n) ← 多い(再帰スタックが n 段積まれる) + BFS の空間: O(1) ← 少ない ✅(常にキューに1ノードしかない) +``` + +LeetCode制約(最大10,000ノード)では、どちらも実用上は問題ない範囲です。 + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **平衡木(balanced tree)**:左右の部分木の高さがほぼ等しい、理想的な形の木 +> - **一本道(skewed tree)**:全ノードが右の子のみ(または左の子のみ)を持つ最悪ケースの木 + +--- + +

Implementation

+ +

Python 実装

+ +> 💡 コードを読む前に、実装の**全体的な骨格**を確認しましょう。 + +**業務開発版(反復 BFS)の骨格:** + +1. `from typing import Optional` で型ヒントを有効にする +2. `root is None` チェックで空の木を早期に返す +3. `deque([root])` でBFS用キューを初期化する +4. `while queue:` で「キューが空になるまで」ループする +5. `level_size = len(queue)` で現在の段のノード数を記録する +6. `level_size` 回 `popleft()` を繰り返し、子をキューへ追加する +7. 1段の処理が終わるたびに `depth += 1` する + +**競技プログラミング版(再帰 DFS)の骨格:** + +1. `root is None` ならば `0` を返す(ベースケース) +2. `1 + max(self.maxDepth(root.left), self.maxDepth(root.right))` を返す(再帰ステップ) + +--- + +```python +from __future__ import annotations +# 型ヒントの前方参照を有効にする。 +# TreeNode を型として使うとき、定義前に参照してもエラーにならないようにするため。 + +from typing import Optional, TYPE_CHECKING +from collections import deque + +# TYPE_CHECKING ブロック:pylance(型チェッカー)に TreeNode の型を伝えるための宣言。 +# 実行時(LeetCode環境)には TreeNode はすでに定義済みなので、 +# このブロックは実行されない(型チェック時のみ有効)。 +if TYPE_CHECKING: + class TreeNode: + val: int + left: Optional[TreeNode] + right: Optional[TreeNode] + def __init__( + self, + val: int = 0, + left: Optional[TreeNode] = None, + right: Optional[TreeNode] = None, + ) -> None: ... + + +class Solution: + """ + LeetCode 104: Maximum Depth of Binary Tree + + 2つの実装を提供する: + - maxDepth : 業務開発向け(反復BFS。再帰深度制限を回避) + - maxDepth_recursive : 競技プログラミング向け(再帰DFS。最もシンプル) + """ + + # ════════════════════════════════════════════════════════ + # 業務開発版:反復 BFS(collections.deque を使用) + # ════════════════════════════════════════════════════════ + def maxDepth(self, root: Optional[TreeNode]) -> int: + """ + 二分木の最大深さを返す(BFS反復版・業務開発向け)。 + + CPythonのデフォルト再帰深度制限(約1000)を回避するため、 + 再帰を使わず deque を使った反復BFSで実装する。 + + Args: + root: 二分木の根ノード。None は空の木を意味する。 + + Returns: + 根から最も遠い葉ノードまでのノード数。空の木は 0。 + + Time: O(n) — 全ノードを1回ずつ訪問 + Space: O(w) — w は木の最大幅(同じ深さのノード数の最大値) + """ + + # ── エッジケース:空の木 ──────────────────────────────────────── + # root が None の場合、ノードが1つもないため深さは 0。 + # 後続の deque 処理に None を入れないための早期リターン。 + if root is None: + return 0 + + # ── BFS 用キューの初期化 ──────────────────────────────────────── + # collections.deque を使う理由: + # list.pop(0) は全要素をシフトするため O(n) かかる。 + # deque.popleft() は O(1) で済む。 + # 大量ノードを処理するときに list では著しく遅くなるため、 + # BFS には必ず deque を使うのが Python の慣習。 + queue: deque[TreeNode] = deque([root]) + + # ── 深さカウンター ────────────────────────────────────────────── + # 「1レベルの処理が完了するたびに +1」という方針でカウントする。 + # BFS は同じ深さのノードをまとめて処理するため、 + # この方法で正確に深さを数えられる。 + depth: int = 0 + + # ── BFS メインループ ──────────────────────────────────────────── + # キューが空になる = 全ノードを処理済み → ループ終了 + while queue: + + # この時点の queue の長さ = 「今のレベルにいるノードの数」。 + # この数だけ popleft() を行うことで「1段分だけ」処理できる。 + level_size: int = len(queue) + + # 今のレベルのノードを全て処理する。 + for _ in range(level_size): + # deque の先頭からノードを O(1) で取り出す。 + # list.pop(0) は使ってはいけない(O(n) になるため)。 + node: TreeNode = queue.popleft() + + # 左の子が存在すれば「次のレベル」としてキューへ追加する。 + # None チェックを先に行うことで、None をキューに入れない。 + # None をキューに入れると、次のループで node.left アクセス時に + # AttributeError が発生するリスクがある。 + if node.left is not None: + queue.append(node.left) + + # 右の子も同様に処理する。 + if node.right is not None: + queue.append(node.right) + + # 今のレベルを全部処理し終えた = 1段下りた。 + depth += 1 + + # 全レベルを処理し終えたので、数えた深さを返す。 + return depth + + # ════════════════════════════════════════════════════════ + # 競技プログラミング版:再帰 DFS(最もシンプルな実装) + # ════════════════════════════════════════════════════════ + def maxDepth_recursive(self, root: Optional[TreeNode]) -> int: + """ + 二分木の最大深さを返す(再帰DFS版・競技プログラミング向け)。 + + 「木の深さ = 1 + max(左の深さ, 右の深さ)」をそのままコードで表現。 + コードが極めて短く、アルゴリズムの本質が一目で分かる。 + + 注意: CPython のデフォルト再帰深度制限(約1000)があるため、 + ノード数が多い一本道の木では sys.setrecursionlimit() が必要。 + LeetCode 制約(最大10,000)では完全な一本道でなければ安全。 + + Time: O(n) — 全ノードを1回ずつ訪問 + Space: O(h) — h は木の高さ(再帰のコールスタック分) + """ + + # ── ベースケース ──────────────────────────────────────────────── + # root が None = 「この方向には木がない」。 + # 存在しないノードの深さは 0 なので 0 を返して再帰を終了する。 + # "if root is None:" と明示することで pylance の型推論が通る。 + # "if not root:" は TreeNode(val=0) でも True になる可能性があり、 + # 意図しない挙動になりうるため使わない。 + if root is None: + return 0 + + # ── 再帰ステップ ──────────────────────────────────────────────── + # max() は C言語実装の組み込み関数なので、if文での比較より高速。 + # 「左の部分木の深さ」と「右の部分木の深さ」を再帰で求め、 + # 大きい方を選んで現在のノード分(+1)を加える。 + return 1 + max( + self.maxDepth_recursive(root.left), # 左の部分木の深さ + self.maxDepth_recursive(root.right), # 右の部分木の深さ + ) +``` + +--- + +### コードの動作トレース(競技プログラミング版・入力例1) + +`root = [3, 9, 20, null, null, 15, 7]` + +``` +maxDepth_recursive(TreeNode(3)) を呼び出す + +Call 1: root=TreeNode(3) + ├─ Call 2: root=TreeNode(9) ← root.left + │ ├─ Call 3: root=None → return 0 (9の左はNone) + │ ├─ Call 4: root=None → return 0 (9の右はNone) + │ └─ 1 + max(0, 0) = 1 + │ ↑ left_depth = 1 + + └─ Call 5: root=TreeNode(20) ← root.right + ├─ Call 6: root=TreeNode(15) ← 20の左 + │ ├─ Call 7: root=None → return 0 + │ ├─ Call 8: root=None → return 0 + │ └─ 1 + max(0, 0) = 1 + + └─ Call 9: root=TreeNode(7) ← 20の右 + ├─ Call 10: root=None → return 0 + ├─ Call 11: root=None → return 0 + └─ 1 + max(0, 0) = 1 + + └─ 1 + max(1, 1) = 2 + ↑ right_depth = 2 + +Call 1 の最終結果: 1 + max(1, 2) = 3 ✅ +``` + +> 📖 **この章で登場した用語** +> +> - **`from __future__ import annotations`**:型ヒントを文字列として扱うようにする宣言。前方参照(=まだ定義されていないクラスを型として使う)を解決できる +> - **`TYPE_CHECKING`**:型チェックツール(pylance)が解析するときだけ`True`になる定数。実行時は`False`なのでブロック内のコードは実行されない +> - **`Optional[X]`**:`X`または`None`のどちらかであることを表す型ヒント。`X | None`と同じ意味(Python 3.10以降) +> - **`deque`**:両端キュー(Double-Ended Queue)。前後どちらからも O(1) で追加・削除できる + +--- + +

Optimization

+ +

CPython 最適化ポイント

+ +> 💡 この章では「同じ処理でも Python の書き方によって速さが変わる理由」を説明します。 +> 最適化テクニックは**最適化前 → 最適化後 → なぜ速くなるか**の3点セットで示します。 + +### 最適化①:`list.pop(0)` ではなく `deque.popleft()` を使う + +```python +# ── 最適化前:list を使った先頭削除(遅い)────────────────────────── +queue_list: list[TreeNode] = [root] +node = queue_list.pop(0) # O(n):全要素を1つずつ前へシフトするため遅い + +# ── 最適化後:deque を使った先頭削除(速い)───────────────────────── +from collections import deque +queue_deque: deque[TreeNode] = deque([root]) +node = queue_deque.popleft() # O(1):ポインタを1つ動かすだけ + +# なぜ速いか: +# list はメモリ上で連続した配列として実装されている。 +# 先頭を削除すると残り全要素を1つずつ前へずらす操作(シフト)が発生し O(n)。 +# deque は「前後にポインタを持つ双方向リンクリスト」なので、 +# 先頭の削除はポインタを1つ動かすだけで O(1) になる。 +``` + +### 最適化②:`max()` 組み込み関数を使う + +```python +# ── 最適化前:if文での比較──────────────────────────────────────────── +if left_depth > right_depth: + deeper = left_depth +else: + deeper = right_depth +return 1 + deeper + +# ── 最適化後:組み込み関数 max() を使う────────────────────────────── +return 1 + max(left_depth, right_depth) + +# なぜ速いか: +# max() は C言語で実装された組み込み関数。 +# Python インタープリタを介さずに C言語レベルで比較するため、 +# if文(Pythonバイトコード)より高速かつコードが短くなる。 +# 同様に min(), sum(), all(), any() も C実装なので積極的に活用する。 +``` + +### 最適化③:`root is None` vs `not root` の違い + +```python +# ── 安全ではない書き方:not root──────────────────────────────────────── +if not root: + return 0 +# 問題点:None チェックの意図が不明瞭になります。 + +# ── 推奨される書き方:is None────────────────────────────────────────── +if root is None: + return 0 +# 理由:"is None" を用いることでノードの欠損 (None) を判定する意図が明確になり、 +# pylance などの型チェッカーが、この分岐以降で root の型を確実に TreeNode へ +# 絞り込む(型ナローイングする)ことができるため推奨されます。 +``` + +### 重要ポイント:`len(queue)` のループ前キャプチャ(正しさの担保) + +```python +# ── 意図が不明瞭な書き方:直接 range(len(queue)) と書く──────────── +for _ in range(len(queue)): + +# ── 推奨される書き方:ループ前に一度だけキャプチャする──────────────── +level_size = len(queue) # 現在のレベルのノード数をスナップショットとして固定 +for _ in range(level_size): # 記録した数だけ厳密に処理する + +# なぜ必要か(最適化ではなく正しさのため): +# これは微小な高速化テクニックではなく、アルゴリズムの正しさを保つための重要な設計です。 +# ループの開始前に len(queue) を level_size にキャプチャしておくことで、現在のBFSレベルにある +# ノード数だけを正確に処理できます。ループ中に新しく queue に追加された子ノードが同じレベルの +# ノードとして処理されるのを防ぎ、レベルオーダー探索(段ごとの処理)の正しさを維持します。 +``` + +> 📖 **この章で登場した用語** +> +> - **バイトコード**:Pythonのコードがインタープリタに渡される前に変換された中間表現。if文はバイトコード命令として実行される +> - **C実装**:Pythonコードではなく内部でC言語で書かれた関数。Pythonバイトコードを介さないため大幅に高速 +> - **型ナローイング(Type Narrowing)**:条件分岐の後でpylanceが変数の型を絞り込む仕組み。`if root is None: return 0` の後では `root` が `TreeNode` であることをpylanceが認識できる +> - **双方向リンクリスト**:各要素が「前の要素」と「次の要素」へのポインタを持つデータ構造。先頭・末尾へのアクセスが O(1) + +--- + +

エッジケースと検証観点

+ +> 💡 エッジケースとは「入力が空・最小値・最大値・極端な形状」など、境界的な入力のことです。 +> 普通のテストは通るのに特定の入力でだけバグが発生する、というのがエッジケースの怖さです。 +> 各ケースで「なぜそのケースが問題になりうるか」も一緒に確認します。 + +| # | ケース | 入力 | 期待出力 | なぜ問題になりうるか | +| --- | ---------------- | ------------------------------ | ---------- | -------------------------------------------------------------- | +| 1 | 空の木 | `root = None` | `0` | `root.left` にアクセスすると `AttributeError` が発生する | +| 2 | ノードが1つ | `root = [1]` | `1` | 葉ノードの子は全て`None`。ベースケースが正しく機能するかの確認 | +| 3 | 左に偏った一本道 | `root = [1,2,null,3,null,...]` | ノード数 | 再帰深度がノード数に等しくなる。CPython再帰制限への最接近 | +| 4 | 右に偏った一本道 | `root = [1,null,2,null,3,...]` | ノード数 | 同上。`root.right` 方向のみに深くなるケース | +| 5 | 完全二分木 | `root = [1,2,3,4,5,6,7]` | `3` | 最も「平衡な」形。深さはlog₂(n)≈14(n=10,000時)で安全 | +| 6 | 最大ノード数 | 10,000ノード | 最大10,000 | 時間・空間計算量が制限に収まるかの確認 | +| 7 | 値が全て同じ | `root = [0,0,0,...]` | 木の高さ | 値ではなく構造(左右の子)で深さを判定するため、値は関係ない | +| 8 | 負の値を含む | `root = [-100,-50,50]` | `2` | 値の大小は深さの計算に影響しない。判定は `is None` のみ | + +### 特に注意すべきケース3・4の対処方法 + +```python +# 一本道ツリーで再帰版を使う場合の対処(競技プログラミング版のみ必要) +import sys + +# デフォルトの再帰深度制限(約1000)を引き上げる。 +# LeetCode では問題によっては必要になる場合がある。 +# 業務開発版(BFS)ではこの対処は不要。 +sys.setrecursionlimit(20000) +``` + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空の木・ノード1つ・最大サイズ入力など、境界的な条件の入力 +> - **AttributeError**:存在しない属性(プロパティ)にアクセスしようとしたときに起きるエラー。`None.left` のようなアクセスで発生する +> - **`sys.setrecursionlimit(n)`**:CPythonの再帰深度制限を n 回まで拡張する関数。デフォルトは約1000 +> - **完全二分木**:全ての内部ノードが2つの子を持ち、葉が全て同じ深さにある理想的な木 + +--- + +

FAQ

+ +> 💡 ここでは初学者がつまずきやすいポイントをQ&A形式でまとめます。 +> 各回答は「**結論 → 理由 → 補足(具体例)**」の順で書いています。 + +--- + +**Q1. なぜ `if root is None:` と書くのに `if not root:` を使わないのですか?** + +**結論**:`is None` のほうが安全で、pylance の型推論も正確に動くからです。 + +**理由**:`not root` は Python の「真偽値評価(falsy チェック)」を使います。 +`TreeNode` クラスに `__bool__` や `__len__` が定義されている場合、 +`val=0` のノードで `not root` が `True` になってしまう可能性があります。 +`root is None` は「`None` と完全に同一かどうか」だけをチェックするため、 +どんな `TreeNode` に対しても安全に動作します。 + +**補足**:pylance の観点からも `is None` チェックの後では変数の型が自動的に +`TreeNode`(`None`を除いた型)に絞り込まれる「型ナローイング」が機能します。 +これにより `root.left` へのアクセスが型エラーなしに認識されます。 + +--- + +**Q2. 業務開発版でなぜ `deque` を使うのですか?`list` ではダメなのですか?** + +**結論**:`list.pop(0)` が O(n) になるため、大量ノードで著しく遅くなります。 + +**理由**:Python の `list` はメモリ上で連続した配列として実装されています。 +先頭要素を削除する `pop(0)` を呼ぶと、残りの全要素を1つずつ前にずらす操作が必要で、 +ノード数 n に比例した時間(O(n))がかかります。 +`deque` は双方向リンクリストなので `popleft()` がO(1)で済みます。 + +**補足**:BFS では1つのノードを処理するごとに先頭削除が発生します。 +`list` を使うとトータルの空間計算量は変わらないのに、時間計算量がO(n²)まで悪化します。 +BFS を実装するときは**必ず `deque`**を使うのが Python のベストプラクティスです。 + +--- + +**Q3. 再帰版のほうがコードが短くてシンプルなのに、なぜ業務版では使わないのですか?** + +**結論**:CPython のデフォルト再帰深度制限(約1000)があるため、本番環境では危険だからです。 + +**理由**:LeetCode の制約(最大10,000ノード)で「一本道の木」が入力されると、 +再帰が10,000段深くなります。CPython のデフォルト制限(約1000)を大幅に超えるため、 +`RecursionError` でクラッシュします。 + +**補足**:`sys.setrecursionlimit(20000)` で制限を引き上げることは可能ですが、 +本番コードでグローバル設定を変更するのは副作用が大きく、業務開発では避けるべきです。 +BFS版はキューというデータ構造でループを管理するため、再帰深度制限に無関係に動作します。 + +--- + +**Q4. `level_size = len(queue)` を事前に記録するのはなぜですか?ループの中で直接 `len(queue)` を見ればよいのでは?** + +**結論**:ループ中にキューの長さが変わるため、事前に記録しておく必要があります。 + +**理由**:ループ内では `node.left` や `node.right` をキューに `append` しています。 +もし `for _ in range(len(queue)):` のように毎回 `len()` を評価すると、 +**次のレベルのノードも含めたカウント**でループが回り続け、1レベル分の処理が正しく区切れません。 + +**補足**:`level_size = len(queue)` と事前に「今のレベルのノード数」を固定することで、 +「今のレベルだけを処理する」という不変条件を守ることができます。 + +--- + +**Q5. `max()` の中に再帰呼び出しを直接入れても大丈夫ですか?** + +**結論**:問題ありません。Python では関数の引数は全て評価されてから関数に渡されます。 + +**理由**:`max(self.maxDepth_recursive(root.left), self.maxDepth_recursive(root.right))` は、 +まず `maxDepth_recursive(root.left)` が完全に評価されて結果(整数)が得られ、 +次に `maxDepth_recursive(root.right)` が評価されて結果(整数)が得られ、 +最後にその2つの整数が `max()` に渡されます。評価の順序は左から右で保証されています。 + +**補足**:一時変数に入れても結果は同じです。 +どちらを選ぶかは可読性の好みの問題ですが、競技プログラミングでは1行で書く派が多いです。 + +--- + +> 📖 **この章で登場した用語** +> +> - **`RecursionError`**:Pythonで再帰深度制限を超えたときに発生するエラー +> - **falsy(偽値)**:Pythonでの `bool(x)` が `False` になる値。`None`, `0`, `""`, `[]`, `{}` などが該当する +> - **型ナローイング(Type Narrowing)**:条件分岐後に変数の型が絞り込まれることをpylanceが認識する仕組み +> - **FAQ**:Frequently Asked Questions の略。よくある質問と回答のこと diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html new file mode 100644 index 0000000..483ae39 --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html @@ -0,0 +1,1552 @@ + + + + + + LeetCode 104 · Maximum Depth of Binary Tree + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「二分木の根から一番遠い葉まで何段あるかを数える問題」です。 +

+

+ 二分木(=各ノードが最大2つの子「左・右」を持つ木構造データ)の根(root)から、 + 一番遠い葉ノード(=子を持たない末端ノード)までのノード数を返します。 + すべてのノードを1回ずつ訪問しなければならないため、時間計算量の理論限界はO(n)です。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 木の深さを知るには左右両方の部分木をすべて探索しないと「どちらが深いか」が分からない +
  • +
  • + CPython + のデフォルト再帰深度制限(約1000)があるため、一本道の木では単純な再帰が失敗する +
  • +
+
+ + +
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量(DFS)
+
+
+
O(w)
+
空間計算量(BFS)
+
+
+
+ 0〜10,000 +
+
ノード数
+
+
+ + +
+
+

+ 入力例 1 +

+
+    3          ← 深さ 1
+   / \
+  9  20        ← 深さ 2
+    /  \
+   15   7      ← 深さ 3 (葉)
+

出力: 3

+

+ 深さ3まで葉ノードが存在するため、最大深さ=3 +

+
+
+

+ 入力例 2 +

+
+  1            ← 深さ 1
+   \
+    2          ← 深さ 2 (葉)
+

出力: 2

+

+ 右の子だけが存在し、葉は深さ2にあるため、最大深さ=2 +

+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 入力例1(root=[3,9,20,null,null,15,7])を使って、BFS反復版の動きを追います。 +

+
+
+ + +
+

+ Python 実装 +

+ +

+ 2つの実装パターンを提供します。 + 業務開発版(BFS)は再帰深度制限を回避するため本番環境に適しており、 + 競技プログラミング版(再帰DFS)は最もシンプルでコードが短い実装です。 +

+ + +
+

+ 📋 業務開発版(BFS)のコード構造 +

+
    +
  1. root が None(空の木)の場合はすぐ 0 を返す(エッジケース処理)
  2. +
  3. deque([root]) でキューを初期化し、depth = 0 を設定する
  4. +
  5. while queue: でキューが空になるまでループする
  6. +
  7. level_size = len(queue) で現在レベルのノード数を事前記録する
  8. +
  9. + level_size 回 popleft() を行い、左右の子が存在すればキューへ追加する +
  10. +
  11. 1レベル処理完了ごとに depth += 1 して最終的に depth を返す
  12. +
+
+ +
from __future__ import annotations
+from typing import Optional
+from collections import deque
+
+# ════════════════════════════════════════════════
+# 業務開発版:反復 BFS(CPython 再帰制限を回避)
+# ════════════════════════════════════════════════
+class Solution:
+    def maxDepth(self, root: Optional[TreeNode]) -> int:
+        # エッジケース:空の木(ノードが1つもない)→ 深さ 0
+        # 後続の deque 処理に None を入れないための早期リターン
+        if root is None:
+            return 0
+
+        # deque を使う理由:
+        #   list.pop(0) は先頭削除が O(n) だが
+        #   deque.popleft() は O(1) で済む
+        queue: deque[TreeNode] = deque([root])
+        depth: int = 0  # 処理したレベルの数 = 深さ
+
+        while queue:
+            # この時点の len(queue) = 今のレベルのノード数
+            # ループ前に固定することで「次のレベルのノードが
+            # append されても影響を受けない」ようにする
+            level_size: int = len(queue)
+
+            for _ in range(level_size):
+                node: TreeNode = queue.popleft()  # O(1)
+
+                # 左・右の子が存在すれば次のレベルとしてキューへ
+                if node.left is not None:
+                    queue.append(node.left)
+                if node.right is not None:
+                    queue.append(node.right)
+
+            # 今のレベルを全部処理し終えた = 1段降りた
+            depth += 1
+
+        return depth
+
+
+# ════════════════════════════════════════════════
+# 競技プログラミング版:再帰 DFS(最もシンプル)
+# ════════════════════════════════════════════════
+class Solution2:
+    def maxDepth(self, root: Optional[TreeNode]) -> int:
+        # ベースケース:None = 存在しないノードの深さは 0
+        # "is None" を使う理由:
+        #   通常のノードオブジェクトはvalの値にかかわらずtruthyですが、
+        #   ノードが存在しない(None)ことを真偽値ではなく明示的に判定するためです。
+        if root is None:
+            return 0
+
+        # max() は C実装の組み込み関数なので if文より高速
+        # 「左の深さ」と「右の深さ」の大きい方 + 現在ノード分(+1)
+        return 1 + max(
+            self.maxDepth(root.left),
+            self.maxDepth(root.right),
+        )
+ + +
+

+ ▶ 入力例1 [3,9,20,null,null,15,7] での動作トレース(BFS版) +

+
+初期状態: queue=deque([Node(3)]), depth=0
+
+【レベル1】level_size=1
+  popleft() → Node(3)
+    left=Node(9)   → append → queue=[Node(9)]
+    right=Node(20) → append → queue=[Node(9),Node(20)]
+  depth=1
+
+【レベル2】level_size=2
+  popleft() → Node(9)
+    left=None, right=None → 追加なし
+  popleft() → Node(20)
+    left=Node(15) → append → queue=[Node(15)]
+    right=Node(7) → append → queue=[Node(15),Node(7)]
+  depth=2
+
+【レベル3】level_size=2
+  popleft() → Node(15) → 子なし
+  popleft() → Node(7)  → 子なし
+  depth=3
+
+queue=deque([]) → 空 → ループ終了
+return 3 ✅
+
+
+ + +
+

+ 処理フローチャート +

+ + +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい + 赤=いいえ + 紫=ループ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + 開始: maxDepth(root) + + + + + + + + + root is None ? + + + (空の木チェック) + + + + + + はい + + + + return 0 + + + + + + いいえ + + + + + + queue = deque([root]) + + + depth = 0 + + + + + + + + + queue が空? + + + (while ループ判定) + + + + + + はい + + + + return depth + + + + + + いいえ + + + + + + level_size = len(queue) + + + + + + + + + level_size 回: node = queue.popleft() + + + 左右の子が存在すれば queue.append() + + + + + + + + + depth += 1 + + + + + + ループ継続 + + +
+ + +
+

+ 🔎 入力例1 [3,9,20,null,null,15,7] でのフロー追跡 +

+
    +
  1. 「開始」→ root=Node(3) を受け取る
  2. +
  3. + 「root is None?」→ Node(3) は None ではない → + いいえ の経路へ +
  4. +
  5. 「キュー初期化」→ queue=deque([Node(3)]), depth=0
  6. +
  7. + 「queue が空?」→ [Node(3)] は空でない → + いいえ +
  8. +
  9. + level_size=1 → Node(3) をpopleft → 子 Node(9),Node(20) をappend → + depth=1 +
  10. +
  11. + 「queue が空?」→ [Node(9),Node(20)] は空でない → level_size=2 → 処理 → + depth=2 +
  12. +
  13. + 「queue が空?」→ [Node(15),Node(7)] は空でない → level_size=2 → 処理 → + depth=3 +
  14. +
  15. + 「queue が空?」→ [] は空 → + はい → 「return depth」→ + 3 を返す ✅ +
  16. +
+
+
+ + +
+

+ 計算量分析 +

+ + +
+

+ 📖 Big-O 記法の読み方(n が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(log n)
+
+ 半分ずつ絞る
例:二分探索 +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ 実装 + + 時間計算量 + + 空間計算量 + + 空間の詳細 +
+ 業務版(BFS) + + O(n) + + O(w) + + w = 木の最大幅。完全二分木では最下段≈n/2 +
+ 競技版(再帰DFS) + + O(n) + + O(h) + + h = 木の高さ。平衡木でO(log n)、一本道でO(n) +
+
+ + +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 時間計算量 O(n):「木の最大深さ」を求めるには、全ノードを少なくとも1回は訪問しなければなりません(どの葉が一番深いか分からないため)。BFS版・DFS版どちらも全n個のノードを正確に1回ずつ処理するので + O(n) です。
+ 空間計算量(BFS)O(w):キューには「現在のレベルのノード」と「次のレベルのノード」が同時に入ります。同じ深さのノード数の最大値 + w が最大キューサイズになります。
+ 空間計算量(DFS)O(h):再帰呼び出しのたびにコールスタックに1段積まれます。最大で「木の高さ + h」段まで積まれるため O(h) です。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。 +

+
+
+ + BFS(幅優先探索) + +
+ Breadth-First Search + の略。木やグラフを「同じ深さ(レベル)のノードを全部見てから次の深さへ」進む方式で探索する手法。階数が同じ場所を先に全部確認してから次の階へ進むエレベーターのイメージ。Pythonでは + collections.deque + を使って実装する。 +
+
+ +
+ + DFS(深さ優先探索) + +
+ Depth-First Search + の略。木の一本道を葉(末端)まで進み切ってから戻り、次の道を探す方式。迷路で「行き止まりまで進んでから引き返す」探索法に似ている。再帰(関数が自分自身を呼び出す仕組み)と相性が良い。 +
+
+ +
+ + O(n) + 記法(ビッグオー記法) + +
+ アルゴリズムの速さやメモリ使用量が「入力の大きさ n + が増えるにつれてどう増えるか」を表す記法。O(n) は「n + が2倍になると処理も約2倍になる」こと、O(1) は「n + に関わらず常に一定」を意味する。 +
+
+ +
+ + コールスタック + +
+ 関数が呼び出されるたびに「この関数に戻ってくる場所」の記録が積み上がる高速なメモリ領域。お皿の積み重ねのように、最後に置いたものが最初に取り出される(LIFO)。再帰が深くなりすぎるとこれが溢れて + RecursionError + が発生する。 +
+
+ +
+ + + collections.deque(両端キュー) + +
+ 前後どちらからも O(1) + で要素を追加・削除できるデータ構造。「両端開きの箱」のイメージ。list.pop(0)(先頭削除)は全要素をずらすため O(n) かかるが、deque.popleft() + は O(1) で済む。BFS の実装には必ず deque を使う。 +
+
+ +
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す手法。「問題を同じ形の小さな問題に分割して解く」のに最適。必ず「ベースケース(終了条件)」が必要で、ないと無限に呼び出しが続いてエラーになる。木の探索と非常に相性が良い。 +
+
+ +
+ + 二分木(Binary Tree) + +
+ 各ノード(節)が最大2つの子(左・右)を持つ木構造のデータ形式。家系図のように根(root)を頂点として下に枝分かれしていく。子を持たないノードを「葉(leaf)」と呼ぶ。 +
+
+ +
+ + Optional[T](型ヒント) + +
+ 「T 型またはNone」のどちらかであることを表すPythonの型ヒント。Optional[TreeNode] + と書くと、pylance(型チェッカー)が「None + かもしれない」ことを理解し、None チェックなしに + root.left + へアクセスすると警告してくれる。 +
+
+ +
+ + ベースケース(Base + Case) + +
+ 再帰の「終了条件」。これ以上小さく分割できない最小の状態を定義する。この問題では + root is None + がベースケースで、「存在しないノードの深さは0」を意味する。ベースケースがないと再帰が無限に続いてエラーになる。 +
+
+
+
+ + +
+ LeetCode 104 · Maximum Depth of Binary Tree · Python 解説ページ +
+
+ + + + + + + + + + + + + diff --git a/public/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html b/public/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html new file mode 100644 index 0000000..c97fbf2 --- /dev/null +++ b/public/Algorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html @@ -0,0 +1,1552 @@ + + + + + + LeetCode 104 · Maximum Depth of Binary Tree + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「二分木の根から一番遠い葉まで何段あるかを数える問題」です。 +

+

+ 二分木(=各ノードが最大2つの子「左・右」を持つ木構造データ)の根(root)から、 + 一番遠い葉ノード(=子を持たない末端ノード)までのノード数を返します。 + すべてのノードを1回ずつ訪問しなければならないため、時間計算量の理論限界はO(n)です。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 木の深さを知るには左右両方の部分木をすべて探索しないと「どちらが深いか」が分からない +
  • +
  • + CPython + のデフォルト再帰深度制限(約1000)があるため、一本道の木では単純な再帰が失敗する +
  • +
+
+ + +
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量(DFS)
+
+
+
O(w)
+
空間計算量(BFS)
+
+
+
+ 0〜10,000 +
+
ノード数
+
+
+ + +
+
+

+ 入力例 1 +

+
+    3          ← 深さ 1
+   / \
+  9  20        ← 深さ 2
+    /  \
+   15   7      ← 深さ 3 (葉)
+

出力: 3

+

+ 深さ3まで葉ノードが存在するため、最大深さ=3 +

+
+
+

+ 入力例 2 +

+
+  1            ← 深さ 1
+   \
+    2          ← 深さ 2 (葉)
+

出力: 2

+

+ 右の子だけが存在し、葉は深さ2にあるため、最大深さ=2 +

+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 入力例1(root=[3,9,20,null,null,15,7])を使って、BFS反復版の動きを追います。 +

+
+
+ + +
+

+ Python 実装 +

+ +

+ 2つの実装パターンを提供します。 + 業務開発版(BFS)は再帰深度制限を回避するため本番環境に適しており、 + 競技プログラミング版(再帰DFS)は最もシンプルでコードが短い実装です。 +

+ + +
+

+ 📋 業務開発版(BFS)のコード構造 +

+
    +
  1. root が None(空の木)の場合はすぐ 0 を返す(エッジケース処理)
  2. +
  3. deque([root]) でキューを初期化し、depth = 0 を設定する
  4. +
  5. while queue: でキューが空になるまでループする
  6. +
  7. level_size = len(queue) で現在レベルのノード数を事前記録する
  8. +
  9. + level_size 回 popleft() を行い、左右の子が存在すればキューへ追加する +
  10. +
  11. 1レベル処理完了ごとに depth += 1 して最終的に depth を返す
  12. +
+
+ +
from __future__ import annotations
+from typing import Optional
+from collections import deque
+
+# ════════════════════════════════════════════════
+# 業務開発版:反復 BFS(CPython 再帰制限を回避)
+# ════════════════════════════════════════════════
+class Solution:
+    def maxDepth(self, root: Optional[TreeNode]) -> int:
+        # エッジケース:空の木(ノードが1つもない)→ 深さ 0
+        # 後続の deque 処理に None を入れないための早期リターン
+        if root is None:
+            return 0
+
+        # deque を使う理由:
+        #   list.pop(0) は先頭削除が O(n) だが
+        #   deque.popleft() は O(1) で済む
+        queue: deque[TreeNode] = deque([root])
+        depth: int = 0  # 処理したレベルの数 = 深さ
+
+        while queue:
+            # この時点の len(queue) = 今のレベルのノード数
+            # ループ前に固定することで「次のレベルのノードが
+            # append されても影響を受けない」ようにする
+            level_size: int = len(queue)
+
+            for _ in range(level_size):
+                node: TreeNode = queue.popleft()  # O(1)
+
+                # 左・右の子が存在すれば次のレベルとしてキューへ
+                if node.left is not None:
+                    queue.append(node.left)
+                if node.right is not None:
+                    queue.append(node.right)
+
+            # 今のレベルを全部処理し終えた = 1段降りた
+            depth += 1
+
+        return depth
+
+
+# ════════════════════════════════════════════════
+# 競技プログラミング版:再帰 DFS(最もシンプル)
+# ════════════════════════════════════════════════
+class Solution2:
+    def maxDepth(self, root: Optional[TreeNode]) -> int:
+        # ベースケース:None = 存在しないノードの深さは 0
+        # "is None" を使う理由:
+        #   通常のノードオブジェクトはvalの値にかかわらずtruthyですが、
+        #   ノードが存在しない(None)ことを真偽値ではなく明示的に判定するためです。
+        if root is None:
+            return 0
+
+        # max() は C実装の組み込み関数なので if文より高速
+        # 「左の深さ」と「右の深さ」の大きい方 + 現在ノード分(+1)
+        return 1 + max(
+            self.maxDepth(root.left),
+            self.maxDepth(root.right),
+        )
+ + +
+

+ ▶ 入力例1 [3,9,20,null,null,15,7] での動作トレース(BFS版) +

+
+初期状態: queue=deque([Node(3)]), depth=0
+
+【レベル1】level_size=1
+  popleft() → Node(3)
+    left=Node(9)   → append → queue=[Node(9)]
+    right=Node(20) → append → queue=[Node(9),Node(20)]
+  depth=1
+
+【レベル2】level_size=2
+  popleft() → Node(9)
+    left=None, right=None → 追加なし
+  popleft() → Node(20)
+    left=Node(15) → append → queue=[Node(15)]
+    right=Node(7) → append → queue=[Node(15),Node(7)]
+  depth=2
+
+【レベル3】level_size=2
+  popleft() → Node(15) → 子なし
+  popleft() → Node(7)  → 子なし
+  depth=3
+
+queue=deque([]) → 空 → ループ終了
+return 3 ✅
+
+
+ + +
+

+ 処理フローチャート +

+ + +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい + 赤=いいえ + 紫=ループ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + 開始: maxDepth(root) + + + + + + + + + root is None ? + + + (空の木チェック) + + + + + + はい + + + + return 0 + + + + + + いいえ + + + + + + queue = deque([root]) + + + depth = 0 + + + + + + + + + queue が空? + + + (while ループ判定) + + + + + + はい + + + + return depth + + + + + + いいえ + + + + + + level_size = len(queue) + + + + + + + + + level_size 回: node = queue.popleft() + + + 左右の子が存在すれば queue.append() + + + + + + + + + depth += 1 + + + + + + ループ継続 + + +
+ + +
+

+ 🔎 入力例1 [3,9,20,null,null,15,7] でのフロー追跡 +

+
    +
  1. 「開始」→ root=Node(3) を受け取る
  2. +
  3. + 「root is None?」→ Node(3) は None ではない → + いいえ の経路へ +
  4. +
  5. 「キュー初期化」→ queue=deque([Node(3)]), depth=0
  6. +
  7. + 「queue が空?」→ [Node(3)] は空でない → + いいえ +
  8. +
  9. + level_size=1 → Node(3) をpopleft → 子 Node(9),Node(20) をappend → + depth=1 +
  10. +
  11. + 「queue が空?」→ [Node(9),Node(20)] は空でない → level_size=2 → 処理 → + depth=2 +
  12. +
  13. + 「queue が空?」→ [Node(15),Node(7)] は空でない → level_size=2 → 処理 → + depth=3 +
  14. +
  15. + 「queue が空?」→ [] は空 → + はい → 「return depth」→ + 3 を返す ✅ +
  16. +
+
+
+ + +
+

+ 計算量分析 +

+ + +
+

+ 📖 Big-O 記法の読み方(n が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(log n)
+
+ 半分ずつ絞る
例:二分探索 +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ 実装 + + 時間計算量 + + 空間計算量 + + 空間の詳細 +
+ 業務版(BFS) + + O(n) + + O(w) + + w = 木の最大幅。完全二分木では最下段≈n/2 +
+ 競技版(再帰DFS) + + O(n) + + O(h) + + h = 木の高さ。平衡木でO(log n)、一本道でO(n) +
+
+ + +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 時間計算量 O(n):「木の最大深さ」を求めるには、全ノードを少なくとも1回は訪問しなければなりません(どの葉が一番深いか分からないため)。BFS版・DFS版どちらも全n個のノードを正確に1回ずつ処理するので + O(n) です。
+ 空間計算量(BFS)O(w):キューには「現在のレベルのノード」と「次のレベルのノード」が同時に入ります。同じ深さのノード数の最大値 + w が最大キューサイズになります。
+ 空間計算量(DFS)O(h):再帰呼び出しのたびにコールスタックに1段積まれます。最大で「木の高さ + h」段まで積まれるため O(h) です。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。 +

+
+
+ + BFS(幅優先探索) + +
+ Breadth-First Search + の略。木やグラフを「同じ深さ(レベル)のノードを全部見てから次の深さへ」進む方式で探索する手法。階数が同じ場所を先に全部確認してから次の階へ進むエレベーターのイメージ。Pythonでは + collections.deque + を使って実装する。 +
+
+ +
+ + DFS(深さ優先探索) + +
+ Depth-First Search + の略。木の一本道を葉(末端)まで進み切ってから戻り、次の道を探す方式。迷路で「行き止まりまで進んでから引き返す」探索法に似ている。再帰(関数が自分自身を呼び出す仕組み)と相性が良い。 +
+
+ +
+ + O(n) + 記法(ビッグオー記法) + +
+ アルゴリズムの速さやメモリ使用量が「入力の大きさ n + が増えるにつれてどう増えるか」を表す記法。O(n) は「n + が2倍になると処理も約2倍になる」こと、O(1) は「n + に関わらず常に一定」を意味する。 +
+
+ +
+ + コールスタック + +
+ 関数が呼び出されるたびに「この関数に戻ってくる場所」の記録が積み上がる高速なメモリ領域。お皿の積み重ねのように、最後に置いたものが最初に取り出される(LIFO)。再帰が深くなりすぎるとこれが溢れて + RecursionError + が発生する。 +
+
+ +
+ + + collections.deque(両端キュー) + +
+ 前後どちらからも O(1) + で要素を追加・削除できるデータ構造。「両端開きの箱」のイメージ。list.pop(0)(先頭削除)は全要素をずらすため O(n) かかるが、deque.popleft() + は O(1) で済む。BFS の実装には必ず deque を使う。 +
+
+ +
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す手法。「問題を同じ形の小さな問題に分割して解く」のに最適。必ず「ベースケース(終了条件)」が必要で、ないと無限に呼び出しが続いてエラーになる。木の探索と非常に相性が良い。 +
+
+ +
+ + 二分木(Binary Tree) + +
+ 各ノード(節)が最大2つの子(左・右)を持つ木構造のデータ形式。家系図のように根(root)を頂点として下に枝分かれしていく。子を持たないノードを「葉(leaf)」と呼ぶ。 +
+
+ +
+ + Optional[T](型ヒント) + +
+ 「T 型またはNone」のどちらかであることを表すPythonの型ヒント。Optional[TreeNode] + と書くと、pylance(型チェッカー)が「None + かもしれない」ことを理解し、None チェックなしに + root.left + へアクセスすると警告してくれる。 +
+
+ +
+ + ベースケース(Base + Case) + +
+ 再帰の「終了条件」。これ以上小さく分割できない最小の状態を定義する。この問題では + root is None + がベースケースで、「存在しないノードの深さは0」を意味する。ベースケースがないと再帰が無限に続いてエラーになる。 +
+
+
+
+ + +
+ LeetCode 104 · Maximum Depth of Binary Tree · Python 解説ページ +
+
+ + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index 0881b39..aba3d4a 100644 --- a/public/index.html +++ b/public/index.html @@ -416,7 +416,7 @@

🧪 Algorithm Study Index

-

170 interactive lessons across 6 domains

+

171 interactive lessons across 6 domains

@@ -431,9 +431,9 @@

- + @@ -469,6 +469,7 @@

  • 🧩LeetCode 100 — Same Tree | 再帰DFS解説Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html
  • 🧩LeetCode 102 · Binary Tree Level Order TraversalAlgorithm/BinaryTree/claude sonnet 4.6 extended/102. Binary Tree Level Order Traversal/README_react.html
  • 🧩LeetCode 103 – Binary Tree Zigzag Level Order TraversalAlgorithm/BinaryTree/claude sonnet 4.6 extended/103. Binary Tree Zigzag Level Order Traversal/README_React.html
  • +
  • 🧩LeetCode 104 · Maximum Depth of Binary TreeAlgorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html
  • 🧩LeetCode 5: Longest Palindromic Substring - 中心展開法Algorithm/ExpandAroundCenter/leetcode/5. Longest Palindromic Substring/Claude/README.html
  • 🧩LeetCode 66: Plus One - 右から左への繰り上がり処理Algorithm/Other/leetcode/66. Plus One/Claude Sonnet 4.5/README_react.html
  • 🧩LeetCode 67: Add Binary - 二進数加算Algorithm/TwoPointers/leetcode/67. Add Binary/Claude/README_react.html
  • @@ -646,6 +647,7 @@

  • 🧩LeetCode 100 — Same Tree | 再帰DFS解説Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html
  • 🧩LeetCode 102 · Binary Tree Level Order TraversalAlgorithm/BinaryTree/claude sonnet 4.6 extended/102. Binary Tree Level Order Traversal/README_react.html
  • 🧩LeetCode 103 – Binary Tree Zigzag Level Order TraversalAlgorithm/BinaryTree/claude sonnet 4.6 extended/103. Binary Tree Zigzag Level Order Traversal/README_React.html
  • +
  • 🧩LeetCode 104 · Maximum Depth of Binary TreeAlgorithm/BinaryTree/claude sonnet 4.6 extended/104. Maximum Depth of Binary Tree/README_React.html
  • 🧩LeetCode 5: Longest Palindromic Substring - 中心展開法Algorithm/ExpandAroundCenter/leetcode/5. Longest Palindromic Substring/Claude/README.html
  • 🧩LeetCode 66: Plus One - 右から左への繰り上がり処理Algorithm/Other/leetcode/66. Plus One/Claude Sonnet 4.5/README_react.html
  • 🧩LeetCode 67: Add Binary - 二進数加算Algorithm/TwoPointers/leetcode/67. Add Binary/Claude/README_react.html
  • @@ -823,7 +825,7 @@

    🧪 - Generated on 2026-04-12 + Generated on 2026-04-13