diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_Rust.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_Rust.md new file mode 100644 index 0000000..16a77e1 --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_Rust.md @@ -0,0 +1,163 @@ +# Binary Tree Inorder Traversal — Rust Edition + +## 1. 問題の分析 + +### 競技プログラミング視点での分析 + +- 全ノードを一度だけ訪れる **O(N) 時間・O(N) 空間**が理論下界 +- LeetCode の Rust 環境では `TreeNode` が `Option>>` でラップされており、**所有権の移動なしに借用で読み取る** 設計が必須 +- 明示スタックによる反復実装でコールスタック消費を排除 + +### 業務開発視点での分析 + +- `Option>>` という複合型を安全に扱うため、`.borrow()` による共有参照と `Option` の `?`/`if let` による null 安全な展開が鍵 +- `Vec` への追記は `push` のみで副作用をローカルに限定 → Pure function に近い設計 +- `Result` は不要(入力が空 = 空ベクタを返すだけで、エラー状態がない) + +### Rust特有の考慮点 + +- `Rc>` の共有所有権モデル:`clone()` はポインタのコピーのみ(O(1)) +- `.borrow()` で `Ref` を取得 → 借用スコープを最小化してデッドロック回避 +- スタックの型を `Rc>` とすることで、非 null 要素のみを格納できる + +--- + +## 2. アルゴリズムアプローチ比較 + +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | +| --------------------------- | ---------- | ---------- | -------------- | ------ | ------ | --------------------------------------------------- | +| **A: 再帰 DFS** | O(N) | O(N) | 低 | 高 | 最高 | コールスタック深さN、深い木でスタックオーバーフロー | +| **B: 反復(明示スタック)** | O(N) | O(N) | 中 | 高 | 高 | Follow-up要件を満たす。`Rc::clone`でO(1)コピー | +| **C: Morris Traversal** | O(N) | O(1) | 非常に高 | 低 | 低 | `RefCell`の可変借用が複数箇所で必要、実装困難 | + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**: **B: 反復(明示スタック)** +- **理由**: + - Follow-upの反復解要件を満たす + - `Rc>` モデルで Morris は `borrow_mut()` の多重借用を誘発しやすく危険 + - `Rc::clone()` はポインタカウントのインクリメントのみで、ノード値コピーなし + - スタック `Vec>>` で型安全かつ非 null 要素のみ管理 + +- **Rust特有の最適化ポイント**: + - `Rc::clone(&node)` の明示的クローンでコスト意識を表現 + - `.borrow()` の借用スコープを `{}` ブロックで最小化(借用の早期解放) + - `Vec::with_capacity` でリアロケーション回数を削減(N≤100 なので省略可) + +--- + +## 4. 実装コード + +```rust +// Runtime 0 ms +// Beats 100.00% +// Memory 2.19MB +// Beats 74.02% + +// leetcode環境では use std::rc::Rc; use std::cell::RefCell; が提供済み + +// ---- メイン実装 ---- + +/// Binary Tree Inorder Traversal(反復・明示スタック実装) +/// +/// アルゴリズム: +/// 1. current カーソルを根から開始 +/// 2. current が Some の間、左端までスタックに積む(Rc::clone でポインタコピー) +/// 3. スタックから pop → 値を記録 → current を右子に移す +/// 4. current と stack が両方空になったら終了 +/// +/// # Arguments +/// * `root` - 二分木の根ノード(`None` = 空木) +/// +/// # Returns +/// 中順走査の値ベクタ(空木の場合は空ベクタ) +/// +/// # Complexity +/// - Time: O(N) — 全ノードを一度だけ訪問 +/// - Space: O(N) — 明示スタック最大深さ N(最悪: 左に偏った木) +pub fn inorder_traversal(root: Option>>) -> Vec { + // 結果バッファ(N ≤ 100 なのでデフォルト容量で十分) + let mut result: Vec = Vec::new(); + + // 明示スタック:非 null ノードのみ格納(型レベルで None 混入を排除) + let mut stack: Vec>> = Vec::new(); + + // カーソル:Option で「未訪問ノードあり」「なし」を型安全に表現 + let mut current: Option>> = root; + + // current(未訪問)とstack(保留)のいずれかが残る間ループ + while current.is_some() || !stack.is_empty() { + + // ---- フェーズ1: 左端まで潜りながらスタックに積む ---- + while let Some(node_rc) = current { + // Rc::clone はポインタカウントのインクリメントのみ(O(1)、コピーコストなし) + stack.push(Rc::clone(&node_rc)); + + // .borrow() スコープを最小化: 左子のクローンを取得したら即解放 + let left = node_rc.borrow().left.clone(); + current = left; // 左へ進む(None なら次のwhileを脱出) + } + + // ---- フェーズ2: スタック top を取り出して訪問 ---- + // stack.is_empty() でないことはループ条件で保証済み → unwrap 安全 + if let Some(node_rc) = stack.pop() { + // 借用スコープを {} で明示的に限定(右子取得前に解放) + let (val, right) = { + let node = node_rc.borrow(); // Ref + (node.val, node.right.clone()) + }; // ← ここで Ref が drop され、借用解放 + + // 中順で値を記録 + result.push(val); + + // ---- フェーズ3: 右部分木へカーソルを移す ---- + current = right; // None なら次ループで即 pop フェーズへ + } + } + + result +} +``` + +--- + +## 5. アルゴリズム動作トレース + +`root = [1, null, 2, 3]` を例に各フェーズを可視化します。 + +``` +ツリー構造: + 1 + \ + 2 + / + 3 +``` + +| ステップ | current | stack(底→top) | result | 操作 | +| -------- | ------------ | --------------- | ------- | --------------------------------- | +| 初期 | Some(1) | [] | [] | — | +| Ph1 | None | [1] | [] | 1 を push、left=None で停止 | +| Ph2 | — | [] | [1] | pop→1、val=1 を記録 | +| Ph3 | Some(2) | [] | [1] | current = right(2) | +| Ph1 | Some(3)→None | [2,3] | [1] | 2 push → 3 push、left=None で停止 | +| Ph2 | — | [2] | [1,3] | pop→3、val=3 を記録 | +| Ph3 | None | [2] | [1,3] | current = right(None) | +| Ph2 | — | [] | [1,3,2] | pop→2、val=2 を記録 | +| Ph3 | None | [] | [1,3,2] | ループ終了 | + +**Output: `[1, 3, 2]` ✅** + +--- + +## Rust固有の最適化観点まとめ + +| 観点 | 本実装での適用 | +| ----------------- | ---------------------------------------------------------------------- | +| **所有権管理** | `Rc::clone` でポインタ共有(ノード値コピーなし) | +| **借用の最小化** | `{ let node = node_rc.borrow(); ... }` で借用スコープを即解放 | +| **null 安全性** | `Option>>` + `while let` で None を型レベルで排除 | +| **Pure function** | 入力ツリーへの書き込みゼロ(`.borrow()` のみ、`.borrow_mut()` 不使用) | +| **パニック制御** | `unwrap()` を排除し `if let` / `while let` で安全展開 | diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_python.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_python.md new file mode 100644 index 0000000..0b63c62 --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_python.md @@ -0,0 +1,183 @@ +# Binary Tree Inorder Traversal — Python Edition + +## 1. 問題分析結果 + +### 競技プログラミング視点 + +- **制約分析**: N ≤ 100 と極小。O(N) 時間・O(N) 空間が理論下界 +- **最速手法**: 明示スタックによる反復実装。Python の関数呼び出しオーバーヘッドを回避 +- **CPython最適化**: `list.append()` は C 実装で O(1) 均償。`list.pop()` も同様 + +### 業務開発視点 + +- **型安全設計**: `Optional[TreeNode]`、`list[int]` で Pylance 完全対応 +- **エラーハンドリング**: 空木(`None`)はガード節で即 `[]` を返し、例外を発生させない +- **可読性**: フェーズをコメントで明示し、意図を self-documenting に + +### Python特有分析 + +- **データ構造選択**: スタックは `list`(`append`/`pop` ともに O(1) で deque 不要) +- **再帰 vs 反復**: Python のデフォルト再帰上限は 1,000。反復実装が本番安全 +- **`while` + `list.pop()`**: CPython の C レイヤーで動作し最速 + +--- + +## 2. 採用アルゴリズムと根拠 + +- **選択**: 反復(明示スタック)+ カーソルポインタ方式 +- **Python最適化戦略**: `list.append` / `list.pop` を直接呼び出し、属性ルックアップを最小化 +- **トレードオフ**: 再帰より数行多いが、スタックオーバーフロー耐性と Follow-up 要件を同時に満たす + +--- + +## 3. 実装パターン + +### 業務開発版(型安全・可読性重視) + +```python +# Runtime 0 ms +# Beats 100.00% +# Memory 19.22 MB +# Beats 85.80% + +# 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 + +from __future__ import annotations +from typing import Optional + + +class Solution: + """ + Binary Tree Inorder Traversal + 中順走査(左 → 根 → 右)を反復スタックで実装。 + Follow-up: 再帰を使わない反復解。 + """ + + def inorderTraversal(self, root: Optional[TreeNode]) -> list[int]: + """ + 二分木の中順走査を反復で実行する。 + + Args: + root: 二分木の根ノード(None = 空木) + + Returns: + 中順走査の値リスト。空木の場合は空リスト。 + + Complexity: + Time: O(N) — 全ノードを一度だけ訪問 + Space: O(N) — 明示スタックの最大深さ(最悪: 左偏木) + """ + # ガード: 空木は即座に空リストを返す + if root is None: + return [] + + result: list[int] = [] + stack: list[TreeNode] = [] + current: Optional[TreeNode] = root + + while current is not None or stack: + + # ── フェーズ1: 左端まで潜りながらスタックに積む ────────────── + while current is not None: + stack.append(current) # 右・自身は後回し + current = current.left # 左へ進む + + # ── フェーズ2: スタック top を取り出して訪問 ───────────────── + # ループ条件より stack が空でないことは保証済み + node: TreeNode = stack.pop() + result.append(node.val) # ← 中順で値を記録 + + # ── フェーズ3: 右部分木へカーソルを移す ────────────────────── + current = node.right # None なら次ループで即 pop フェーズへ + + return result +``` + +--- + +### 競技プログラミング版(性能最優先) + +```python +from typing import Optional + + +class Solution: + def inorderTraversal(self, root: Optional[TreeNode]) -> list[int]: + """ + 中順走査 反復実装(最速版) + + Time: O(N) + Space: O(N) + """ + res: list[int] = [] + stk: list[TreeNode] = [] + cur = root + + while cur or stk: + # 左端まで積む + while cur: + stk.append(cur) + cur = cur.left + # 訪問 → 右へ + cur = stk.pop() + res.append(cur.val) + cur = cur.right + + return res +``` + +--- + +## 4. アルゴリズムアプローチ比較 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| --------------------------- | ---------- | ---------- | ---------------- | ------ | ------------------ | ------------- | --------------------------------------------------- | +| **A: 再帰 DFS** | O(N) | O(N) | 低 | ★★★ | — | 不適 | 再帰上限 1,000 でスタックオーバーフローリスク | +| **B: 反復(明示スタック)** | O(N) | O(N) | 中 | ★★★ | `list` | 適 | Follow-up 要件を満たす。本実装 ✅ | +| **C: Morris Traversal** | O(N) | O(1) | 高 | ★☆☆ | — | 不適 | ノード書き換えで副作用あり、Python では実装コスト高 | + +--- + +## 5. 動作トレース + +`root = [1, null, 2, 3]` を例に。 + +``` +ツリー: + 1 + \ + 2 + / + 3 +``` + +| ステップ | current | stack(底→top) | result | 操作 | +| -------- | ------- | --------------- | --------- | --------------------------- | +| 初期 | 1 | [] | [] | — | +| Ph1 | None | [1] | [] | 1 を push、left=None で停止 | +| Ph2 | — | [] | [1] | pop→1、val=1 を記録 | +| Ph3 | 2 | [] | [1] | current = right(2) | +| Ph1 | 3→None | [2, 3] | [1] | 2 push → 3 push | +| Ph2 | — | [2] | [1, 3] | pop→3、val=3 を記録 | +| Ph3 | None | [2] | [1, 3] | current = right(None) | +| Ph2 | — | [] | [1, 3, 2] | pop→2、val=2 を記録 | +| Ph3 | None | [] | [1, 3, 2] | ループ終了 | + +**Output: `[1, 3, 2]` ✅** + +--- + +## Python固有の最適化観点まとめ + +| 観点 | 本実装での適用 | +| -------------------- | -------------------------------------------------------------- | +| **Pylance 型安全** | `Optional[TreeNode]`・`list[int]`・`list[TreeNode]` で完全対応 | +| **CPython 最速操作** | `list.append` / `list.pop` は C 実装 O(1) 均償 | +| **再帰回避** | 明示スタックでデフォルト再帰上限(1,000)の制約を完全回避 | +| **Pure function** | 入力ツリーへの書き込みゼロ、副作用なし | +| **エッジケース** | `root is None` ガードで空木を即返却、例外発生なし | diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_typescript.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_typescript.md new file mode 100644 index 0000000..7fd3249 --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/Binary_Tree_Inorder_Traversal_typescript.md @@ -0,0 +1,153 @@ +# Binary Tree Inorder Traversal + +## 1. 問題の分析 + +### 競技プログラミング視点での分析 + +- **中順走査(左→根→右)** の順でノード値を収集する古典的な木探索問題 +- ノード数 N ≤ 100 と制約が小さいため、漸近的複全体ノードを一度訪れる O(N) が理論下界 +- Follow-up:**反復(スタック)実装**が求められており、再帰によるコールスタック消費を回避できる + +### 業務開発視点での分析 + +- `TreeNode | null` という Union 型を正確に扱う null 安全性が重要 +- 再帰は可読性が高いが、深い木でスタックオーバーフローリスクあり +- イテレーティブ実装は明示的スタック管理により**実行時安全性**が高い + +### TypeScript特有の考慮点 + +- `TreeNode | null` の型ガードで null チェックを厳密に +- `readonly` 修飾子と `as const` でイミュータブルな中間状態を表現 +- スタックの型を `TreeNode[]` と明示し、型推論を最大活用 + +--- + +## 2. アルゴリズムアプローチ比較 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| --------------------------- | ---------- | ---------- | ------------ | -------- | ------ | ---------------------------- | +| **A: 再帰 DFS** | O(N) | O(N)※ | 低 | 高 | 最高 | ※コールスタック深さ N | +| **B: 反復(明示スタック)** | O(N) | O(N) | 中 | 高 | 高 | スタックオーバーフロー回避 | +| **C: Morris Traversal** | O(N) | O(1) | 高 | 中 | 低 | ポインタ書き換えで副作用あり | + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**: **B: 反復(明示スタック)** +- **理由**: + - Follow-upで反復解が明示的に要求されている + - コールスタックを消費しないため、深い木でも安全 + - `TreeNode[]` スタックで型安全に実装可能 + - Morris は入力ツリーを一時変更する副作用があり Pure function の原則に反する + +- **TypeScript特有の最適化ポイント**: + - `current: TreeNode | null` によるカーソル変数の明確な型定義 + - `stack: TreeNode[]` で非 null 要素のみ格納し、pop 後のアサーション不要化 + - `result: number[]` の型推論によりキャスト不要 + +--- + +## 4. 実装コード + +```typescript +// Runtime 0 ms +// Beats 100.00% +// Memory 55.30 MB +// Beats 63.69% + +/** + * Definition for a binary tree node. + * class TreeNode { + * val: number + * left: TreeNode | null + * right: TreeNode | null + * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + * } + */ + +/** + * Binary Tree Inorder Traversal(反復・明示スタック実装) + * + * アルゴリズム: + * 1. current ポインタを根から開始 + * 2. current が非 null の間、左端までスタックに積む + * 3. スタックから pop → 値を記録 → current を右子に移す + * 4. current と stack が両方空になったら終了 + * + * @param root - 二分木の根ノード(null = 空木) + * @returns 中順走査の値配列 + * @complexity Time: O(N), Space: O(N) N = ノード数 + */ +function inorderTraversal(root: TreeNode | null): number[] { + // 入力ガード:空木は即座に空配列を返す(型安全) + if (root === null) return []; + + const result: number[] = []; // 走査結果を蓄積 + const stack: TreeNode[] = []; // 明示スタック(非 null のみ格納) + let current: TreeNode | null = root; + + // current(未訪問ノード)とstack(保留ノード)のいずれかが残る間ループ + while (current !== null || stack.length > 0) { + // フェーズ1: 左端まで潜りながらスタックに積む + while (current !== null) { + stack.push(current); // 右・自身は後回し + current = current.left; // 左へ進む + } + + // フェーズ2: スタック top を取り出して訪問 + // stack.length > 0 保証済みなので non-null assertion は安全 + const node = stack.pop()!; // TreeNode 確定 + result.push(node.val); // ← 中順で値を記録 + + // フェーズ3: 右部分木へカーソルを移す(null なら次ループでpopへ) + current = node.right; + } + + return result; +} +``` + +--- + +## 5. アルゴリズム動作トレース + +`root = [1, null, 2, 3]` を例に各フェーズを可視化します。 + +``` +ツリー構造: + 1 + \ + 2 + / + 3 +``` + +| ステップ | current | stack(底→top) | result | 操作 | +| -------- | ------- | --------------- | ------- | ------------------------------- | +| 初期 | 1 | [] | [] | — | +| Ph1 | null | [1] | [] | 1 を push、左=null で停止 | +| Ph2 | — | [] | [1] | pop→1、val=1 を記録 | +| Ph3 | 2 | [] | [1] | current = 右(2) | +| Ph1 | 3 | [2,3] | [1] | 2 push → 3 push、左=null で停止 | +| Ph2 | — | [2] | [1,3] | pop→3、val=3 を記録 | +| Ph3 | null | [2] | [1,3] | current = 右(null) | +| Ph2 | — | [] | [1,3,2] | pop→2、val=2 を記録 | +| Ph3 | null | [] | [1,3,2] | ループ終了 | + +**Output: `[1, 3, 2]` ✅** + +--- + +## TypeScript固有の最適化観点まとめ + +| 観点 | 本実装での適用 | +| ---------------------- | -------------------------------------------------------------------------- | +| **null 安全性** | `TreeNode \| null` Union 型 + `!` アサーション(スタック長保証後のみ使用) | +| **型推論** | `result`, `stack` の型はイニシャライザから自動推論 | +| **イミュータブル操作** | `result.push` のみで元ツリー構造を一切変更しない Pure function | +| **コンパイル時安全性** | `stack: TreeNode[]` により pop 結果が `TreeNode \| undefined` と明確化 | diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README.md b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README.md new file mode 100644 index 0000000..12da5ca --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README.md @@ -0,0 +1,345 @@ +# Binary Tree Inorder Traversal - 反復スタックで中順走査を完全攻略 + +

目次

+ +- [Overview](#overview) +- [Algorithm](#algorithm) +- [Complexity](#complexity) +- [Implementation](#implementation) +- [Optimization](#optimization) + +--- + +

Overview

+ +**LeetCode 94 — Binary Tree Inorder Traversal** + +二分木の根ノード `root` が与えられるとき、**中順走査(左 → 根 → 右)** の順でノードの値を収集し、リストとして返す。 + +| 項目 | 内容 | +| -------------- | ----------------------------------------------------------------------- | +| 入力 | `root: Optional[TreeNode]`(ノード数 0 ≤ N ≤ 100、値 −100 ≤ val ≤ 100) | +| 出力 | `list[int]`(中順走査の値列) | +| 想定データ構造 | `TreeNode`(`val / left / right`) | +| Follow-up | **再帰を使わない反復解**を実装せよ | + +### 要件まとめ + +- 全ノードをちょうど一度訪問し、中順(左→根→右)で値を記録する +- 空木(`root is None`)は空リスト `[]` を返す +- Python のデフォルト再帰上限(1,000)に依存しない反復実装を提供する + +### FAQ + +**Q1. なぜ再帰ではなく反復で実装するのか?** + +A. Follow-up で明示要求されているほか、Python のデフォルト再帰上限(1,000)を超える深さの木でスタックオーバーフローが発生するリスクがある。反復実装は `sys.setrecursionlimit()` 変更なしに任意深さの木を安全に処理できる。 + +**Q2. Morris Traversal(O(1) 空間)を選ばなかった理由は?** + +A. Morris Traversal はノードの `left` ポインタを一時的に書き換えるため副作用がある(Pure function ではない)。本実装は入力ツリーへの書き込みを一切行わないため、関数の呼び出し前後でツリー構造が変化しない。可読性・安全性の観点からも反復スタック実装が優れている。 + +**Q3. `stack.pop()` の結果を `Optional[TreeNode]` にしなくていいのか?** + +A. `while` ループの条件 `cur is not None or stack` により、Ph2 に到達する時点でスタックが空でないことが保証される。ただし型推論上は `stack.pop()` の戻り値が `TreeNode` と確定しないため、`node: TreeNode = stack.pop()` と明示的な型注釈を付与している。 + +**Q4. `list` と `deque` どちらをスタックとして使うべきか?** + +A. 末尾への `append` / `pop(-1)` のみの操作なら CPython では `list` の方が高速。`deque` は両端操作が O(1) であるメリットが活きるのは `appendleft` / `popleft` を使う場合(BFS のキューなど)。スタック用途では `list` で十分。 + +**Q5. `from __future__ import annotations` が必要な理由は?** + +A. 型ヒントの遅延評価(PEP 563)を有効化し、`Optional[TreeNode]` などの前方参照を文字列として扱うため。これにより、`TreeNode` クラスが `TYPE_CHECKING` ブロック内にある場合でも、実行時に評価が遅延されることでランタイムエラーを回避できる。 + +--- + +

Algorithm

+ +### アルゴリズム要点 TL;DR + +- **戦略**: 明示スタック+カーソルポインタによる反復中順走査 +- **データ構造**: `list[TreeNode]`(スタック)、`Optional[TreeNode]`(カーソル) +- **フェーズ**: + 1. **Ph1** — カーソルが非 `None` の間、左端までスタックに積む + 2. **Ph2** — スタックから `pop` → 値を記録 + 3. **Ph3** — カーソルを右子に移して Ph1 へ戻る +- **終了条件**: カーソルが `None` かつスタックが空 +- **時間計算量**: O(N) — 全ノードを一度だけ訪問 +- **空間計算量**: O(N) — 明示スタックの最大深さ(最悪:左偏木) +- **再帰なし**: コールスタックを消費しないため深い木でも安全 + +### 図解 + +#### フローチャート + +```mermaid +flowchart TD + Start[Start: root = TreeNode or None] --> Guard{root is None} + Guard -- Yes --> RetEmpty[Return empty list] + Guard -- No --> Init[Init result stack cur=root] + Init --> Loop{cur is not None or stack not empty} + Loop -- No --> RetResult[Return result] + Loop -- Yes --> Ph1{cur is not None} + Ph1 -- Yes --> Push[stack.append cur] + Push --> MoveLeft[cur = cur.left] + MoveLeft --> Ph1 + Ph1 -- No --> Pop[node = stack.pop] + Pop --> Record[result.append node.val] + Record --> MoveRight[cur = node.right] + MoveRight --> Loop +``` + +> **読み方**: 左端まで積む(Ph1)→ pop して記録(Ph2)→ 右へ移動(Ph3)の3フェーズを繰り返す。カーソルとスタックが共に空になった時点で終了。 + +#### データフロー図 + +```mermaid +graph LR + subgraph Input + A[root TreeNode or None] + end + subgraph Precheck + A --> B{None check} + B -- None --> C[Return empty list] + end + subgraph Core + B -- not None --> D[cursor = root] + D --> E[Ph1 push left spine] + E --> F[Ph2 pop and record val] + F --> G[Ph3 move to right child] + G --> E + end + subgraph Output + F --> H[result list int] + H --> I[Return result] + end +``` + +> **データの流れ**: `root` → Noneガード → カーソル初期化 → 3フェーズのループ → `result` リストとして返却。 + +#### 具体的なトレース例 + +`root = [1, null, 2, 3]`(ツリー構造は下記) + +``` + 1 + \ + 2 + / + 3 +``` + +| ステップ | cur | stack(底→top) | result | 操作 | +| -------- | ------ | --------------- | --------- | --------------------------- | +| 初期 | 1 | [] | [] | — | +| Ph1 | None | [1] | [] | 1 を push、left=None で停止 | +| Ph2 | — | [] | [1] | pop→1、val=1 を記録 | +| Ph3 | 2 | [] | [1] | cur = right(2) | +| Ph1 | 3→None | [2, 3] | [1] | 2 push → 3 push | +| Ph2 | — | [2] | [1, 3] | pop→3、val=3 を記録 | +| Ph3 | None | [2] | [1, 3] | cur = right(None) | +| Ph2 | — | [] | [1, 3, 2] | pop→2、val=2 を記録 | +| Ph3 | None | [] | [1, 3, 2] | ループ終了 | + +**Output: `[1, 3, 2]`** ✅ + +### 正しさのスケッチ + +#### ループ不変条件 + +> 「`result` には、まだスタックに入っていない・未訪問でもない全ノードの値が中順で格納されている」 + +各反復で以下が保たれる: + +1. **Ph1 完了後**: スタックには「現在の左端パスのノード」が底→根方向で積まれている +2. **Ph2 完了後**: `pop` したノードは左部分木を全て処理済み → 自身の値を記録するのが中順で正しい +3. **Ph3 完了後**: `cur = node.right` により、右部分木が次の未処理対象になる + +#### 網羅性 + +- **左部分木**: Ph1 のループで再帰的に処理 +- **根(自身)**: Ph2 の `pop` + `record` で処理 +- **右部分木**: Ph3 でカーソルを移動 → 次の Ph1 が処理 + +#### 基底条件(終了性) + +- 各反復で必ず `stack.pop()` が1回実行される(スタックサイズが単調減少) +- `cur` が `None` になりスタックも空になれば `while` ループが終了 +- ノード数 N が有限なので、最大 N 回の pop で必ず終了する + +--- + +

Complexity

+ +### 計算量 + +| 観点 | 値 | 理由 | +| -------------- | ---- | ------------------------------------------------------ | +| **時間計算量** | O(N) | 各ノードをスタックに push 1回・pop 1回の合計 2N 操作 | +| **空間計算量** | O(N) | スタックの最大深さ(最悪: N ノードが全て左に偏った木) | + +### アプローチ比較 + +| アプローチ | 時間 | 空間 | 可読性 | 安全性 | 備考 | +| --------------------------- | ---- | ---- | ------ | ------ | -------------------------------------------------- | +| **再帰 DFS** | O(N) | O(N) | ★★★ | △ | 再帰上限 1,000 でスタックオーバーフロー | +| **反復(明示スタック)** ✅ | O(N) | O(N) | ★★★ | ◎ | Follow-up 要件を満たす。本実装 | +| **Morris Traversal** | O(N) | O(1) | ★☆☆ | △ | ノードの `left` ポインタを一時書き換える副作用あり | + +> **選択理由**: Follow-up で反復解が明示要求されており、Morris は入力ツリーを一時変更する副作用があるため不採用。反復スタック実装が安全性・可読性・要件適合の三拍子を満たす最適解。 + +--- + +

Implementation

+ +### Python 実装 + +```python +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + # Pylance の型チェック用スタブ(実行時は LeetCode 環境の定義を使用) + 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: ... + +# LeetCode 実行時フォールバック(TreeNode が未定義の環境向け) +try: + TreeNode # type: ignore[used-before-def] +except NameError: + + class TreeNode: # type: ignore[no-redef] + """最小フォールバック定義(__slots__ でメモリ最小化)""" + + __slots__ = ("val", "left", "right") + + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + """ + LeetCode 94 - Binary Tree Inorder Traversal + + 中順走査(左→根→右)を明示スタックによる反復で実装。 + Follow-up: 再帰を使わない反復解。 + """ + + def inorderTraversal(self, root: Optional[TreeNode]) -> list[int]: + """ + 二分木の中順走査を反復スタックで実行する。 + + Args: + root: 二分木の根ノード(None = 空木) + + Returns: + 中順走査の値リスト。空木の場合は空リスト []。 + + Complexity: + Time: O(N) - 全ノードを一度だけ訪問(push/pop 各 N 回) + Space: O(N) - 明示スタックの最大深さ(最悪: 左偏木で N) + """ + # ── ガード: 空木は即座に空リストを返す ────────────────────────── + if root is None: + return [] + + result: list[int] = [] # 中順走査の結果を蓄積 + stack: list[TreeNode] = [] # 明示スタック(非 None のみ格納) + cur: Optional[TreeNode] = root # 現在注目しているノード + + # cur(未訪問)または stack(保留)のいずれかが残る間ループ + while cur is not None or stack: + + # ── Ph1: 左端まで潜りながらスタックに積む ─────────────────── + while cur is not None: + stack.append(cur) # 自身と右部分木は後回し + cur = cur.left # 左へ進む + + # ── Ph2: スタック top を取り出して訪問 ────────────────────── + # ループ条件より stack が空でないことは保証済み + node: TreeNode = stack.pop() + result.append(node.val) # ← 中順で値を記録(左を全処理した後) + + # ── Ph3: 右部分木へカーソルを移す ─────────────────────────── + cur = node.right # None なら次ループで即 Ph2 へ + + return result +``` + +### エッジケースと検証観点 + +| ケース | 入力 | 期待出力 | 本実装の挙動 | +| -------------------- | ------------------------ | --------------------- | ------------------------------------------------- | +| **空木** | `root = None` | `[]` | 冒頭ガードで即 `return []` | +| **単一ノード** | `root = [1]` | `[1]` | Ph1 で push、Ph2 で記録、Ph3 で `cur=None` → 終了 | +| **左偏木(深さ N)** | `[1,2,null,3,null,...]` | `[N,...,2,1]` | スタック深さ N まで積んでから順次 pop | +| **右偏木(深さ N)** | `[1,null,2,null,3]` | `[1,2,3]` | Ph1 は毎回 1 回のみ push(スタック深さ常に 1) | +| **完全二分木** | `[1,2,3,4,5,null,8,...]` | `[4,2,6,5,7,1,3,9,8]` | 全フェーズが均等に動作 | +| **負の値** | `root = [-100]` | `[-100]` | `node.val` をそのまま記録(符号処理なし) | +| **全ノード同値** | `[0,0,0,0,0]` | `[0,0,0,0,0]` | 値の重複は問題なし(インデックス管理不要) | + +#### 静的型チェック(Pylance 対応確認点) + +- `root: Optional[TreeNode]` — `None` との Union を明示 +- `stack: list[TreeNode]` — 非 None のみ格納を型で保証 +- `cur: Optional[TreeNode]` — カーソルの nullable を型で追跡 +- `node: TreeNode` — `stack.pop()` 後に型を明示し `node.val` / `node.right` の属性アクセスを安全化 + +--- + +

Optimization

+ +### CPython 最適化ポイント + +#### list.append / list.pop の C 実装活用 + +```python +# CPython の list は動的配列。append/pop(-1) は均償 O(1) で C レイヤーで動作 +stack.append(cur) # C 実装: オーバーヘッド最小 +node = stack.pop() # C 実装: pop(-1) はリアロケーション不要 +``` + +> `collections.deque` は両端 O(1) が売りだが、末尾のみの操作なら `list` の方が CPython では高速。 + +#### 属性アクセスのローカル変数キャッシュ + +```python +# N が大きい場合(本問題は N ≤ 100 なので省略可) +_append = result.append # 属性ルックアップを1回に削減 +_pop = stack.pop + +while cur is not None or stack: + while cur is not None: + stack.append(cur) + cur = cur.left + node = _pop() + _append(node.val) + cur = node.right +``` + +> N ≤ 100 の本問題では効果は誤差範囲だが、N が大きいユースケースへの応用時に有効。 + +#### 再帰上限の回避 + +```python +# Python デフォルト再帰上限: sys.getrecursionlimit() = 1000 +# 深さ N の木で再帰 DFS を使うと N > 1000 でクラッシュ +# → 本実装の反復スタックは sys.setrecursionlimit() 不要で安全 +``` diff --git a/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html new file mode 100644 index 0000000..05e581b --- /dev/null +++ b/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html @@ -0,0 +1,1573 @@ + + + + + + LeetCode 94 - Binary Tree Inorder Traversal + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+
+
+
+ 中順走査 +
+
左 → 根 → 右
+
+
+
O(N)
+
時間計算量
+
+
+
O(N)
+
空間計算量
+
+
+
+ 反復スタック +
+
再帰なし実装
+
+
+ +
+
+

📌 問題要約

+

+ 二分木の根ノード + root が与えられる。 + 中順走査(左→根→右) + でノードの値を収集し、リストとして返す。
+ Follow-up: + 再帰を使わない反復解を実装せよ。 +

+
+

+ Input: root = [1, null, 2, 3]
+ Output: [1, 3, 2] +

+
+
+
+

📏 制約

+
    +
  • 🔢 ノード数 N: 0 ≤ N ≤ 100
  • +
  • 🔢 値の範囲: −100 ≤ val ≤ 100
  • +
  • ⚠️ Python再帰上限: デフォルト 1,000(N>1000で危険)
  • +
  • ✅ 反復実装でスタックオーバーフロー回避
  • +
+
+
+
+ + +
+

+ ステップ解説 +

+
+
+ + +
+

+ 実装コード +

+ +
+ + + +
+ + +
+
from __future__ import annotations
+from typing import Optional
+
+
+# Definition for a binary tree node.
+class TreeNode:
+    def __init__(
+        self,
+        val: int = 0,
+        left: Optional["TreeNode"] = None,
+        right: Optional["TreeNode"] = None,
+    ) -> None:
+        self.val = val
+        self.left = left
+        self.right = right
+
+
+class Solution:
+    """
+    LeetCode 94 - Binary Tree Inorder Traversal
+    中順走査(左→根→右)を明示スタックによる反復で実装。
+
+    Time:  O(N) - 全ノードを一度だけ訪問
+    Space: O(N) - 明示スタックの最大深さ(最悪: 左偏木で N)
+    """
+
+    def inorderTraversal(self, root: Optional[TreeNode]) -> list[int]:
+        # ── ガード: 空木は即座に空リストを返す
+        if root is None:
+            return []
+
+        result: list[int] = []          # 中順走査の結果
+        stack: list[TreeNode] = []      # 明示スタック(非 None のみ格納)
+        cur: Optional[TreeNode] = root  # 現在注目しているノード
+
+        while cur is not None or stack:
+
+            # Ph1: 左端まで潜りながらスタックに積む
+            while cur is not None:
+                stack.append(cur)   # 右・自身は後回し
+                cur = cur.left      # 左へ進む
+
+            # Ph2: スタック top を取り出して訪問
+            node: TreeNode = stack.pop()
+            result.append(node.val)  # ← 中順で値を記録
+
+            # Ph3: 右部分木へカーソルを移す
+            cur = node.right  # None なら次ループで即 Ph2 へ
+
+        return result
+
+ + + + + + +
+ + +
+

+ 処理フローチャート +

+
+ + + + + + + + + + + + + + + + + + + + 開始 + + + + + + + + 初期化 + + + + result=[] stack=[] cur=root + + + + + + + + cur != None + + + または stack 非空? + + + + + + No + + + + + + + + Yes + + + + + + Ph1: cur != None? + + + (左端まで潜る) + + + + + + Yes + + + + + スタックに積む + + + + stack.append(cur) cur = cur.left + + + + + + + + 繰り返し + + + + + + + + No + + + + + + + + Ph2: スタックから取り出して訪問 + + + + node = stack.pop() + + + result.append(node.val) ← 中順で記録 + + + + + + + + Ph3: 右部分木へ移動 + + + + cur = node.right + + + + + + + + + ループ継続 + + + + + + + + + 結果を返す + + + + return result # list[int] + + + + + + + 終了 + + +
+ +
+

+ フローの説明:
+ 1. 初期化: result・stack・curを初期設定する。
+ 2. ループ条件: + curが非Noneまたはstackが空でない間、3フェーズを繰り返す。
+ 3. Ph1(緑): + curが非Noneの間、左端まで潜りながらスタックに積む(ループバック=紫矢印)。
+ 4. Ph2(青): スタックからpopし、val + を結果リストに中順で記録する。
+ 5. Ph3(紫): cur = node.right + に移動し、ループ条件へ戻る(紫矢印)。
+ 6. 終了(赤): curとstackが共に空になったら結果を返す。 +

+
+
+ + +
+

+ 計算量分析 +

+ +
+
+
O(N)
+
時間計算量
+

+ 全ノードをスタックに push 1回・pop 1回の合計 2N 操作。定数倍を無視すると + O(N)。 +

+
+
+
O(N)
+
空間計算量
+

+ 明示スタックの最大深さ。最悪ケースは N + ノードが全て左に偏った木(スタック深さ = N)。 +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ アプローチ + + 時間 + + 空間 + + 可読性 + + 安全性 +
+ ✅ 反復(明示スタック) + O(N)O(N)★★★ + ◎ +
+ 再帰 DFS + O(N)O(N)★★★ + △ ※ +
+ Morris Traversal + O(N)O(1)★☆☆ + △ 副作用 +
+

+ ※ Python デフォルト再帰上限 1,000 でスタックオーバーフローリスクあり +

+
+
+ + +
+ LeetCode 94 — Binary Tree Inorder Traversal | 反復スタック実装解説 +
+
+ + + + + + + + diff --git a/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README.md b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README.md new file mode 100644 index 0000000..1e85fb6 --- /dev/null +++ b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README.md @@ -0,0 +1,706 @@ +# Same Tree — 2つの二分木が同一かを再帰DFSで判定する + +--- + +## 目次(Table of Contents) + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython 最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 **初学者向け補足**:この問題を一言で言うと、「2本の木(ツリー)が、形も値もまったく同じかどうかを確認する問題」です。 + +### 問題の要約 + +LeetCode 100「Same Tree」は、2つの二分木(=各ノードが高々2つの子を持つ木構造)`p` と `q` を受け取り、その2つが**構造的にも値的にも完全に一致するか**を判定する問題です。 + +**なぜこの問題が面白いのか**:木構造は「自分の中に同じ形の小さな構造を持つ」という**再帰的な性質**を持っています。`p` と `q` が同じ木かどうかを調べるには、「根の値が同じか」を確認した後に「左の子木が同じか」と「右の子木が同じか」を再び同じ手順で確認すればよいのです。この「同じ問題を小さくして繰り返す」という構造こそが再帰の本質です。 + +### 入出力仕様 + +| 引数 | 型 | 説明 | +| ------ | -------------------- | ------------------------------------------ | +| `p` | `Optional[TreeNode]` | 比較元の木のルートノード(または `None`) | +| `q` | `Optional[TreeNode]` | 比較先の木のルートノード(または `None`) | +| 戻り値 | `bool` | 2つの木が同一なら `True`、異なれば `False` | + +### 制約 + +- 両方の木のノード数は `0` 以上 `100` 以下 +- `-10^4 <= Node.val <= 10^4` + +### 代表例 + +``` +例1: p = [1,2,3] q = [1,2,3] → True + 1 1 + / \ / \ + 2 3 2 3 + +例2: p = [1,2] q = [1,null,2] → False + 1 1 + / \ + 2 2 + +例3: p = [1,2,1] q = [1,1,2] → False + 1 1 + / \ / \ + 2 1 1 2 +``` + +> 📖 **この章で登場した用語** +> +> - **二分木(Binary Tree)**:各ノードが高々2つの子(左・右)を持つ木構造のこと +> - **ルートノード**:木の最上部にある「根」のノード。ここから全ての子ノードにたどり着ける +> - **再帰(Recursion)**:関数が自分自身を呼び出す仕組み。「木の比較」のように同じ構造が繰り返される問題に適している +> - **制約**:入力として与えられる値の範囲や条件のこと。制約から「どのくらいの計算量まで許容されるか」を逆算できる + +--- + +

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

+ +> 💡 **初学者向け補足**:TL;DR(Too Long; Didn't Read)とは「長くて読めない人向けの要約」という意味です。ここではアルゴリズム全体の戦略を箇条書きでまとめます。詳細は後の章で説明するので、**「なんとなくこういう手順で解くんだな」というイメージを掴む章**として位置づけてください。 + +- **手法**:再帰 DFS(深さ優先探索) + → 問題の定義「p と q が同じ ⟺ 根の値が同じ かつ 左の子木が同じ かつ 右の子木が同じ」がそのまま再帰のコードになるため、最もシンプルで直感的 + +- **データ構造**:追加のデータ構造は不要 + → 再帰コールスタック(関数呼び出しの積み重ね)のみを使用。`deque` や `list` は必要ない + +- **終了条件(基底条件)**:両方が `None` になった時点で `True` を返す + → 葉ノード(子を持たないノード)のさらに下は必ず `None` なので、ここが再帰の底になる + +- **早期終了**:片方だけ `None` または値が違う時点で `False` を即座に返す + → Python の `and` の短絡評価(=左辺が `False` なら右辺を評価しない仕組み)を活用し、不要な探索を省く + +- **時間計算量**:O(n)(n = 総ノード数) + → 全ノードを最大1回しか訪問しないため + +- **空間計算量**:O(h)(h = 木の高さ) + → 再帰の深さ = 木の高さ分だけコールスタックを消費。平均 O(log n)、最悪 O(n) + +> 📖 **この章で登場した用語** +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +> - **DFS(深さ優先探索)**:木やグラフを「根から葉まで深く潜ってから戻る」順番で探索する手法 +> - **基底条件**:再帰の終了条件。これがないと関数が無限に自分自身を呼び続けてしまう +> - **コールスタック**:関数呼び出しの履歴を積み上げるメモリ領域。再帰が深くなるほど消費量が増える +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない・`A or B` の A が `True` なら B を評価しない仕組み + +--- + +

図解

+ +> 💡 **初学者向け補足**:Mermaid フローチャートの基本的な読み方を説明します。 +> +> - **長方形(`[]`)**:処理のステップを表します +> - **ひし形(`{}`)**:条件分岐を表します。`Yes` か `No` かで矢印の向きが変わります +> - **矢印(`-->`)**:処理の流れを表します + +--- + +### フローチャート + +この図は `isSameTree(p, q)` 関数が1回の呼び出しでどのように処理を進めるかを表しています。上から下へ読み進めてください。再帰呼び出しは `Recurse` ノードが自分自身を再び呼び出すことを表します。 + +```mermaid +flowchart TD + Start[Start isSameTree p q] + BothNone{Both p and q are None} + RetTrue[Return True] + OnlyOne{Only one is None} + RetFalse1[Return False] + ValCheck{p.val equals q.val} + RetFalse2[Return False] + RecurLeft[Recurse on left children] + LeftOK{left result is True} + RetFalse3[Return False] + RecurRight[Recurse on right children] + RetRight[Return right result] + + Start --> BothNone + BothNone -- Yes --> RetTrue + BothNone -- No --> OnlyOne + OnlyOne -- Yes --> RetFalse1 + OnlyOne -- No --> ValCheck + ValCheck -- No --> RetFalse2 + ValCheck -- Yes --> RecurLeft + RecurLeft --> LeftOK + LeftOK -- No --> RetFalse3 + LeftOK -- Yes --> RecurRight + RecurRight --> RetRight +``` + +主要なノードの意味: + +- `Start`:関数の入り口。`p` と `q` を受け取る +- `BothNone`:両方が `None` かどうかを判定するひし形(条件分岐) +- `OnlyOne`:どちらか一方だけが `None` かを判定する(片方だけ None = 構造が違う) +- `ValCheck`:2つのノードの値 `p.val` と `q.val` が等しいかを判定する +- `RecurLeft / RecurRight`:左・右の子ノードに対して同じ処理を再帰的に繰り返す +- `RetTrue / RetFalse1〜3`:それぞれの条件で確定した結果を返す + +--- + +### データフロー図 + +この図は、入力の2本の木がどのような順序でノードを比較されていくかのデータの流れを表しています。 + +```mermaid +graph LR + subgraph Input + P[Tree p] + Q[Tree q] + end + subgraph Precheck + P --> CN{Compare nodes} + Q --> CN + CN --> BN{Both None} + CN --> ON{One is None} + CN --> VC{Val check} + end + subgraph Recurse + VC -- match --> RL[Left subtree] + VC -- mismatch --> RF[Return False] + RL -- True --> RR[Right subtree] + RL -- False --> RF + end + subgraph Output + RR --> Result[bool result] + BN -- Yes --> ResultT[True] + ON -- Yes --> RF + end +``` + +主要な流れの説明: + +- `Tree p → Compare nodes`:p と q のノードを同時に比較ステージへ送る +- `Both None → True`:両方が `None` の場合は即座に `True` +- `One is None → False`:片方だけ `None` の場合は即座に `False` +- `Val check → Left subtree → Right subtree`:値が一致したら左・右の子木を順に比較する + +--- + +> 💡 **代表例でのトレース**:`p=[1,2,3]` と `q=[1,2,3]`(例1)を入力として、フローチャートの各ノードを通過する様子を追います。 + +``` +Step 1: isSameTree(Node(1), Node(1)) + → BothNone? No(両方非 None) + → OnlyOne? No(どちらも非 None) + → ValCheck? 1 == 1 → Yes + → RecurLeft: isSameTree(Node(2), Node(2)) を呼び出す + +Step 2: isSameTree(Node(2), Node(2)) + → ValCheck? 2 == 2 → Yes + → RecurLeft: isSameTree(None, None) を呼び出す + +Step 3: isSameTree(None, None) + → BothNone? Yes → Return True ← 再帰の底(基底条件) + +Step 4: Step2 に戻る + → LeftOK = True → RecurRight: isSameTree(None, None) + → BothNone? Yes → Return True + +Step 5: Step2 の結果 = True + +Step 6: Step1 に戻る + → LeftOK = True → RecurRight: isSameTree(Node(3), Node(3)) + → 同様に True + +最終結果: True +``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理ステップ +> - **データフロー図**:データがどのように変換・移動するかを示す図 +> - **サブグラフ**:フローチャートの中で関連する処理をグループ化したもの +> - **基底条件**:再帰の終了条件。`Both None → Return True` がここに相当する + +--- + +

正しさのスケッチ

+ +> 💡 **初学者向け補足**:「正しさのスケッチ」とは、アルゴリズムが**常に正しい答えを返すことの根拠**を整理したものです。数学的な厳密な証明ではなく「なぜ正しいと言えるか」の説明です。 + +### 1. 基底条件(再帰の終わり) + +**条件**:`p is None and q is None` のとき `True` を返す +**根拠**:両方が `None` ということは「どちらにも子が存在しない」ことを意味します。構造的に同じ「葉の先端」に到達したため、`True` を返すのは正しい判断です。 +**具体例**:`p=[1]` と `q=[1]` の場合、ルートの比較後に `isSameTree(None, None)` が呼ばれ `True` が返ります。これは「どちらも子がない = 同じ構造」を正しく表しています。 + +### 2. 不変条件(処理中ずっと成り立つ条件) + +**条件**:「`isSameTree(p, q)` が `True` を返す ⟺ p を根とする部分木と q を根とする部分木が完全に一致する」 +**根拠**:この条件は再帰の各段階で維持されます。 + +- 根の値が一致し、左の子木が一致し、右の子木が一致する場合のみ `True` +- 一つでも不一致があれば `False` + +どの段階でも「この時点で確認できた範囲では一致している」という状態を保ちながら再帰が進むため、不変条件は常に成立しています。 + +### 3. 網羅性(すべてのケースを処理しているか) + +`(p, q)` の組み合わせは以下の4通りしかなく、すべてを処理しています: + +| p の状態 | q の状態 | 処理 | +| --------- | --------- | ------------------- | +| `None` | `None` | `True`(基底条件) | +| `None` | 非 `None` | `False`(構造違い) | +| 非 `None` | `None` | `False`(構造違い) | +| 非 `None` | 非 `None` | 値比較 → 再帰 | + +`if p is None and q is None` → `if p is None or q is None` の順で判定することで、2番目と3番目のケースをまとめて処理しています(1番目でリターン済みなので、`or` の条件に来た時点で必ず「どちらか一方だけ `None`」です)。 + +### 4. 終了性(有限ステップで終わるか) + +各再帰呼び出しでは必ず「子ノード」に向かって進みます。木のノード数は有限(制約より最大100)なので、必ず `None` に到達して再帰が終了します。無限ループは発生しません。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **基底条件**:再帰の終了条件。これがないと関数が無限に自分自身を呼び続けてしまう +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 +> - **部分木(サブツリー)**:ある木の中の特定のノードを根として切り出した、より小さな木 + +--- + +

計算量

+ +> 💡 **初学者向け補足**:計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| ---------- | ---------------------- | ------------------------------ | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(log n)` | 入力の桁数に比例 | 辞書を二分探索で引く | +| `O(h)` | 木の高さに比例 | 木の根から葉まで降りていく深さ | + +--- + +### 時間計算量:O(n) + +- n = 2つの木の総ノード数 +- すべてのノードを**最大1回**しか訪問しない +- 各ノードでの処理(`None` チェック・値比較)は O(1) のため、全体で O(n) +- **早期終了**が発生する場合は O(n) より少ないステップで終わる + +### 空間計算量:O(h) + +- h = 木の高さ(再帰コールスタックの深さ) +- 再帰呼び出しが積み重なるコールスタックが唯一の追加メモリ + +| 木の形 | 高さ h | 空間計算量 | +| ------------------------------ | -------- | -------------- | +| 平衡二分木(バランスが良い木) | log₂ n | O(log n) | +| 一本道(最悪ケース) | n | O(n) | +| 本問題(ノード数 ≤ 100) | 最大 100 | 実用上問題なし | + +### 他のアプローチとの比較 + +| アプローチ | 時間計算量 | 空間計算量 | 備考 | +| -------------------- | ---------- | ---------- | -------------------------------------- | +| **再帰 DFS**(採用) | O(n) | O(h) | コードが最もシンプル | +| 反復 DFS(スタック) | O(n) | O(h) | `list` をスタック代わりに使用 | +| 反復 BFS(キュー) | O(n) | O(n) | 常に O(n) メモリを消費、`deque` が必要 | + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **平衡二分木**:左右の子木の高さの差が小さい、バランスの取れた二分木。高さが O(log n) になる +> - **早期終了**:不一致が見つかった時点で以降の処理を打ち切ること。最悪ケースの計算量は変わらないが、平均的には高速になる + +--- + +

Python 実装

+ +> 💡 **初学者向け補足**:コードを読む前に、実装の全体的な骨格を確認しましょう。 +> +> 1. `Optional["TreeNode"]` 型ヒントで「TreeNode か None のどちらか」を表現する +> 2. `if p is None and q is None:` で基底条件(両方 None)を最初に処理する +> 3. `if p is None or q is None:` で「片方だけ None」の構造違いを処理する +> 4. `p.val != q.val` で値の違いを確認する +> 5. `isSameTree(p.left, q.left) and isSameTree(p.right, q.right)` で左右の子木を再帰的に比較する + +--- + +### 関数シグネチャ(LeetCode 形式) + +```python +class Solution(object): + def isSameTree(self, p, q): + # :type p: Optional[TreeNode] + # :type q: Optional[TreeNode] + # :rtype: bool +``` + +--- + +### 完全な実装コード + +```python +from __future__ import annotations + +# TYPE_CHECKING は「型チェック時(pylance 動作時)のみ True になるフラグ」。 +# 実行時には False になるため、TreeNode の定義がなくてもエラーにならない。 +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + # pylance が TreeNode の型情報を正しく解析するためのスタブ定義。 + # LeetCode の実行環境では TreeNode はすでに定義済みなので、 + # ここでは型チェック専用として宣言している。 + 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: + def isSameTree( + self, + p: Optional["TreeNode"], + q: Optional["TreeNode"], + ) -> bool: + """ + 2つの二分木が構造・値ともに完全に一致するかを再帰DFSで判定する。 + + 「p と q が同じ木」は以下の3条件すべてが成立するときに限る: + 1. p.val == q.val (根の値が同じ) + 2. isSameTree(p.left, q.left) が True (左の子木が同じ) + 3. isSameTree(p.right, q.right) が True (右の子木が同じ) + この定義を再帰でそのままコードにしている。 + + Time: O(n) n = 総ノード数 + Space: O(h) h = 木の高さ(コールスタックの深さ) + """ + + # ── ① 両方 None のとき ────────────────────────────────── + # 葉ノードのさらに下(= 何もない場所)に到達した。 + # 「両方に何もない」= 構造が一致しているので True を返す。 + # `is None` を使うのが Pythonic(Pythonらしい書き方)。 + # `== None` は非推奨で pylance も警告を出す。 + if p is None and q is None: + return True + + # ── ② 片方だけ None のとき ────────────────────────────── + # ①で「両方 None」は return 済みなので、ここに来るのは + # 「どちらか一方だけ None」の場合のみ。 + # 片方にノードがあり、片方にない = 構造が違う → False。 + if p is None or q is None: + return False + + # ── ③ 値の比較 ─────────────────────────────────────────── + # ①②を通過した時点で p も q も None ではないことが確定。 + # pylance もここでは p・q を TreeNode として認識する + # (型の絞り込み = Type Narrowing と呼ばれる仕組み)。 + # 値が違えば木の内容が異なるので False を返す。 + if p.val != q.val: + return False + + # ── ④ 左右の子木を再帰で比較 ──────────────────────────── + # 根の値が一致したので、次は左の子木・右の子木を再帰で確認する。 + # `and` の短絡評価により、左が False なら右の再帰は実行されない。 + # → 不一致が見つかった時点で即座に False を返せる(無駄な探索を省く)。 + return ( + self.isSameTree(p.left, q.left) + and self.isSameTree(p.right, q.right) + ) +``` + +--- + +> 💡 **コードの動作トレース**(例2: `p=[1,2]`、`q=[1,null,2]`) +> +> ``` +> 呼び出し①: isSameTree(p=Node(val=1, left=Node(2), right=None), +> q=Node(val=1, left=None, right=Node(2))) +> → ① p,q ともに非 None → パス +> → ② どちらも非 None → パス +> → ③ p.val=1, q.val=1 → 1 == 1 → パス +> → ④ 左の子木を比較するために再帰へ +> +> 呼び出し②: isSameTree(p=Node(val=2), q=None) +> → ① p は非 None → パス +> → ② p は非 None だが q は None → return False ← ここで終了! +> +> 呼び出し①に戻る: +> → isSameTree(p.left, q.left) = False +> → and の短絡評価: False and ... → 右辺の再帰は実行されない +> → return False +> +> 全体の結果: False +> ``` + +--- + +> 📖 **この章で登場した用語** +> +> - **`Optional["TreeNode"]`**:`TreeNode` または `None` のどちらかであることを表す型ヒント。`"TreeNode"` と文字列にするのは「前方参照」と呼ばれ、クラス定義より前に型名を使うための書き方 +> - **型の絞り込み(Type Narrowing)**:`if p is None: return` の後では pylance が「p は None でない」と自動的に判断してくれる仕組み +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない仕組み。不要な再帰呼び出しを省ける +> - **`TYPE_CHECKING`**:`typing` モジュールに含まれるフラグ。型チェック時のみ `True` になり、実行時には `False` になる +> - **Pythonic**:Pythonらしい、慣用的な書き方のこと。`is None` は `== None` よりも意図が明確でPythonicとされる + +--- + +

CPython 最適化ポイント

+ +> 💡 **初学者向け補足**:この章では「同じ処理でもPythonの書き方によって速さが変わる理由」を説明します。本問題は制約がノード数 ≤ 100 と小さいため、最適化の効果は小さいですが、考え方として知っておくと他の問題でも役立ちます。 + +--- + +### ポイント① `is None` vs `== None` + +CPython の内部では `None` はシングルトン(=プログラム全体で1つしか存在しないオブジェクト)です。 + +```python +# 最適化前(非推奨) +if p == None: # __eq__ メソッドを呼び出すため、わずかに遅い + return True + +# 最適化後(推奨) +if p is None: # オブジェクトのIDを比較するだけ。最速かつ pylance 警告なし + return True + +# 理由:`is` はオブジェクトのメモリアドレスを直接比較する。 +# `==` はオブジェクトの __eq__ メソッドを呼び出すため、わずかにオーバーヘッドがある。 +# None に対しては `is` を使うのが Python の慣習。 +``` + +--- + +### ポイント② `and` の短絡評価を活用した早期終了 + +```python +# 最適化前:左の結果を変数に格納してから and で結合 +left_result = self.isSameTree(p.left, q.left) +right_result = self.isSameTree(p.right, q.right) +return left_result and right_result +# 問題点:left_result が False でも right_result の再帰が実行されてしまう + +# 最適化後:and の短絡評価に任せる +return ( + self.isSameTree(p.left, q.left) + and self.isSameTree(p.right, q.right) +) +# 理由:左の再帰が False を返した瞬間、右の再帰は実行されない。 +# 不一致が見つかった時点で探索を打ち切れる。 +``` + +--- + +### ポイント③ 早期 return によるネストの削減 + +```python +# 最適化前:深いネストで可読性が低い +def isSameTree(self, p, q): + if p is not None or q is not None: + if p is not None and q is not None: + if p.val == q.val: + return (self.isSameTree(p.left, q.left) + and self.isSameTree(p.right, q.right)) + return p is None and q is None + +# 最適化後:早期 return でネストを浅く保つ +def isSameTree(self, p, q): + if p is None and q is None: + return True # ← 確定した時点で即 return + if p is None or q is None: + return False # ← 確定した時点で即 return + if p.val != q.val: + return False # ← 確定した時点で即 return + return (self.isSameTree(p.left, q.left) + and self.isSameTree(p.right, q.right)) + +# 理由:早期 return によりインデントが深くならず、後続のコードで +# 「ここに来た時点でこの条件は成立している」という前提が明確になる。 +``` + +--- + +### ポイント④ 本問題でメモ化(`lru_cache`)が使えない理由 + +```python +# 使えない理由:TreeNode は mutable(変更可能)なオブジェクトのため、 +# ハッシュ値を持たない(hashable でない)。 +# lru_cache はハッシュ値でキャッシュを管理するため、 +# hashable でない引数を持つ関数には使えない。 +# +# もし仮に使えたとしても、この問題では同じ (p, q) の組み合わせが +# 再度呼ばれることはないため、メモ化の効果はゼロ。 +``` + +> 📖 **この章で登場した用語** +> +> - **シングルトン**:プログラム全体で1つしか存在しないオブジェクト。`None`、`True`、`False` が該当する +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない仕組み +> - **早期 return**:関数の先頭で確定したケースを先に返すことで、以降のコードをシンプルに保つテクニック +> - **`lru_cache`**:関数の結果をキャッシュするデコレータ。引数が同じなら計算をスキップしてキャッシュを返す +> - **hashable(ハッシュ可能)**:ハッシュ値(= 値を整数に変換したもの)を持つオブジェクトのこと。辞書のキーや `lru_cache` の引数に使える条件 + +--- + +

エッジケースと検証観点

+ +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・特殊な形」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。本問題で想定されるエッジケースをすべて確認しましょう。 + +| ケース | p | q | 期待出力 | なぜ重要か | +| ------------------ | --------------- | ------------ | -------- | --------------------------------------------------------------------------------- | +| 両方空 | `None` | `None` | `True` | 基底条件が正しく `True` を返すかの確認 | +| p だけ空 | `None` | `[1]` | `False` | 「片方だけ None」の処理が抜けていると誤って比較しようとして AttributeError が出る | +| q だけ空 | `[1]` | `None` | `False` | 上記と対称。`or` 条件で両方まとめて処理する | +| 1ノードのみ・同値 | `[1]` | `[1]` | `True` | 子なしの最小ケース。再帰が正しく終了するかの確認 | +| 1ノードのみ・異値 | `[1]` | `[2]` | `False` | 根の値比較だけで即 `False` になるかの確認 | +| 構造同一・値同一 | `[1,2,3]` | `[1,2,3]` | `True` | 通常の正常系 | +| 構造異なる(左右) | `[1,2]` | `[1,null,2]` | `False` | ノード数が同じでも位置が違うケース | +| 値だけ異なる | `[1,2,1]` | `[1,1,2]` | `False` | 構造は同一だが左右の値が反転しているケース | +| 最大深さ(一本道) | 100ノードの直列 | 同上 | `True` | 再帰深度 100 でスタックオーバーフローしないかの確認 | +| 負の値を含む | `[-10000]` | `[-10000]` | `True` | 制約の下限値(`-10^4`)が正しく比較されるかの確認 | + +### 特に注意すべきケース + +**「片方だけ None」の見落とし**が最もよくあるバグです。以下のような実装ミスに注意してください。 + +```python +# ❌ 誤った実装例:None チェックが不完全 +def isSameTree(self, p, q): + if p is None and q is None: + return True + # ここで p か q が None のまま p.val にアクセスすると + # AttributeError: 'NoneType' object has no attribute 'val + if p.val != q.val: # ← p が None のとき AttributeError が発生! + return False + return (self.isSameTree(p.left, q.left) + and self.isSameTree(p.right, q.right)) + +# ✅ 正しい実装:片方だけ None のケースを先に排除する +def isSameTree(self, p, q): + if p is None and q is None: + return True + if p is None or q is None: # ← これを追加することで安全になる + return False + if p.val != q.val: + return False + ... +``` + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空のツリー・ノード1つ・最大深さなど、境界的な条件の入力 +> - **境界値**:制約の上限・下限にあたる値。例:ノード数 0(= None)や最大 100 ノード +> - **AttributeError**:存在しない属性にアクセスしようとしたときのエラー。`None.val` のような操作で発生する +> - **スタックオーバーフロー**:再帰が深くなりすぎてコールスタックがメモリを使い果たすエラー。CPython は `RecursionError` を発生させる + +--- + +

FAQ

+ +> 💡 **初学者向け補足**:FAQは「初学者がつまずきやすいポイント」を想定した質問と回答です。「結論 → 理由 → 補足」の順で説明します。 + +--- + +**Q1. なぜ `if p is None or q is None` を `if p is None and q is None` の後に書くのか?** + +**結論**:順番を守らないと「両方 None」のケースも `False` を返してしまうからです。 + +**理由**:Python は `if` を上から順に評価します。`or` の条件を先に書くと、`p=None, q=None` のとき `p is None` が `True` になった瞬間に短絡評価が働き、`False` を返してしまいます。 + +**補足(具体例)**: + +```python +# ❌ 順番を間違えた例 +if p is None or q is None: # p=None, q=None → p is None が True → False を返す(誤り!) + return False +if p is None and q is None: # ここには到達しない + return True + +# ✅ 正しい順番 +if p is None and q is None: # まず「両方 None」を先に確認 + return True +if p is None or q is None: # 次に「片方だけ None」を確認 + return False +``` + +--- + +**Q2. なぜ BFS(幅優先探索)ではなく再帰 DFS(深さ優先探索)を使うのか?** + +**結論**:この問題の構造が再帰と完全に一致しているため、再帰が最もシンプルで可読性が高いからです。 + +**理由**:「2つの木が同じかどうか」の定義が「根の値が同じ かつ 左の子木が同じ かつ 右の子木が同じ」という再帰的な構造になっています。この定義をそのままコードにしたのが再帰DFSです。BFS を使うと `deque` の管理が必要になり、コードが複雑になります。 + +**補足**:計算量は BFS も再帰 DFS も O(n) で同じです。ノード数が最大 100 という制約下では速度差もありません。コードの明瞭さを優先した選択です。 + +--- + +**Q3. 再帰でスタックオーバーフローにならないのか?** + +**結論**:この問題の制約(ノード数 ≤ 100)では問題ありません。 + +**理由**:CPython のデフォルトの再帰深度制限は 1000 です。本問題では木の高さが最大 100 なので、再帰の深さは 100 を超えることがありません。 + +**補足**:もし制約が「ノード数 ≤ 10^5」のような大きな値であれば、反復DFS(`list` をスタックとして使う実装)に切り替えることを検討します。 + +```python +import sys +# 万が一のための確認(今回は不要だが、大きな問題では念のため) +print(sys.getrecursionlimit()) # デフォルト: 1000 +``` + +--- + +**Q4. `p.val` と `q.val` を比較する前に `None` チェックをしないと何が起こるか?** + +**結論**:`AttributeError` が発生してプログラムがクラッシュします。 + +**理由**:`None` は Python の組み込み型 `NoneType` のオブジェクトです。`NoneType` には `val` という属性が存在しないため、`None.val` にアクセスすると `AttributeError: 'NoneType' object has no attribute 'val'` というエラーが発生します。 + +**補足**:TypeScriptや Rust ではコンパイル時にこのような null アクセスを防いでくれますが、Python では実行時まで検出できません。だからこそ `None` チェックを先に行うことが重要です。pylance を使うと静的解析で警告を出してくれます。 + +--- + +**Q5. `return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)` の1行で書いても正しく動くか?** + +**結論**:正しく動きます。さらに `and` の短絡評価のおかげで、不必要な再帰呼び出しを省けます。 + +**理由**:`and` は左辺が `False` の場合に右辺を評価しません。つまり左の子木の比較で `False` が返った瞬間、右の子木の比較は実行されません。これは「不一致が見つかった時点で探索を打ち切る」という効果を自動的に実現しています。 + +**補足**: + +```python +# この1行は... +return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) + +# 以下と同じ意味(でも1行の方が簡潔) +left_same = self.isSameTree(p.left, q.left) +if not left_same: + return False # ← and の短絡評価がこれを自動でやってくれる +return self.isSameTree(p.right, q.right) +``` + +> 📖 **この章で登場した用語** +> +> - **FAQ**:Frequently Asked Questions の略。よくある質問と回答のこと +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない仕組み +> - **AttributeError**:存在しない属性にアクセスしようとしたときのエラー +> - **スタックオーバーフロー**:再帰が深くなりすぎてメモリを使い果たすエラー。CPython では `RecursionError` として発生する +> - **静的解析**:プログラムを実行せずにコードを読むだけでバグや型エラーを検出する手法。pylance はこれを行うツール diff --git a/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html new file mode 100644 index 0000000..c2d1c3e --- /dev/null +++ b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html @@ -0,0 +1,1997 @@ + + + + + + LeetCode 100 — Same Tree | 再帰DFS解説 + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「2本の二分木が、形も値もまったく同じかどうかを確認する問題」 +

+

+ 二分木(=各ノードが左と右に高々1つずつ子を持つ木構造)が2本与えられます。 + 「同じ木」とは、すべての対応するノードが同じ値を持ち、かつ木の形(どこに子がいるか)も完全に一致することです。 + 単純に見えますが、「木の形の比較」という点で少し考える必要があります。 +

+
+ +
+

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

+
    +
  • + null(何もない)の扱いが難しい:片方の木にはノードがあり、もう片方には何もない(null)という場合を正確に区別しなければならない +
  • +
  • + 全ノードを調べる必要がある:根(ルート)の値が同じでも、葉(末端)の値や位置が違えば「異なる木」になる。部分的な確認では不十分 +
  • +
+
+ +
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量
+
+
+
+ 再帰DFS +
+
アルゴリズム
+
+
+
Easy
+
難易度
+
+
+ + +
+
+

+ 例1 → true +

+
+p: [1,2,3]    q: [1,2,3]
+    1              1
+   / \            / \
+  2   3          2   3
+

形も値もすべて同じ → true ✅

+
+
+

+ 例2 → false +

+
+p: [1,2]      q: [1,null,2]
+    1              1
+   /                \
+  2                  2
+

+ 値は同じでも位置(左 vs 右)が違う → false ❌ +

+
+
+

+ 例3 → false +

+
+p: [1,2,1]    q: [1,1,2]
+    1              1
+   / \            / \
+  2   1          1   2
+

+ 左右の値が入れ替わっている → false ❌ +

+
+
+ +
+

📌 制約

+
    +
  • + 両方の木のノード数は + 0 以上 + 100 以下 +
  • +
  • + -10⁴ ≤ Node.val ≤ 10⁴ +
  • +
+
+
+ + +
+

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

+

+ 再帰DFSがどのように動くかを4つのステップで確認しましょう。 ▶ Play + ボタンで自動的に進めることもできます。 +

+
+
+ + +
+

+ Python 実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. + 両方が + None + かを確認 → 同じ葉の先端なら + True +
  2. +
  3. + 片方だけ + None + かを確認 → 構造が違うので + False +
  4. +
  5. + 両方の値(p.val != q.val)が違うなら + False +
  6. +
  7. + 左の子木・右の子木を再帰で比較し、両方一致なら + True +
  8. +
+
+ +
class Solution(object):
+    def isSameTree(self, p, q):
+        """
+        :type p: Optional[TreeNode]
+        :type q: Optional[TreeNode]
+        :rtype: bool
+        """
+        # ── ① 両方 None のとき ──────────────────────────
+        # 葉ノードのさらに下(何もない場所)に両方とも到達した。
+        # 「どちらにも子がない」=構造が一致している → True
+        # `is None` を使うのが Pythonic(Pythonらしい慣用的な書き方)
+        if p is None and q is None:
+            return True
+
+        # ── ② 片方だけ None のとき ──────────────────────
+        # ①で「両方 None」はすでに return 済み。
+        # ここに来るのは「どちらか一方だけ None」の場合のみ。
+        # 片方にノードがあり、片方にない = 構造が違う → False
+        if p is None or q is None:
+            return False
+
+        # ── ③ 値の比較 ───────────────────────────────────
+        # ①②を通過した時点で p も q も None でないことが確定。
+        # pylance もここでは p・q を TreeNode として認識する
+        # (型の絞り込み = Type Narrowing と呼ばれる仕組み)。
+        if p.val != q.val:
+            return False
+
+        # ── ④ 左右の子木を再帰で比較 ────────────────────
+        # 根の値が一致したので、次は左・右の子木を同じ手順で比較する。
+        # `and` の短絡評価(左が False なら右は実行しない)で
+        # 不一致が見つかった時点で即座に False を返せる。
+        return (
+            self.isSameTree(p.left, q.left)
+            and self.isSameTree(p.right, q.right)
+        )
+ +
+

+ ▶ 入力例 p=[1,2] / q=[1,null,2] での動作トレース +

+
+呼び出し①: isSameTree(Node(1), Node(1))
+  → ① 両方非 None → パス
+  → ② どちらも非 None → パス
+  → ③ 1 == 1 → パス(値が等しいので続ける)
+  → ④ 左の子を比較するために再帰呼び出し
+
+呼び出し②: isSameTree(Node(2), None)   ← p.left=Node(2), q.left=None
+  → ① p は非 None → パス(両方 None ではない)
+  → ② p は非 None だが q は None → return False ← ここで終了!
+
+呼び出し①に戻る:
+  → isSameTree(p.left, q.left) = False
+  → and の短絡評価:False and ... → 右辺の再帰は実行されない
+  → return False
+
+最終結果: False ✅
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

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

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい + 赤=いいえ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + 開始: isSameTree(p, q) + + + + + + + + + p と q は + + + どちらも None? + + + + + + はい + + + + + + True + + + + + + いいえ + + + + + + どちらか一方だけ + + + None? + + + + + + はい + + + + False + + + + + + いいえ + + + + + + p.val ≠ q.val + + + (値が違う)? + + + + + + はい + + + + False + + + + + + いいえ + + + + + + isSameTree(p.left, q.left) を再帰呼び出し + + + + + + + + + 左の結果は + + + True? + + + + + + いいえ + + + + False + + + + + + はい + + + + + + isSameTree(p.right, q.right) を再帰呼び出し + + + + + + + + + 右の結果は + + + True? + + + + + + いいえ + + + + False + + + + + + はい + + + + + + 終了: True を返す + + + + + + + + 再帰ループ(子ノードへ) + + +
+ +
+

+ 🔎 入力例 p=[1,2,3] / q=[1,2,3] でのフロー追跡 +

+
    +
  1. 「開始」ノード → isSameTree(Node(1), Node(1)) を受け取る
  2. +
  3. + 「どちらも None?」ノード → 両方非 None → + いいえ の経路へ +
  4. +
  5. + 「片方だけ None?」ノード → どちらも非 None → + いいえ の経路へ +
  6. +
  7. + 「p.val ≠ q.val?」ノード → 1 == 1 → + いいえ の経路へ(値が等しいので続ける) +
  8. +
  9. + 「左の子木を再帰比較」→ isSameTree(Node(2), Node(2)) + を再帰呼び出し(さらに深く潜る) +
  10. +
  11. 「左の結果 True?」→ 左の子木も一致 → はい の経路へ
  12. +
  13. 「右の子木を再帰比較」→ isSameTree(Node(3), Node(3)) を再帰呼び出し
  14. +
  15. 「右の結果 True?」→ 右の子木も一致 → はい の経路へ
  16. +
  17. 「終了」ノード → True を返す ✅
  18. +
+
+
+ + +
+

+ 計算量分析 +

+ +
+

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

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ n より少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
観点計算量条件
時間計算量O(n) + n = 2つの木の総ノード数。全ノードを最大1回訪問 +
空間計算量(平均)O(log n) + 平衡二分木の場合(高さ h ≈ log₂ n) +
空間計算量(最悪)O(n) + 一本道の木(高さ = ノード数)の場合 +
本問題での実際O(100) + ノード数 ≤ 100 の制約により事実上定数 +
+
+ +
+

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

+

+ 時間計算量 O(n):「2つの木が同じかどうか」を確認するには、すべてのノードを少なくとも1回は調べなければなりません。再帰DFSは各ノードをちょうど1回だけ訪問するため、n + ノードに対して O(n) の操作で済みます。
+ 空間計算量 O(h):再帰呼び出しはコールスタック(=関数呼び出しの積み重ね)にメモリを使います。一番深くまで潜ったとき(葉ノードに到達したとき)の積み重ねの深さが木の高さ + h なので、O(h) + のメモリが必要です。追加のデータ構造(リストやキューなど)は一切使わないため、スタック以外のメモリは + O(1) です。 +

+
+ + +
+

📊 アプローチ別比較

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間空間特徴
+ ✅ 再帰DFS(採用) + O(n)O(h) + コードが最もシンプル。問題の定義と1対1対応 +
+ 反復DFS(スタック) + O(n)O(h) + list をスタック代わりに使用。再帰を使わない +
+ 反復BFS(キュー) + O(n) + O(n) + + deque 使用。常に O(n) メモリを消費して不利 +
+
+
+
+ + +
+

+ 📖 用語集 +

+

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

+
+
+ + None(ナン) + +
+ Pythonで「何もない」を表す特別な値。他の言語の + null + に相当します。 二分木では、子がいないノードの + left や + right + が + None + になります。 比較する際は + == None + ではなく + is None + を使うのが Pythonic です。 +
+
+ +
+ + DFS(深さ優先探索 / + Depth-First Search) + +
+ 木やグラフを「根から葉まで深く潜ってから戻る」順番で探索する手法。 + 迷路を解くとき「行き止まりに当たるまでまっすぐ進み、行き止まりになったら戻って別の道を試す」のと同じ考え方です。 + 今回の問題では再帰関数が自動的に DFS の順序でノードを訪問します。 +
+
+ +
+ + コールスタック(Call + Stack) + +
+ 関数を呼び出すたびに「呼び出し情報」を積み上げていくメモリ領域。 + お皿の山積みに例えると、新しい関数呼び出しのたびにお皿を1枚重ね、関数が終了するとお皿を1枚取り除きます。 + 再帰が深くなるほどお皿が積み重なり、メモリを消費します。これが空間計算量 + O(h) の理由です。 +
+
+ +
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す仕組み。「木の比較」のように「同じ問題が小さいサイズで繰り返される」構造に特に適しています。 + 必ず「基底条件(これ以上深く行かない条件)」を設定しないと無限ループになるので注意が必要です。 + 今回の基底条件は + p is None and q is None + のときに + True + を返す部分です。 +
+
+ +
+ + + 短絡評価(Short-Circuit Evaluation) + +
+ A and B + の A が + False + なら B を評価しない・ + A or B + の A が + True + なら B を評価しない仕組み。 今回のコードでは + isSameTree(p.left, q.left) and isSameTree(p.right, q.right) + において、 + 左の子木が一致しなければ右の再帰は実行されません。不必要な処理を省いて効率化できます。 +
+
+ +
+ + 二分木(Binary + Tree) + +
+ 各ノード(節点)が高々2つの子(左の子・右の子)を持つ木構造のこと。 + 家系図に例えると、親が最大2人の子を持てる構造です。 今回の問題の + TreeNode + クラスはこの構造を + left と + right + の2つの参照で表現しています。 +
+
+ +
+ + 平衡二分木(Balanced + Binary Tree) + +
+ 左右の子木の高さの差が小さい、バランスの取れた二分木のこと。 n + 個のノードを持つ平衡二分木の高さは約 log₂ n になります。 例えば 1000 + ノードなら高さは約 10 です。 逆に「一本道」の木(チェーン状)は高さが n + になり、最悪ケースの空間計算量 O(n) に相当します。 +
+
+ +
+ + + Pythonic(パイソニック) + +
+ Pythonらしい、慣用的な書き方のこと。Pythonコミュニティが「この書き方が自然で読みやすい」と考えるスタイルを指します。 + 例えば + x == None + より + x is None、 + len(lst) == 0 + より + not lst + が Pythonic とされています。 +
+
+
+
+ + +
+

LeetCode 100 — Same Tree | 再帰DFS による O(n) 実装解説

+

Python 3 · 初学者向け解説ページ

+
+
+ + + + + + + + diff --git a/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_Python.md b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_Python.md new file mode 100644 index 0000000..78bf8b6 --- /dev/null +++ b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_Python.md @@ -0,0 +1,234 @@ +## 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題を一言で言うと「2本の二分木が、形も値もまったく同じかどうかをPythonの再帰で確認する問題」です。 +> +> **Pythonで解く際に特に気をつけるべきCPython特有の注意点**:LeetCodeが提供する `TreeNode` は通常のPythonクラスです。TypeScriptやRustのような複雑なラッパー型は不要で、`Optional[TreeNode]` という型ヒントで「ノードまたは `None`」を表現できます。Python の再帰はデフォルトで深さ1000までしか許可されていませんが(`sys.setrecursionlimit` で変更可)、本問題の制約はノード数100以下なので問題ありません。また `None` の比較は `is None` を使うのが Pythonic(=Pythonらしい書き方)です。 + +#### 競技プログラミング視点 + +- **制約分析**:ノード数 ≦ 100 → O(n) で十分。再帰深度も最大100なのでスタックオーバーフローの心配なし +- **最速手法**:再帰DFS(深さ優先探索)。Pythonの関数呼び出しオーバーヘッドはあるが、n≦100の制約では無視できる +- **メモリ最小化**:再帰スタックのみ使用。追加のデータ構造(`deque` や `list`)は不要 + +#### 業務開発視点 + +- **型安全設計**:`Optional[TreeNode]` を引数・戻り値に明示し、pylanceが `None` を渡した場合の型エラーを検出できるようにする +- **エラーハンドリング**:制約上 `val` は必ず `int` なので型エラーは現実的ではないが、`None` アクセスを `if` で防ぐことが重要 +- **可読性**:`if p is None and q is None` のように意図が明確な条件式を使う + +#### Python特有分析 + +- **データ構造選択**:追加のデータ構造不要。再帰コールスタックのみ +- **標準ライブラリ活用度**:今回は `typing.Optional` のみ使用。シンプルな問題ほどPython組み込みの強みが活きる +- **CPython最適化度**:再帰は Pure Python だが、n≦100 の制約では実用上問題なし + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速 +> - **`Optional[T]`**:「T または None のどちらか」を表す型ヒント。`Optional[TreeNode]` は「TreeNodeかNoneか」 +> - **Pythonic**:Pythonらしい、慣用的な書き方のこと。`is None` は `== None` より意図が明確でPythonicとされる +> - **再帰深度制限**:CPythonはデフォルトで関数の入れ子呼び出しを1000回までに制限している。`sys.getrecursionlimit()` で確認できる + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。「Python的にどの書き方が速いか・読みやすいか」という観点で比較します。C実装の組み込み関数を使えるかどうかも重要な判断基準です。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| -------------------- | ---------- | ---------- | ---------------- | ------ | ------------------------------- | ----------------------------- | -------------------------------- | +| **再帰DFS** | O(n) | O(h) | 低 | ★★★ | `typing` のみ | 不適(Pure Python再帰) | 問題構造と直接対応、最もシンプル | +| **反復DFS(stack)** | O(n) | O(h) | 中 | ★★☆ | `collections` 不要・`list` のみ | 適(list操作はC実装) | `append`/`pop` は O(1) | +| **反復BFS(deque)** | O(n) | O(n) | 中 | ★★☆ | `collections.deque` | 適(`deque.popleft` が O(1)) | 常に O(n) メモリを消費 | + +**選択理由**:再帰DFSを採用します。`list` の `pop()` はC実装で速いですが、今回は n≦100 の制約なので速度差は無意味です。「2つの木が同じ ⟺ 根の値が同じ かつ 左の子木が同じ かつ 右の子木が同じ」という問題の定義がそのまま再帰の構造に対応しており、コードの意図が最も明確に伝わるためです。 + +**Python最適化戦略**:`and` の短絡評価(=左辺が `False` なら右辺を評価しない仕組み)を活用して、不一致が見つかった時点で即座に `False` を返します。 + +> 📖 **このセクションで登場した用語** +> +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない仕組み。`False and 重い処理()` は重い処理を実行しない +> - **`deque.popleft()`**:`deque`(両端キュー)の先頭要素を O(1) で取り出す操作。`list.pop(0)` は O(n) なので大きなデータには不向き +> - **Pure Python**:C言語ではなくPythonで書かれたコード。CPythonの組み込み関数より遅い傾向がある + +--- + +## 3. 実装パターン + +> コードの骨格: +> +> 1. 両方が `None` なら → 同じ(`True`) +> 2. 片方だけ `None` なら → 違う(`False`) +> 3. 値が違うなら → 違う(`False`) +> 4. 値が同じなら → 左の子木・右の子木を再帰で比較 + +--- + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。型ヒントを丁寧に書き、pylanceが `None` の不正アクセスを実行前に検出できる構造にしています。コメントや docstring で意図を伝えることを優先します。 + +```python +from typing import Optional + + +class Solution: + def isSameTree( + self, + p: Optional["TreeNode"], + q: Optional["TreeNode"], + ) -> bool: + """ + 2つの二分木が構造・値ともに完全に一致するか判定する(業務開発版) + + Args: + p: 比較元の木のルートノード(または None) + q: 比較先の木のルートノード(または None) + + Returns: + 2つの木が同一なら True、異なれば False + + Complexity: + Time: O(n) n = 総ノード数(全ノードを最大1回訪問) + Space: O(h) h = 木の高さ(再帰コールスタックの深さ) + 平均 O(log n)、最悪(一本道の木)O(n) + """ + # ── ① 両方 None のとき ────────────────────────────────── + # 両方が None ということは「どちらにも子が存在しない」 + # = 葉ノードの先端に到達し、構造が一致している → True + # `is None` を使うのが Pythonic。`== None` は非推奨(pylance 警告あり) + if p is None and q is None: + return True + + # ── ② 片方だけ None のとき ────────────────────────────── + # ①で「両方 None」は処理済みなので、ここは「どちらか一方だけ None」の場合 + # 構造が異なる → False + if p is None or q is None: + return False + + # ── ③ 値の比較 ─────────────────────────────────────────── + # ここに到達した時点で p も q も None でないことが確定している。 + # pylance もここでは p・q を TreeNode として認識する(型の絞り込み)。 + # 値が違えば木の内容が異なる → False + if p.val != q.val: + return False + + # ── ④ 左右の子木を再帰で比較 ──────────────────────────── + # 「p と q が同じ木」 ⟺ + # 「根の値が同じ」かつ「左の子木が同じ」かつ「右の子木が同じ」 + # `and` の短絡評価により、左が False なら右の再帰は実行されない。 + # 不一致が見つかった時点で即座に False を返せるので効率的。 + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) +``` + +--- + +【競技プログラミング版を使う場面】 +LeetCodeなど制限時間内に正解を出すことが目的のコードに向きます。エラーハンドリングや丁寧なコメントを省き、1つの `return` 文で処理全体を表現します。 + +```python +from typing import Optional + + +class Solution: + def isSameTree( + self, + p: Optional["TreeNode"], + q: Optional["TreeNode"], + ) -> bool: + # 両方 None → True、片方だけ None → False を1行で処理。 + # `p and q` は p が None(= Falsy)なら False を返す短絡評価。 + # `not (p or q)` は「どちらも None」のとき True になる。 + if not p and not q: + return True + # 片方だけ None、または値が違う → False + if not p or not q or p.val != q.val: + return False + # 左右の子木を再帰で比較。and の短絡評価で早期終了。 + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) +``` + +--- + +> 💡 **コードの動作トレース**(Example 2: `p=[1,2]`, `q=[1,null,2]`) +> +> ``` +> 呼び出し①: isSameTree(p=Node(1), q=Node(1)) +> → ① p,q ともに非 None → パス +> → ② どちらも非 None → パス +> → ③ 1 == 1 → パス +> → ④ isSameTree(p.left, q.left) を呼び出す +> +> 呼び出し②: isSameTree(p=Node(2), q=None) +> → ① p は非 None → パス +> → ② q は None → return False ← ここで終了! +> +> 呼び出し①に戻る: +> → 左の子木の比較結果が False +> → and の短絡評価により、右の子木 (p.right, q.right) の再帰は実行されない +> → return False +> +> 全体の結果: False +> ``` + +--- + +> 📖 **このセクションで登場した用語** +> +> - **型の絞り込み(Type Narrowing)**:`if p is None: return` の後ではpylanceが「pはNoneではない」と自動的に判断してくれる仕組み +> - **Falsy**:`bool(x)` が `False` になる値。Pythonでは `None`・`0`・空リスト `[]`・空文字 `""` などが該当する +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない・`A or B` の A が `True` なら B を評価しない仕組み +> - **`Optional["TreeNode"]`**:文字列で型名を書く「前方参照」。クラス定義より前に型名を使いたいときに `"TreeNode"` と文字列にする + +--- + +## 4. 検証 + +> エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +| ケース | p | q | 期待出力 | 確認ポイント | +| ---------------- | --------- | ------------ | -------- | ------------------------- | +| 両方空 | `None` | `None` | `True` | ① の `None and None` 処理 | +| 片方空 | `[1]` | `None` | `False` | ② の片側 `None` 処理 | +| 構造同一・値同一 | `[1,2,3]` | `[1,2,3]` | `True` | 全ノード比較の正常系 | +| 構造異なる | `[1,2]` | `[1,null,2]` | `False` | 片方だけ子がある場合 | +| 値異なる | `[1,2,1]` | `[1,1,2]` | `False` | ③ の値比較(左右反転) | +| 1ノードのみ同値 | `[1]` | `[1]` | `True` | 葉ノード単体の比較 | +| 1ノードのみ異値 | `[1]` | `[2]` | `False` | 根だけで即 `False` | + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空の入力・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **正常系**:想定通りの入力で期待通りの出力が得られることを確認するテスト +> - **前方参照**:クラスや関数の定義より前に、その型名を文字列 `"ClassName"` で書くPythonの慣用的な書き方。Python 3.10以降は `from __future__ import annotations` で省略できる + +```python +class Solution(object): + def isSameTree(self, p, q): + """ + :type p: Optional[TreeNode] + :type q: Optional[TreeNode] + :rtype: bool + """ + # ── ① 両方 None のとき ────────────────────────────────── + # 葉ノードの先端に到達し、構造が一致している → True + # `is None` を使うのが Pythonic(`== None` より意図が明確) + if p is None and q is None: + return True + + # ── ② 片方だけ None のとき ────────────────────────────── + # ①で「両方 None」は処理済みなので、ここは「どちらか一方だけ None」 + # 構造が異なる → False + if p is None or q is None: + return False + + # ── ③ 値の比較 ─────────────────────────────────────────── + # 両方 None でないことが確定しているので .val に安全にアクセスできる + # 値が違えば木の内容が異なる → False + if p.val != q.val: + return False + + # ── ④ 左右の子木を再帰で比較 ──────────────────────────── + # `and` の短絡評価により、左が False なら右の再帰は実行されない + # 不一致が見つかった時点で即座に False を返せるので効率的 + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) +``` diff --git a/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_Rust.md b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_Rust.md new file mode 100644 index 0000000..511c4cc --- /dev/null +++ b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_Rust.md @@ -0,0 +1,227 @@ +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題を一言で言うと「2本の二分木が、形も値もまったく同じかどうかをRustの所有権モデルを壊さずに確認する問題」です。 +> +> **Rustで解く際に特に気をつけるべき点**:LeetCodeが提供する `TreeNode` は `Option>>` という複雑な型で表現されています。これは「値がある/ない」を `Option` で、「ヒープ上の共有所有権」を `Rc`(参照カウント=複数の変数が同じ値を共有する仕組み)で、「コンパイル時ではなく実行時の可変参照」を `RefCell` で実現しているためです。このネストした型を安全にたどる方法を設計することが、Rust実装の核心です。 + +**競技プログラミング視点での分析** + +全ノードを最低1回は訪問しないと「同じかどうか」を確定できないため、時間計算量の下限は O(n) です。再帰スタックは木の高さ h ぶん消費し、最悪(一本道の木)で O(n)、平均(バランス木)で O(log n) です。`Rc>` において、`RefCell::borrow()` は実行時に動的借用チェック(借用状態のトラッキング)を行うため小さなオーバーヘッドがあり、ルール違反時にはパニックする可能性があります(参照カウントの操作は `Rc::clone()` や `drop` が行います)。しかし、LeetCodeの制約(ノード数 ≦ 100)ではこの実行時コストは無視できます。 + +**業務開発視点での分析** + +`Option>>` 型は `borrow()` で参照を取り出す操作が必要です。`borrow()` は実行時に「すでに可変借用中でないか」を確認し、問題なければ `Ref` を返します。この `Ref` は `borrow()` の返値のライフタイム中のみ有効なため、一時変数に束縛しながら慎重に扱う必要があります。戻り値は `bool` で十分なシンプルな問題です。 + +**Rust特有の考慮点** + +- `Rc>` は `Clone` するとヒープ上の同じデータへの参照カウントが増えるだけで、深いコピーは発生しません。所有権を移さずに `.as_ref()` と `borrow()` で中身を読み取ります +- 再帰呼び出し時は `Option>>` を `clone()` して渡します(参照カウントのインクリメントのみ) +- `unsafe` は一切不要です + +📖 **このセクションで登場した用語** + +- **`Rc`(参照カウント)**:複数の変数が同じヒープ上の値を「共同所有」する仕組み。誰も使わなくなったら自動解放 +- **`RefCell`**:通常Rustは「借用ルールをコンパイル時に検査」するが、`RefCell` は「実行時に検査」する。木構造のような再帰的なデータ構造で必要になる +- **`borrow()`**:`RefCell` から `&T`(共有参照)を取り出すメソッド。実行時に借用ルールを確認する +- **ダングリングポインタ**:解放済みのメモリを指す参照。Rustの所有権システムはこれをコンパイル時に防ぐ + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。Rustでは「所有権の移動が何回起きるか」「ヒープアロケーションが必要か」も重要な判断基準です。 + +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | +| ----------------------- | ---------- | ---------- | -------------- | ------ | ------ | ------------------------------------------------------ | +| **再帰DFS** | O(n) | O(h) | 低 | 高 | 高 | 問題構造と直接対応、`clone()`は参照カウントのみ | +| **反復DFS(スタック)** | O(n) | O(h) | 中 | 高 | 中 | `Vec` をスタック代わりに使用、ヒープアロケーション発生 | +| **反復BFS(キュー)** | O(n) | O(n) | 中 | 高 | 中 | `VecDeque` 使用、常に O(n) メモリ消費 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(h)`:木の高さ h に比例。バランス木なら `O(log n)`、最悪ケースで `O(n)` +> - `O(n)`:全ノード数 n に比例した時間・メモリが必要 + +--- + +> 📖 **このセクションで登場した用語** +> +> - **`VecDeque`**:両端からの追加・取り出しが O(1) の双方向キュー。標準ライブラリに含まれる +> - **アロケーション**:ヒープ上にメモリを確保する操作。`Vec::new()` などが該当。頻繁に行うと速度が落ちる + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**:再帰DFS + +- **理由**: + - BFS を選ばなかった理由:`VecDeque` の確保で常に O(n) のヒープアロケーションが発生する。ノード数が最大100と小さい問題でもメモリ効率が悪い + - 反復DFS を選ばなかった理由:`Vec<(Option<...>, Option<...>)>` をスタックとして管理するコードが増え、可読性が下がる。`clone()` の呼び出し回数も増える + - 再帰DFS が優れる理由:「2つの木が同じ ⟺ 根の値が同じ かつ 左の子木が同じ かつ 右の子木が同じ」という問題の定義がそのまま再帰になる。コードと仕様が1対1で対応し、保守性が高い + +- **Rust特有の最適化ポイント**: + - `Rc>` の `clone()` は参照カウントをインクリメントするだけで O(1)。ディープコピーは発生しない + - `borrow()` で取り出した `Ref` を一時変数に束縛することで、ライフタイムを明確に管理できる + - 再帰の深さは最大100(制約より)なので、スタックオーバーフローの心配は不要 + +> 📖 **このセクションで登場した用語** +> +> - **ゼロコスト抽象化**:高レベルな書き方をしても、手書きの低レベルコードと同等の速さになるRustの特性 +> - **ライフタイム**:参照が有効でいられる期間。`borrow()` の返値 `Ref` は `borrow()` を呼んだ変数が生きている間だけ有効 + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:このコードの骨格は以下の通りです。 +> +> 1. `p` と `q` の両方が `None`(空)なら → 同じ(`true`) +> 2. 片方だけ `None` なら → 違う(`false`) +> 3. 両方に値があり、`borrow()` で中身を取り出して値を比較 +> 4. 値が同じなら → 左の子木・右の子木を再帰で比較 + +```rust +// LeetCode が提供する型定義(そのまま使用) +// use std::rc::Rc; +// use std::cell::RefCell; + +/// 2つの二分木が構造・値ともに完全に一致するか判定する +/// +/// # Arguments +/// * `p` - 比較元の木のルートノード(または None) +/// * `q` - 比較先の木のルートノード(または None) +/// +/// # Returns +/// 2つの木が同一なら `true`、異なれば `false` +/// +/// # Complexity +/// - Time: O(n) n = 総ノード数(全ノードを最大1回訪問) +/// - Space: O(h) h = 木の高さ(再帰コールスタックの深さ) +/// 平均 O(log n)、最悪 O(n) +impl Solution { + pub fn is_same_tree( + p: Option>>, + q: Option>>, + ) -> bool { + + // ── match で p と q の「ある/なし」を同時にパターンマッチ ── + // Rustには null がなく、「値がない」状態は Option の None で表す。 + // (Option, Option) のタプルを match することで、 + // 4通りの組み合わせを漏れなく・安全に処理できる。 + match (p, q) { + + // ① 両方 None → 構造的に同じ(葉ノードの先は常にここに到達) + (None, None) => true, + + // ② 片方だけ Some → 構造が違う → false + // (Some(_), None) と (None, Some(_)) を `|` でまとめて書ける。 + // `_` は「中身は使わないが、値があることは確認した」という意味。 + (Some(_), None) | (None, Some(_)) => false, + + // ③ 両方 Some → 中身を取り出して比較する + (Some(p_node), Some(q_node)) => { + + // .borrow() で RefCell の中の TreeNode への共有参照を取り出す。 + // これは実行時借用チェックを行う。今は他に可変借用がないため安全。 + // p_ref / q_ref は Ref 型で、このスコープ内でのみ有効。 + let p_ref = p_node.borrow(); + let q_ref = q_node.borrow(); + + // ── ③-a: 値の比較 ────────────────────────────────── + // 値が違えば即 false。以降の再帰を行う必要がない(短絡評価)。 + if p_ref.val != q_ref.val { + return false; + } + + // ── ③-b: 左の子木を再帰で比較 ───────────────────── + // p_ref.left は Option>> 型。 + // clone() は Rc の参照カウントをインクリメントするだけで O(1)。 + // ディープコピー(全ノードの複製)は発生しない。 + let left_same = + Self::is_same_tree(p_ref.left.clone(), q_ref.left.clone()); + + // 左が違えばここで false を返す(右の再帰は実行しない) + if !left_same { + return false; + } + + // ── ③-c: 右の子木を再帰で比較 ───────────────────── + // 左が同じだった場合のみ右を比較する。 + // && の短絡評価と同じ効果を、明示的な if で表現している。 + Self::is_same_tree(p_ref.right.clone(), q_ref.right.clone()) + } + } + } +} +``` + +--- + +> 💡 **コードの動作トレース**(Example 2: `p=[1,2]`, `q=[1,null,2]`) +> +> ``` +> 呼び出し①: is_same_tree(Some(Node{val:1,L:2,R:null}), Some(Node{val:1,L:null,R:2})) +> → match (Some, Some) → ③ へ +> → p_ref.val=1, q_ref.val=1 → 一致 +> → 左の子を比較するため再帰へ +> +> 呼び出し②: is_same_tree(Some(Node{val:2}), None) +> → match (Some(_), None) → ② へ +> → 即座に false を返す ← ここで終了! +> +> 呼び出し①に戻る: +> → left_same = false +> → if !left_same → return false +> → 右の子の再帰は実行されない(短絡終了) +> +> 全体の結果: false +> ``` + +--- + +> 💡 **`match` タプルパターンの読み方**(初学者向け) +> +> ``` +> match (p, q) { +> (None, None) => ... // 両方なし +> (Some(_), None) => ... // p だけあり +> (None, Some(_)) => ... // q だけあり +> (Some(a), Some(b)) => ... // 両方あり → a, b に束縛 +> } +> ``` +> +> これにより `null` チェックの書き忘れがコンパイル時に検出されます。 +> パターンが網羅的でないと Rust コンパイラがエラーを出してくれます。 + +--- + +> 📖 **このセクションで登場した用語** +> +> - **`Option`**:値がある(`Some(T)`)かない(`None`)かを型で表現する。他言語の `null` と違い、ある/なしの確認をコンパイラが強制してくれる +> - **パターンマッチ(`match`)**:値の形(パターン)によって処理を分岐させる構文。全ケースの網羅をコンパイラが保証する +> - **`borrow()`**:`RefCell` から共有参照 `Ref` を取り出す。実行時に他の可変借用がないか確認する +> - **`Rc::clone()`**:ヒープ上のデータをコピーせず、参照カウントだけを増やす O(1) の操作 +> - **短絡評価**:`A && B` の A が `false` なら B を評価しない仕組み。`if !left_same { return false; }` は同じ効果を明示的に書いたもの + +--- + +## Rust固有の最適化観点 + +### 所有権・借用・ライフタイムの活用 + +`Rc>` という型は、Rustの所有権ルール(「値の所有者は常に1人」)では木構造のような「複数の場所から参照されるデータ」を表現できないため使われています。`Rc` が「複数の共有所有者」を実現し、`RefCell` が「実行時の借用チェック」を提供します。`Rc>::borrow()` で取り出した `Ref` は `p_ref` などの変数に束縛されており、この変数のスコープを超えて生きることはありません。これによりダングリングポインタが原理的に発生しません。 + +### ゼロコスト抽象化 + +`match` による網羅的なパターンマッチは、コンパイル後は単純な分岐命令に変換されます。`Option` の `Some`/`None` もランタイムのオーバーヘッドはなく、ポインタの null チェックと同等のコードが生成されます。 + +### エラーハンドリング設計 + +本問題の戻り値は `bool` で十分なため `Result` は使用していません。`Rc>` に対して `borrow()` を呼ぶと、もし外部で同時に `borrow_mut()` を使って可変借用されていれば実行時パニックする可能性があります。しかし、本実装では `borrow_mut()` を一切呼ばず、シングルスレッド環境での参照のみを行うため、通常はパニックしません。そのため `.expect()` などの明示的なパニック制御も不要としています。 + +> 📖 **最終用語まとめ** +> +> - **`Ref`**:`RefCell::borrow()` が返す型。スコープを抜けると自動的に借用が解放される +> - **網羅性チェック**:`match` で全パターンを列挙しないとコンパイルエラーになるRustの仕組み。バグの原因となるケース漏れを防ぐ +> - **参照カウント**:`Rc` が内部で持つカウンター。`clone()` で +1、変数がドロップされると -1。0になったらメモリ解放 diff --git a/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_TypeScript.md b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_TypeScript.md new file mode 100644 index 0000000..0b199f8 --- /dev/null +++ b/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/Same_Tree_TypeScript.md @@ -0,0 +1,200 @@ +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題を一言で言うと、「2本の木(ツリー)が、形も値もまったく同じかどうかを確認する問題」です。木を上から下に向かってたどりながら、対応するノード(節点)が一致しているかをすべて確認します。 + +--- + +**競技プログラミング視点での分析** + +全ノードを最低1回は訪問しないと「同じかどうか」を確定できません。したがって最低でも O(n) の処理が必要で、これが理論上の下限です。メモリは再帰の深さ(木の高さ h)ぶんのスタックを消費します。最悪ケース(片方に偏った木)は O(n)、平均ケースは O(log n) です。 + +**業務開発視点での分析** + +`null` チェックが多発する構造なので、型システムで `TreeNode | null` を明示し、コンパイル時に未処理の `null` を排除することが重要です。再帰関数にすることで「同じ処理を左右の子に繰り返す」という意図が読み手に伝わりやすくなります。 + +**TypeScript特有の考慮点** + +LeetCode の定義済みクラス `TreeNode` をそのまま使います。戻り値型 `boolean` を明示することで、誤って数値や文字列を返すミスをコンパイル時に防げます。`null` との比較は `===` を使い、型ガードを活用します。 + +> 📖 **このセクションで登場した用語** +> +> - **ノード(節点)**:木構造の各要素。値と、子への参照を持つ +> - **型ガード**:「この値が特定の型かどうか」を実行時に確認し、TypeScriptに型を教える処理 +> - **コンパイル時**:TypeScriptのコードをJavaScriptに変換する段階。ここでエラーを検出できると実行時のバグを防げる + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使い具合(空間計算量)」を比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| --------------------------- | ---------- | ---------- | ------------ | -------- | ------ | ------------------------ | +| **再帰DFS**(深さ優先) | O(n) | O(h) | 低 | 高 | 高 | 問題構造と直接対応 | +| **反復BFS**(幅優先)キュー | O(n) | O(n) | 中 | 高 | 中 | キューの実装が必要 | +| **反復DFS** スタック | O(n) | O(h) | 中 | 高 | 中 | 明示的スタック管理が必要 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(n)`:ノード数 n に比例した時間・メモリが必要(線形) +> - `O(h)`:木の高さ h に比例したメモリが必要。バランスの良い木なら `O(log n)`、最悪ケースで `O(n)` + +--- + +> 📖 **このセクションで登場した用語** +> +> - **DFS(深さ優先探索)**:木の根から葉まで深く潜ってから戻る探索法。「縦に掘り進む」イメージ +> - **BFS(幅優先探索)**:同じ深さのノードを横方向にすべて調べてから次の深さへ進む探索法 +> - **スタック**:「最後に積んだものを最初に取り出す」構造(本の山積みをイメージ) + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**:再帰 DFS(深さ優先探索) + +- **理由**: + - BFS を選ばなかった理由:キューを配列で管理すると O(n) の追加メモリが常に必要。再帰DFSは木の高さぶんのスタックしか使わない + - 反復DFSを選ばなかった理由:スタックを明示的に管理するコードが増え、読みやすさが下がる + - 再帰DFSが優れる理由:「2つの木が同じ ⟺ 根の値が同じ かつ 左の部分木が同じ かつ 右の部分木が同じ」という問題の定義そのものが再帰になっており、コードと定義が1対1で対応している + +- **TypeScript特有の最適化ポイント**: + - 引数型 `TreeNode | null` を明示し、`null` を渡してもクラッシュしない + - 戻り値型 `boolean` を明示し、うっかり `undefined` を返すミスをコンパイル時に防ぐ + - 早期リターン(early return)パターンで `null` ケースを先に処理することで、以降のコードで `null` を心配せずに `.val` などにアクセスできる + +> 📖 **このセクションで登場した用語** +> +> - **早期リターン(early return)**:関数の先頭で例外ケースを先に返すことで、以降のコードをシンプルに保つテクニック +> - **部分木(サブツリー)**:ある木の中の特定ノードを根として切り出した、より小さな木 + +--- + +ここで図解を確認しましょう。再帰 DFS がどのように木を比較するかを視覚的に示します。 +まず、2本の木の構造と、再帰がどの順序でノードを訪問するかを示します。 + +```text +Tree p: Tree q: + 1 1 + / \ / \ + 2 3 2 3 +``` + +次に、「同じでない」ケース(Example 2: `p=[1,2]`, `q=[1,null,2]`)で再帰がどの時点で `false` を返すかを示します。 + +```text +Tree p: Tree q: + 1 1 + / \ + 2 2 + +呼び出し①: isSameTree(Node(1), Node(1)) + → 1 == 1 なので一致。 + → 左の子木を比較するため再帰へ。 + +呼び出し②: isSameTree(Node(2), null) + → p は値を持つが、q は null。 + → 構造が異なるため false を返す ← ここで終了! + +呼び出し①に戻る: + → 左の子木の比較結果が false。 + → 右の子木の比較は実行されず、全体として false を返す。 +``` + +## 4. 実装コード + +> 💡 **初学者向け補足**:このコードの骨格は以下の通りです。 +> +> 1. 両方が `null` なら → 同じ(`true`) +> 2. 片方だけ `null` なら → 違う(`false`) +> 3. 両方に値があり、値が違うなら → 違う(`false`) +> 4. 値が同じなら → 左の子木・右の子木を再帰的に比較 + +```typescript +/** + * 2つの二分木が同一かどうかを再帰DFSで判定する + * @param p - 比較元の木のノード(または null) + * @param q - 比較先の木のノード(または null) + * @returns 2つの木が構造・値ともに完全に一致する場合 true + * @complexity Time: O(n), Space: O(h) + * n = 総ノード数, h = 木の高さ(再帰スタックの深さ) + */ +function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean { + // ── ① 両方 null のとき ────────────────────────────── + // 両方が null ということは「どちらにも子が存在しない」 + // = 構造が同じ(null == null は true)なので true を返す + // ※ 葉ノードのさらに下は必ずここに到達する + if (p === null && q === null) return true; + + // ── ② 片方だけ null のとき ────────────────────────── + // p だけ null、または q だけ null → 構造が違う → false + // ここで両方 null のケースはすでに上で return 済みなので、 + // どちらか一方だけ null の場合のみこの条件に入る + if (p === null || q === null) return false; + + // ── ③ どちらも null でない → 値を比較 ────────────── + // ここに到達した時点で p も q も非 null であることが確定している。 + // TypeScript の型システムも、ここでは p・q を TreeNode として扱う。 + // 値が違うなら木の内容が異なる → false + if (p.val !== q.val) return false; + + // ── ④ 値が一致 → 左右の子木を再帰で比較 ──────────── + // 「p と q が同じ木」 ⟺ + // 「根の値が同じ」かつ「左の子木が同じ」かつ「右の子木が同じ」 + // この定義をそのままコードにしたのが以下の1行。 + // && (AND) を使うことで、左が false なら右の再帰は実行されない + // (短絡評価=ムダな比較をしない) + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); +} +``` + +--- + +> 💡 **コードの動作トレース**(Example 2: `p=[1,2]`, `q=[1,null,2]`) +> +> ``` +> 呼び出し①: isSameTree(p=Node(1), q=Node(1)) +> → ①③ を通過(両方非 null, 1 == 1) +> → isSameTree(p.left, q.left) を呼び出す +> +> 呼び出し②: isSameTree(p=Node(2), q=null) +> → p は非 null, q は null +> → ② の条件: p === null || q === null → true +> → 即座に false を返す ← ここで終了! +> +> ② が false を返した瞬間、① の && 演算子の左辺が false になる。 +> 短絡評価により右辺(p.right vs q.right)は実行されない。 +> 全体の結果: false +> ``` + +--- + +> 📖 **このセクションで登場した用語** +> +> - **短絡評価(ショートサーキット)**:`A && B` の A が `false` の時点で B を評価せず `false` を返す仕組み。無駄な処理を省ける +> - **再帰(recursion)**:関数が自分自身を呼び出す仕組み。「木の比較 = 根の比較 + 左の子木の比較 + 右の子木の比較」という繰り返し構造に最適 +> - **O(h)**:h は木の高さ(height)。平衡二分木(バランスが取れた木)では `h ≈ log₂ n`、最悪(一本道)では `h = n` + +--- + +## TypeScript固有の最適化観点 + +### 型安全性の活用 + +`TreeNode | null` という**ユニオン型**(=「AかBのどちらかの型」を表す TypeScript の仕組み)を引数に使うことで、「ノードか null を渡す」という仕様を型が文書として機能します。①②のチェック後、③④では TypeScript が「ここでは p・q は必ず `TreeNode` だ」と型を絞り込んでくれます(これを**型の絞り込み / narrowing** と言います)。 + +### コンパイル時最適化 + +`strict: true` モード下では、`null` チェックなしに `p.val` へアクセスしようとするとコンパイルエラーになります。今回のコード構成は「先に `null` を排除してから `.val` を触る」という構造になっているため、**コンパイラーが安全性を保証したうえで実行される**、理想的な形です。 + +### 開発効率と保守性 + +戻り値 `boolean` を明示したことで、IDE の IntelliSense が `isSameTree(...)` の呼び出し元に型情報を提供します。将来このロジックを別の関数に組み込む際も、型の不一致をコンパイラーが即座に検出してくれます。 + +> 📖 **最終用語まとめ** +> +> - **ユニオン型**:`A | B` の形で「A か B のどちらか」の型を表す TypeScript の機能 +> - **型の絞り込み(narrowing)**:`if (p === null)` などのチェックの後で TypeScript が自動的に型を絞り込む仕組み +> - **IntelliSense**:IDE が型情報をもとに補完候補やエラーをリアルタイムに表示する機能。型定義が詳細なほど精度が上がる diff --git a/generate_index.py b/generate_index.py index e75e5b6..6d1080f 100644 --- a/generate_index.py +++ b/generate_index.py @@ -955,7 +955,7 @@ def render_category_files(structure, sorted_categories): safe_category = html.escape(category, quote=True) icon = category_icons.get(category, '📁') # Default icon if not found - tabs_html_list.append(f'\n') diff --git a/public/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html b/public/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html new file mode 100644 index 0000000..590b8c8 --- /dev/null +++ b/public/Algorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html @@ -0,0 +1,1573 @@ + + + + + + LeetCode 94 - Binary Tree Inorder Traversal + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html b/public/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html new file mode 100644 index 0000000..88c396a --- /dev/null +++ b/public/Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/README_react.html @@ -0,0 +1,1997 @@ + + + + + + LeetCode 100 — Same Tree | 再帰DFS解説 + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「2本の二分木が、形も値もまったく同じかどうかを確認する問題」 +

+

+ 二分木(=各ノードが左と右に高々1つずつ子を持つ木構造)が2本与えられます。 + 「同じ木」とは、すべての対応するノードが同じ値を持ち、かつ木の形(どこに子がいるか)も完全に一致することです。 + 単純に見えますが、「木の形の比較」という点で少し考える必要があります。 +

+
+ +
+

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

+
    +
  • + null(何もない)の扱いが難しい:片方の木にはノードがあり、もう片方には何もない(null)という場合を正確に区別しなければならない +
  • +
  • + 全ノードを調べる必要がある:根(ルート)の値が同じでも、葉(末端)の値や位置が違えば「異なる木」になる。部分的な確認では不十分 +
  • +
+
+ +
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量
+
+
+
+ 再帰DFS +
+
アルゴリズム
+
+
+
Easy
+
難易度
+
+
+ + +
+
+

+ 例1 → true +

+
+p: [1,2,3]    q: [1,2,3]
+    1              1
+   / \            / \
+  2   3          2   3
+

形も値もすべて同じ → true ✅

+
+
+

+ 例2 → false +

+
+p: [1,2]      q: [1,null,2]
+    1              1
+   /                \
+  2                  2
+

+ 値は同じでも位置(左 vs 右)が違う → false ❌ +

+
+
+

+ 例3 → false +

+
+p: [1,2,1]    q: [1,1,2]
+    1              1
+   / \            / \
+  2   1          1   2
+

+ 左右の値が入れ替わっている → false ❌ +

+
+
+ +
+

📌 制約

+
    +
  • + 両方の木のノード数は + 0 以上 + 100 以下 +
  • +
  • + -10⁴ ≤ Node.val ≤ 10⁴ +
  • +
+
+
+ + +
+

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

+

+ 再帰DFSがどのように動くかを4つのステップで確認しましょう。 ▶ Play + ボタンで自動的に進めることもできます。 +

+
+
+ + +
+

+ Python 実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. + 両方が + None + かを確認 → 同じ葉の先端なら + True +
  2. +
  3. + 片方だけ + None + かを確認 → 構造が違うので + False +
  4. +
  5. + 両方の値(p.val != q.val)が違うなら + False +
  6. +
  7. + 左の子木・右の子木を再帰で比較し、両方一致なら + True +
  8. +
+
+ +
class Solution(object):
+    def isSameTree(self, p, q):
+        """
+        :type p: Optional[TreeNode]
+        :type q: Optional[TreeNode]
+        :rtype: bool
+        """
+        # ── ① 両方 None のとき ──────────────────────────
+        # 葉ノードのさらに下(何もない場所)に両方とも到達した。
+        # 「どちらにも子がない」=構造が一致している → True
+        # `is None` を使うのが Pythonic(Pythonらしい慣用的な書き方)
+        if p is None and q is None:
+            return True
+
+        # ── ② 片方だけ None のとき ──────────────────────
+        # ①で「両方 None」はすでに return 済み。
+        # ここに来るのは「どちらか一方だけ None」の場合のみ。
+        # 片方にノードがあり、片方にない = 構造が違う → False
+        if p is None or q is None:
+            return False
+
+        # ── ③ 値の比較 ───────────────────────────────────
+        # ①②を通過した時点で p も q も None でないことが確定。
+        # pylance もここでは p・q を TreeNode として認識する
+        # (型の絞り込み = Type Narrowing と呼ばれる仕組み)。
+        if p.val != q.val:
+            return False
+
+        # ── ④ 左右の子木を再帰で比較 ────────────────────
+        # 根の値が一致したので、次は左・右の子木を同じ手順で比較する。
+        # `and` の短絡評価(左が False なら右は実行しない)で
+        # 不一致が見つかった時点で即座に False を返せる。
+        return (
+            self.isSameTree(p.left, q.left)
+            and self.isSameTree(p.right, q.right)
+        )
+ +
+

+ ▶ 入力例 p=[1,2] / q=[1,null,2] での動作トレース +

+
+呼び出し①: isSameTree(Node(1), Node(1))
+  → ① 両方非 None → パス
+  → ② どちらも非 None → パス
+  → ③ 1 == 1 → パス(値が等しいので続ける)
+  → ④ 左の子を比較するために再帰呼び出し
+
+呼び出し②: isSameTree(Node(2), None)   ← p.left=Node(2), q.left=None
+  → ① p は非 None → パス(両方 None ではない)
+  → ② p は非 None だが q は None → return False ← ここで終了!
+
+呼び出し①に戻る:
+  → isSameTree(p.left, q.left) = False
+  → and の短絡評価:False and ... → 右辺の再帰は実行されない
+  → return False
+
+最終結果: False ✅
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

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

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい + 赤=いいえ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + 開始: isSameTree(p, q) + + + + + + + + + p と q は + + + どちらも None? + + + + + + はい + + + + + + True + + + + + + いいえ + + + + + + どちらか一方だけ + + + None? + + + + + + はい + + + + False + + + + + + いいえ + + + + + + p.val ≠ q.val + + + (値が違う)? + + + + + + はい + + + + False + + + + + + いいえ + + + + + + isSameTree(p.left, q.left) を再帰呼び出し + + + + + + + + + 左の結果は + + + True? + + + + + + いいえ + + + + False + + + + + + はい + + + + + + isSameTree(p.right, q.right) を再帰呼び出し + + + + + + + + + 右の結果は + + + True? + + + + + + いいえ + + + + False + + + + + + はい + + + + + + 終了: True を返す + + + + + + + + 再帰ループ(子ノードへ) + + +
+ +
+

+ 🔎 入力例 p=[1,2,3] / q=[1,2,3] でのフロー追跡 +

+
    +
  1. 「開始」ノード → isSameTree(Node(1), Node(1)) を受け取る
  2. +
  3. + 「どちらも None?」ノード → 両方非 None → + いいえ の経路へ +
  4. +
  5. + 「片方だけ None?」ノード → どちらも非 None → + いいえ の経路へ +
  6. +
  7. + 「p.val ≠ q.val?」ノード → 1 == 1 → + いいえ の経路へ(値が等しいので続ける) +
  8. +
  9. + 「左の子木を再帰比較」→ isSameTree(Node(2), Node(2)) + を再帰呼び出し(さらに深く潜る) +
  10. +
  11. 「左の結果 True?」→ 左の子木も一致 → はい の経路へ
  12. +
  13. 「右の子木を再帰比較」→ isSameTree(Node(3), Node(3)) を再帰呼び出し
  14. +
  15. 「右の結果 True?」→ 右の子木も一致 → はい の経路へ
  16. +
  17. 「終了」ノード → True を返す ✅
  18. +
+
+
+ + +
+

+ 計算量分析 +

+ +
+

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

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ n より少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
観点計算量条件
時間計算量O(n) + n = 2つの木の総ノード数。全ノードを最大1回訪問 +
空間計算量(平均)O(log n) + 平衡二分木の場合(高さ h ≈ log₂ n) +
空間計算量(最悪)O(n) + 一本道の木(高さ = ノード数)の場合 +
本問題での実際O(100) + ノード数 ≤ 100 の制約により事実上定数 +
+
+ +
+

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

+

+ 時間計算量 O(n):「2つの木が同じかどうか」を確認するには、すべてのノードを少なくとも1回は調べなければなりません。再帰DFSは各ノードをちょうど1回だけ訪問するため、n + ノードに対して O(n) の操作で済みます。
+ 空間計算量 O(h):再帰呼び出しはコールスタック(=関数呼び出しの積み重ね)にメモリを使います。一番深くまで潜ったとき(葉ノードに到達したとき)の積み重ねの深さが木の高さ + h なので、O(h) + のメモリが必要です。追加のデータ構造(リストやキューなど)は一切使わないため、スタック以外のメモリは + O(1) です。 +

+
+ + +
+

📊 アプローチ別比較

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間空間特徴
+ ✅ 再帰DFS(採用) + O(n)O(h) + コードが最もシンプル。問題の定義と1対1対応 +
+ 反復DFS(スタック) + O(n)O(h) + list をスタック代わりに使用。再帰を使わない +
+ 反復BFS(キュー) + O(n) + O(n) + + deque 使用。常に O(n) メモリを消費して不利 +
+
+
+
+ + +
+

+ 📖 用語集 +

+

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

+
+
+ + None(ナン) + +
+ Pythonで「何もない」を表す特別な値。他の言語の + null + に相当します。 二分木では、子がいないノードの + left や + right + が + None + になります。 比較する際は + == None + ではなく + is None + を使うのが Pythonic です。 +
+
+ +
+ + DFS(深さ優先探索 / + Depth-First Search) + +
+ 木やグラフを「根から葉まで深く潜ってから戻る」順番で探索する手法。 + 迷路を解くとき「行き止まりに当たるまでまっすぐ進み、行き止まりになったら戻って別の道を試す」のと同じ考え方です。 + 今回の問題では再帰関数が自動的に DFS の順序でノードを訪問します。 +
+
+ +
+ + コールスタック(Call + Stack) + +
+ 関数を呼び出すたびに「呼び出し情報」を積み上げていくメモリ領域。 + お皿の山積みに例えると、新しい関数呼び出しのたびにお皿を1枚重ね、関数が終了するとお皿を1枚取り除きます。 + 再帰が深くなるほどお皿が積み重なり、メモリを消費します。これが空間計算量 + O(h) の理由です。 +
+
+ +
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す仕組み。「木の比較」のように「同じ問題が小さいサイズで繰り返される」構造に特に適しています。 + 必ず「基底条件(これ以上深く行かない条件)」を設定しないと無限ループになるので注意が必要です。 + 今回の基底条件は + p is None and q is None + のときに + True + を返す部分です。 +
+
+ +
+ + + 短絡評価(Short-Circuit Evaluation) + +
+ A and B + の A が + False + なら B を評価しない・ + A or B + の A が + True + なら B を評価しない仕組み。 今回のコードでは + isSameTree(p.left, q.left) and isSameTree(p.right, q.right) + において、 + 左の子木が一致しなければ右の再帰は実行されません。不必要な処理を省いて効率化できます。 +
+
+ +
+ + 二分木(Binary + Tree) + +
+ 各ノード(節点)が高々2つの子(左の子・右の子)を持つ木構造のこと。 + 家系図に例えると、親が最大2人の子を持てる構造です。 今回の問題の + TreeNode + クラスはこの構造を + left と + right + の2つの参照で表現しています。 +
+
+ +
+ + 平衡二分木(Balanced + Binary Tree) + +
+ 左右の子木の高さの差が小さい、バランスの取れた二分木のこと。 n + 個のノードを持つ平衡二分木の高さは約 log₂ n になります。 例えば 1000 + ノードなら高さは約 10 です。 逆に「一本道」の木(チェーン状)は高さが n + になり、最悪ケースの空間計算量 O(n) に相当します。 +
+
+ +
+ + + Pythonic(パイソニック) + +
+ Pythonらしい、慣用的な書き方のこと。Pythonコミュニティが「この書き方が自然で読みやすい」と考えるスタイルを指します。 + 例えば + x == None + より + x is None、 + len(lst) == 0 + より + not lst + が Pythonic とされています。 +
+
+
+
+ + +
+

LeetCode 100 — Same Tree | 再帰DFS による O(n) 実装解説

+

Python 3 · 初学者向け解説ページ

+
+
+ + + + + + + + diff --git a/public/index.html b/public/index.html index ef6224d..a4b0ea3 100644 --- a/public/index.html +++ b/public/index.html @@ -416,7 +416,7 @@

🧪 Algorithm Study Index

-

165 interactive lessons across 6 domains

+

167 interactive lessons across 6 domains

@@ -431,14 +431,14 @@

- - - - - - + + + + + +
@@ -466,6 +466,7 @@

  • 🧩Jump Game Algorithm AnalysisAlgorithm/greedy algorithm/leetcode/55. Jump Game/Claude/README.html
  • 🧩Jump Game II アルゴリズム解析Algorithm/greedy algorithm/leetcode/45. Jump Game II/Claude/README.html
  • 🧩LeetCode #83 - Remove Duplicates from Sorted ListAlgorithm/Other/leetcode/83. Remove Duplicates from Sorted List/Claude 4.6 extended/README_React.html
  • +
  • 🧩LeetCode 100 — Same Tree | 再帰DFS解説Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/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
  • @@ -475,6 +476,7 @@

  • 🧩LeetCode 7: Reverse Integer - 文字列反転法Algorithm/Other/leetcode/7. Reverse Integer/claude/README.html
  • 🧩LeetCode 88 – Merge Sorted ArrayAlgorithm/Sort/MergeSort/leetcode/claude 4.6 sonnet extended/88. Merge Sorted Array/README_React.html
  • 🧩LeetCode 93: Restore IP Addresses - DFS + 枝刈り解説Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.html
  • +
  • 🧩LeetCode 94 - Binary Tree Inorder TraversalAlgorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html
  • 🧩LeetCode 96: Unique Binary Search Trees - カタラン数解説Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README_react.html
  • 🧩LeetCode 97: Interleaving String - 1D DP解説Algorithm/DynamicProgramming/leetcode/97. Interleaving String/Claude Sonnet 4.5/README_React.html
  • 🧩LeetCode 98: Validate Binary Search TreeAlgorithm/BinarySearch/leetcode/98. Validate Binary Search Tree/Claude Sonnet 4.5/README_react.html
  • @@ -638,6 +640,7 @@

  • 🧩Jump Game Algorithm AnalysisAlgorithm/greedy algorithm/leetcode/55. Jump Game/Claude/README.html
  • 🧩Jump Game II アルゴリズム解析Algorithm/greedy algorithm/leetcode/45. Jump Game II/Claude/README.html
  • 🧩LeetCode #83 - Remove Duplicates from Sorted ListAlgorithm/Other/leetcode/83. Remove Duplicates from Sorted List/Claude 4.6 extended/README_React.html
  • +
  • 🧩LeetCode 100 — Same Tree | 再帰DFS解説Algorithm/Other/leetcode/100. Same Tree/claude sonnet 4.6 extended/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
  • @@ -647,6 +650,7 @@

  • 🧩LeetCode 7: Reverse Integer - 文字列反転法Algorithm/Other/leetcode/7. Reverse Integer/claude/README.html
  • 🧩LeetCode 88 – Merge Sorted ArrayAlgorithm/Sort/MergeSort/leetcode/claude 4.6 sonnet extended/88. Merge Sorted Array/README_React.html
  • 🧩LeetCode 93: Restore IP Addresses - DFS + 枝刈り解説Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.html
  • +
  • 🧩LeetCode 94 - Binary Tree Inorder TraversalAlgorithm/BinaryTree/claude sonnet 4.6 extended/94. Binary Tree Inorder Traversal/README_react.html
  • 🧩LeetCode 96: Unique Binary Search Trees - カタラン数解説Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README_react.html
  • 🧩LeetCode 97: Interleaving String - 1D DP解説Algorithm/DynamicProgramming/leetcode/97. Interleaving String/Claude Sonnet 4.5/README_React.html
  • 🧩LeetCode 98: Validate Binary Search TreeAlgorithm/BinarySearch/leetcode/98. Validate Binary Search Tree/Claude Sonnet 4.5/README_react.html
  • @@ -813,7 +817,7 @@

    🧪 - Generated on 2026-03-14 + Generated on 2026-03-19

    + + + + +
    +

    + アルゴリズム概要 +

    +
    +
    +
    + 中順走査 +
    +
    左 → 根 → 右
    +
    +
    +
    O(N)
    +
    時間計算量
    +
    +
    +
    O(N)
    +
    空間計算量
    +
    +
    +
    + 反復スタック +
    +
    再帰なし実装
    +
    +
    + +
    +
    +

    📌 問題要約

    +

    + 二分木の根ノード + root が与えられる。 + 中順走査(左→根→右) + でノードの値を収集し、リストとして返す。
    + Follow-up: + 再帰を使わない反復解を実装せよ。 +

    +
    +

    + Input: root = [1, null, 2, 3]
    + Output: [1, 3, 2] +

    +
    +
    +
    +

    📏 制約

    +
      +
    • 🔢 ノード数 N: 0 ≤ N ≤ 100
    • +
    • 🔢 値の範囲: −100 ≤ val ≤ 100
    • +
    • ⚠️ Python再帰上限: デフォルト 1,000(N>1000で危険)
    • +
    • ✅ 反復実装でスタックオーバーフロー回避
    • +
    +
    +
    +
    + + +
    +

    + ステップ解説 +

    +
    +
    + + +
    +

    + 実装コード +

    + +
    + + + +
    + + +
    +
    from __future__ import annotations
    +from typing import Optional
    +
    +
    +# Definition for a binary tree node.
    +class TreeNode:
    +    def __init__(
    +        self,
    +        val: int = 0,
    +        left: Optional["TreeNode"] = None,
    +        right: Optional["TreeNode"] = None,
    +    ) -> None:
    +        self.val = val
    +        self.left = left
    +        self.right = right
    +
    +
    +class Solution:
    +    """
    +    LeetCode 94 - Binary Tree Inorder Traversal
    +    中順走査(左→根→右)を明示スタックによる反復で実装。
    +
    +    Time:  O(N) - 全ノードを一度だけ訪問
    +    Space: O(N) - 明示スタックの最大深さ(最悪: 左偏木で N)
    +    """
    +
    +    def inorderTraversal(self, root: Optional[TreeNode]) -> list[int]:
    +        # ── ガード: 空木は即座に空リストを返す
    +        if root is None:
    +            return []
    +
    +        result: list[int] = []          # 中順走査の結果
    +        stack: list[TreeNode] = []      # 明示スタック(非 None のみ格納)
    +        cur: Optional[TreeNode] = root  # 現在注目しているノード
    +
    +        while cur is not None or stack:
    +
    +            # Ph1: 左端まで潜りながらスタックに積む
    +            while cur is not None:
    +                stack.append(cur)   # 右・自身は後回し
    +                cur = cur.left      # 左へ進む
    +
    +            # Ph2: スタック top を取り出して訪問
    +            node: TreeNode = stack.pop()
    +            result.append(node.val)  # ← 中順で値を記録
    +
    +            # Ph3: 右部分木へカーソルを移す
    +            cur = node.right  # None なら次ループで即 Ph2 へ
    +
    +        return result
    +
    + + + + + + +
    + + +
    +

    + 処理フローチャート +

    +
    + + + + + + + + + + + + + + + + + + + + 開始 + + + + + + + + 初期化 + + + + result=[] stack=[] cur=root + + + + + + + + cur != None + + + または stack 非空? + + + + + + No + + + + + + + + Yes + + + + + + Ph1: cur != None? + + + (左端まで潜る) + + + + + + Yes + + + + + スタックに積む + + + + stack.append(cur) cur = cur.left + + + + + + + + 繰り返し + + + + + + + + No + + + + + + + + Ph2: スタックから取り出して訪問 + + + + node = stack.pop() + + + result.append(node.val) ← 中順で記録 + + + + + + + + Ph3: 右部分木へ移動 + + + + cur = node.right + + + + + + + + + ループ継続 + + + + + + + + + 結果を返す + + + + return result # list[int] + + + + + + + 終了 + + +
    + +
    +

    + フローの説明:
    + 1. 初期化: result・stack・curを初期設定する。
    + 2. ループ条件: + curが非Noneまたはstackが空でない間、3フェーズを繰り返す。
    + 3. Ph1(緑): + curが非Noneの間、左端まで潜りながらスタックに積む(ループバック=紫矢印)。
    + 4. Ph2(青): スタックからpopし、val + を結果リストに中順で記録する。
    + 5. Ph3(紫): cur = node.right + に移動し、ループ条件へ戻る(紫矢印)。
    + 6. 終了(赤): curとstackが共に空になったら結果を返す。 +

    +
    +
    + + +
    +

    + 計算量分析 +

    + +
    +
    +
    O(N)
    +
    時間計算量
    +

    + 全ノードをスタックに push 1回・pop 1回の合計 2N 操作。定数倍を無視すると + O(N)。 +

    +
    +
    +
    O(N)
    +
    空間計算量
    +

    + 明示スタックの最大深さ。最悪ケースは N + ノードが全て左に偏った木(スタック深さ = N)。 +

    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + アプローチ + + 時間 + + 空間 + + 可読性 + + 安全性 +
    + ✅ 反復(明示スタック) + O(N)O(N)★★★ + ◎ +
    + 再帰 DFS + O(N)O(N)★★★ + △ ※ +
    + Morris Traversal + O(N)O(1)★☆☆ + △ 副作用 +
    +

    + ※ Python デフォルト再帰上限 1,000 でスタックオーバーフローリスクあり +

    +
    +
    + + + +