diff --git a/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README.md b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README.md new file mode 100644 index 00000000..cc1e74fe --- /dev/null +++ b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README.md @@ -0,0 +1,756 @@ +# Symmetric Tree - 二分木が鏡写しかどうかを判定する + +--- + +## 目次(Table of Contents) + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 **初学者向け補足**:この問題は一言で言うと「**二分木が中心軸に対して鏡写しになっているかを確認する問題**」です。 + +与えられた二分木(=各ノードが最大2つの子を持つ木構造)が、中心軸(ルートノード)を境に左右対称かどうかを返します。 + +**なぜ難しいのか**:「鏡写し」は単純に「左と右が同じ」ではなく、「左サブツリーの左子」と「右サブツリーの右子」が対応するという**交差した対応関係**を正確に追う必要があるためです。また、ノードの「値が同じ」だけでなく「構造(形)も同じ」でなければならない点もつまずきやすいポイントです。 + +### 問題の制約 + +| 項目 | 内容 | +| ---------------- | ---------------------------- | +| プラットフォーム | LeetCode #101 | +| ノード数 | 1 以上 1000 以下 | +| ノードの値 | -100 以上 100 以下 | +| フォローアップ | 再帰・反復の両方で解けるか? | + +### 入出力例 + +``` +例1) +入力: root = [1, 2, 2, 3, 4, 4, 3] +出力: True +理由: 左右が完全に鏡写し + + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + +例2) +入力: root = [1, 2, 2, null, 3, null, 3] +出力: False +理由: 右の2にleftがなく、rightしかないので非対称 + + 1 + / \ + 2 2 + \ \ + 3 3 +``` + +> 📖 **この章で登場した用語** +> +> - **二分木**:各ノードが最大2つの子(left, right)を持つ木構造のデータ構造 +> - **ルートノード**:木の最上位にあるノード。親を持たない +> - **サブツリー**:あるノードを根とした部分木。左サブツリー・右サブツリーと呼ぶ +> - **対称(鏡写し)**:中心軸を境に左右の形と値が完全に一致している状態 + +--- + +

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

+ +> 💡 **初学者向け補足**:TL;DR(Too Long; Didn't Read)とは「長くて読めない人向けの要約」という意味です。ここではアルゴリズム全体の戦略を箇条書きでまとめます。詳細は後の章で説明するので、**「なんとなくこういう手順で解くんだな」というイメージを掴む章**として位置づけています。 + +### 戦略(再帰版) + +1. **ルートを中心に左右を比較する**:ルート自体は中心軸なので比較せず、`root.left` と `root.right` を「鏡ペア」として渡す +2. **鏡ペアの3条件を再帰でチェックする**: + - 両方 `None` → 対称(基底条件) + - 片方だけ `None` → 非対称(基底条件) + - 両方存在 → 値が等しく、かつ外側ペア・内側ペアも鏡写しか(再帰) +3. **`and` の短絡評価で早期終了**:値が違えば再帰を呼ばずに即 `False` を返す + +### 戦略(反復版) + +1. **`deque`(両端キュー)に「鏡ペア」を積む**:`deque` を使う理由は `popleft()` がO(1)で高速なため +2. **ペアを取り出しながら条件を確認**:再帰と同じ3条件を順番にチェック +3. **次のペアをキューに追加**:外側ペア(左の左子 ↔ 右の右子)と内側ペア(左の右子 ↔ 右の左子) + +### 計算量サマリ + +| 解法 | 時間計算量 | 空間計算量 | +| ----------- | ---------- | -------------------- | +| 再帰(DFS) | O(n) | O(h) — hは木の高さ | +| 反復(BFS) | O(n) | O(w) — wは木の最大幅 | + +> 📖 **この章で登場した用語** +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +> - **DFS(深さ優先探索)**:根から葉へ向かって深く潜っていく探索方法。再帰と相性が良い +> - **BFS(幅優先探索)**:同じ深さのノードを横断する探索方法。キューと相性が良い +> - **短絡評価**:`A and B` でAが `False` なら、Bをまったく評価せず即 `False` を返す仕組み +> - **`deque`(デック)**:前からも後ろからも出し入れできる"両端開きの箱"。C実装のため先頭削除がO(1) + +--- + +

図解

+ +> 💡 **初学者向け補足**:Mermaidフローチャートの読み方として、**ひし形(`{}`)は条件分岐**(Yes/Noに分かれる)、**長方形(`[]`)は処理ステップ**(何かを実行する)を表します。矢印は処理の流れの方向を示します。 + +--- + +### フローチャート(再帰版) + +この図は `_is_mirror(left, right)` という再帰ヘルパー関数の処理の流れを表しています。上から下へ読み進めてください。 + +```mermaid +flowchart TD + Start[Start _is_mirror left right] + BothNone{Both None} + RetTrue[Return True] + EitherNone{Either None} + RetFalse[Return False] + ValCheck{left.val == right.val} + OuterPair[Check outer pair _is_mirror left.left right.right] + InnerPair[Check inner pair _is_mirror left.right right.left] + RetResult[Return combined result] + + Start --> BothNone + BothNone -- Yes --> RetTrue + BothNone -- No --> EitherNone + EitherNone -- Yes --> RetFalse + EitherNone -- No --> ValCheck + ValCheck -- No --> RetFalse + ValCheck -- Yes --> OuterPair + OuterPair --> InnerPair + InnerPair --> RetResult +``` + +**主要なノードの意味**: + +- `Start`:ヘルパー関数の入り口。左右のノードを受け取る +- `BothNone`:両方 `None` かどうかの判定。葉の「次」の位置(存在しない場所)同士の比較 +- `EitherNone`:片方だけ `None` かどうかの判定。一方だけ枝が伸びている非対称の検出 +- `ValCheck`:値が等しいかどうかの判定。構造は同じでも値が違えば非対称 +- `OuterPair`:外側のペア(左の左子 ↔ 右の右子)を再帰確認 +- `InnerPair`:内側のペア(左の右子 ↔ 右の左子)を再帰確認 + +--- + +### データフロー図(鏡ペアの対応関係) + +この図は「どのノード同士がペアとして比較されるか」を表しています。中心軸(ルート)を境に交差した対応関係があることに注目してください。 + +```mermaid +graph LR + subgraph Input + Root[Root node 1] + end + subgraph Left_subtree + L[left node 2] + LL[left.left node 3] + LR[left.right node 4] + end + subgraph Right_subtree + R[right node 2] + RL[right.left node 4] + RR[right.right node 3] + end + subgraph Mirror_pairs + P1[Pair1 L vs R] + P2[Pair2 LL vs RR] + P3[Pair3 LR vs RL] + end + + Root --> L + Root --> R + L --> P1 + R --> P1 + LL --> P2 + RR --> P2 + LR --> P3 + RL --> P3 +``` + +**主要な流れの説明**: + +- `Root → L / R`:ルートから左右のサブツリーへ分岐。ルート自体は比較しない +- `L vs R (Pair1)`:左の2 と 右の2 を最初のペアとして比較 +- `LL vs RR (Pair2)`:外側ペア。左の左子(3)と右の右子(3)を比較 +- `LR vs RL (Pair3)`:内側ペア。左の右子(4)と右の左子(4)を比較 + +--- + +> 💡 **代表例でのトレース**:`root = [1, 2, 2, 3, 4, 4, 3]` を入力として各ステップを追います。 + +``` +初期状態: + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + +Step 1: isSymmetric(root=1) + → root != None なので _is_mirror(root.left=2, root.right=2) を呼ぶ + +Step 2: _is_mirror(left=Node(2), right=Node(2)) + → BothNone? No(両方ノードが存在) + → EitherNone? No(どちらもNoneではない) + → ValCheck: 2 == 2 ✅ + → 外側ペア: _is_mirror(left.left=Node(3), right.right=Node(3)) を呼ぶ + +Step 3: _is_mirror(left=Node(3), right=Node(3)) + → 3 == 3 ✅ + → _is_mirror(None, None) → BothNone = True ✅ + → _is_mirror(None, None) → BothNone = True ✅ + → True を返す + +Step 4: Step2 に戻り 内側ペア: _is_mirror(left.right=Node(4), right.left=Node(4)) + → 4 == 4 ✅ + → _is_mirror(None, None) → True ✅ + → _is_mirror(None, None) → True ✅ + → True を返す + +Step 5: True and True and True = True +最終結果: True ✅ +``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理 +> - **データフロー図**:データがどのように変換・移動するかを示す図 +> - **外側ペア**:左サブツリーの「左子」と右サブツリーの「右子」の組み合わせ +> - **内側ペア**:左サブツリーの「右子」と右サブツリーの「左子」の組み合わせ + +--- + +

正しさのスケッチ

+ +> 💡 **初学者向け補足**:「正しさのスケッチ」とは、アルゴリズムが**常に正しい答えを返すことの根拠**を整理したものです。数学的な厳密な証明ではなく、「なぜ正しいと言えるか」の説明です。 + +### 基底条件(再帰が止まる条件) + +再帰が終わらないと無限ループになります。このアルゴリズムでは2つの基底条件があります。 + +| 条件 | 処理 | 意味 | +| -------------------------------- | -------------- | -------------------------------------------------------------------------- | +| `left is None and right is None` | `True` を返す | 「空同士」は対称と定義できる。葉ノードの先(存在しない位置)を比較している | +| `left is None or right is None` | `False` を返す | 一方だけ枝がある = 非対称。片方だけ子が存在するので鏡写しではない | + +### 不変条件(処理中ずっと成り立つ条件) + +> 不変条件(=アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件) + +`_is_mirror(left, right)` を呼ぶとき、`left` と `right` は常に「同じ深さの鏡対応するノード」です。 + +- 最初の呼び出し:`_is_mirror(root.left, root.right)` → 深さ1の左右ノード +- 次の呼び出し:`_is_mirror(left.left, right.right)` → 深さ2の外側ノード +- この対応関係は再帰のたびに「1段深い鏡ペア」に移行するので、常に正しいペアを比較している + +### 網羅性(すべてのケースを処理しているか) + +`match (left, right)` の3ケースは**互いに排他的かつ網羅的**です。 + +``` +ケース① left is None and right is None → True(両方空) +ケース② left is None or right is None → False(片方空) +ケース③ 上記以外(両方ノードが存在) → 値比較 + 再帰 +``` + +この3ケースで「左右ノードのあらゆる組み合わせ」をカバーしています。 + +### 終了性(必ず有限ステップで終わるか) + +> 終了性(=アルゴリズムが必ず有限ステップで終わるという保証) + +再帰のたびに「木の深さが1段増える」ので、最終的には必ず葉ノードの子(`None`)に到達します。ノード数が有限(最大1000)なので、再帰は有限回で終了します。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **基底条件**:再帰の終了条件。これがないと無限ループになる +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 +> - **排他的かつ網羅的**:各ケースが重複せず(排他)、かつすべての入力を受け入れる(網羅)こと + +--- + +

計算量

+ +> 💡 **初学者向け補足**:計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| ------------ | ---------------------- | -------------------------- | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(n log n)` | nよりやや速く増加 | 辞書を二分探索で引く×n回 | +| `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +### この問題の計算量 + +| 解法 | 時間計算量 | 空間計算量 | 理由 | +| ----------- | ---------- | ---------- | ------------------------------------------------------- | +| 再帰(DFS) | **O(n)** | **O(h)** | 全ノードを1回ずつ訪問。スタックの深さ = 木の高さh | +| 反復(BFS) | **O(n)** | **O(w)** | 全ノードを1回ずつ訪問。キューの最大サイズ = 木の最大幅w | + +### 空間計算量の詳細 + +``` +h(木の高さ)の最悪・最良ケース: + 最悪: O(n) → 一直線の木(全ノードが一方向に連なる場合) + 1 + / + 2 + / + 3 ← 高さ = ノード数 n + + 最良: O(log n) → 完全バランス木(全レベルにノードが均等に存在する場合) + 1 + / \ + 2 3 + / \ / \ + 4 5 6 7 ← 高さ = log₂(n) + +w(木の最大幅)の最悪ケース: + 最悪: O(n) → 完全二分木の最下段(葉ノードがn/2個) + → 反復BFS版では完全バランス木の方が多くのメモリを使う! +``` + +### 再帰 vs 反復の比較 + +| 観点 | 再帰版 | 反復版 | +| ---------------------------- | --------------------------- | -------------------------- | +| コードの読みやすさ | ★★★(定義に近い自然な記述) | ★★☆(少し複雑) | +| スタックオーバーフローリスク | あり(深さ1000で上限近傍) | なし | +| メモリ使用パターン | コールスタック(暗黙) | `deque`(明示) | +| 最悪の空間計算量 | O(n)(一直線の木) | O(n)(完全二分木の最下段) | + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **コールスタック**:関数が呼び出されるたびに積み上がる「呼び出し履歴」。再帰が深いほど消費する +> - **スタックオーバーフロー**:コールスタックが上限を超えてクラッシュする現象。Pythonはデフォルト1000回 +> - **完全バランス木**:全レベルにノードが均等に存在する木。高さが最小(log n)になる + +--- + +

Python 実装

+ +> 💡 **初学者向け補足**:コードを読む前に、実装の**全体的な骨格**を確認します。 +> +> **再帰版の骨格**: +> +> 1. `isSymmetric`:エントリーポイント。`root` が `None` なら即 `True`、そうでなければヘルパーを呼ぶ +> 2. `_is_mirror`:2つのノードを受け取り、3ケースで鏡写し判定を再帰的に行う +> +> **反復版の骨格**: +> +> 1. `deque` に最初の鏡ペアを追加 +> 2. キューが空になるまで取り出してはチェックを繰り返す +> 3. 次のペアをキューに積む + +--- + +### 解法①:再帰版(メイン実装) + +```python +from collections import deque + + +# LeetCode が提供する TreeNode クラス(提出時はコメントアウト済みのものを使用) +# class TreeNode(object): +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right + + +class Solution(object): + """ + Symmetric Tree 解決クラス + + 二分木が中心軸に対して鏡写しかどうかを判定する。 + 再帰(DFS)と反復(BFS with deque)の2パターンを提供する。 + """ + + # ============================================= + # 解法①: 再帰版(メイン) + # ============================================= + def isSymmetric(self, root): + """ + 二分木が鏡写し(対称)かどうかを再帰で判定する。 + + :type root: Optional[TreeNode] + :rtype: bool + + Time: O(n) - 全ノードを1回ずつ訪問する + Space: O(h) - 再帰スタックの深さ(h = 木の高さ) + """ + # rootがNone(空の木)は定義上「対称」とする。 + # LeetCodeの制約では最低1ノードあるが、型上Noneがあり得るため処理する + if root is None: + return True + + # rootの左子と右子が「鏡写し」かをヘルパーで確認する。 + # rootそのものは中心軸なので比較対象にならない + return self._is_mirror(root.left, root.right) + + def _is_mirror(self, left, right): + """ + 2つのノードが「鏡写し」の関係かどうかを再帰的に確認するヘルパー。 + + 鏡写しの3条件(すべて満たす必要がある): + 1. 左右の値が等しい + 2. 左の「左子」と右の「右子」が鏡写し(外側ペア) + 3. 左の「右子」と右の「左子」が鏡写し(内側ペア) + + :type left: Optional[TreeNode] + :type right: Optional[TreeNode] + :rtype: bool + """ + # ── 基底条件① ── + # 両方Noneなら「空同士」= 対称 → True。 + # 例: 葉ノードの子(存在しない位置)同士を比較した場合 + if left is None and right is None: + return True + + # ── 基底条件② ── + # 片方だけNoneなら「一方だけ枝がある」= 非対称 → False。 + # `is None` を使う理由: `== None` より高速(同一性チェック)かつ + # Pythonの慣用的な書き方(イディオム) + if left is None or right is None: + return False + + # ── 再帰ステップ ── + # 両方ノードが存在する場合。3条件を `and` で繋いで確認する。 + # `and` の短絡評価(左辺がFalseなら右辺を評価しない)により + # 値が違った時点で即座にFalseを返し、無駄な再帰を省く + return ( + left.val == right.val # 条件1: 値が同じか? + and self._is_mirror(left.left, right.right) # 条件2: 外側ペアを再帰確認 + and self._is_mirror(left.right, right.left) # 条件3: 内側ペアを再帰確認 + ) + + # ============================================= + # 解法②: 反復版(フォローアップ) + # ============================================= + def isSymmetricIterative(self, root): + """ + 二分木が鏡写しかどうかを反復(deque使用)で判定する。 + 再帰の深さ制限(RecursionError)が心配な場合に使う代替実装。 + + dequeを使う理由: + list.pop(0) はO(n)(先頭削除のたびに全要素をずらす)。 + deque.popleft() はO(1)(C実装の双方向リストのため高速)。 + + :type root: Optional[TreeNode] + :rtype: bool + + Time: O(n) - 全ノードを1回ずつ訪問する + Space: O(w) - wは木の最大幅(dequeに入るペアの最大数) + """ + # rootがNoneなら空の木 → 対称 + if root is None: + return True + + # deque(デック)に「比較すべきノードのペア」をタプルで格納する。 + # タプル (左ノード, 右ノード) を順番に取り出して比較していく + queue = deque() + + # 最初のペア: rootの左子と右子をキューに追加 + queue.append((root.left, root.right)) + + # キューが空になるまで(= 全ペアの確認が終わるまで)繰り返す + while queue: + # popleft() でキューの先頭からペアを取り出す。 + # FIFO(先入れ先出し)= 最初に追加したペアから順番に処理する + left, right = queue.popleft() + + # ケース①: 両方NoneならこのペアはOK → 次のペアへ(continueでスキップ) + if left is None and right is None: + continue + + # ケース②: 片方だけNone → 非対称確定。即座にFalseを返す + if left is None or right is None: + return False + + # ケース③: 値が違う → 非対称確定。即座にFalseを返す + if left.val != right.val: + return False + + # 次に確認すべき「鏡ペア」をキューに積む。 + # 外側ペア: 左の左子 ↔ 右の右子(木の外側の対応) + queue.append((left.left, right.right)) + # 内側ペア: 左の右子 ↔ 右の左子(木の内側の対応) + queue.append((left.right, right.left)) + + # 全ペアをパスしたので対称 + return True +``` + +--- + +> 💡 **Example 2 でのトレース**:`root = [1, 2, 2, null, 3, null, 3]`(False のケース) + +``` +初期状態: + 1 + / \ + 2 2 + \ \ + 3 3 + +Step 1: isSymmetric(root=1) + → _is_mirror(root.left=Node(2), root.right=Node(2)) を呼ぶ + +Step 2: _is_mirror(left=Node(2), right=Node(2)) + → BothNone? No + → EitherNone? No + → 2 == 2 ✅ + → 外側ペア: _is_mirror(left.left=None, right.right=Node(3)) を呼ぶ + +Step 3: _is_mirror(left=None, right=Node(3)) + → BothNone? No(Noneと Node(3)) + → EitherNone? Yes(leftがNone!) → False ❌ を返す + +Step 4: and の短絡評価が働く + → 外側ペアが False なので、内側ペアの _is_mirror は呼ばれない(省エネ) + → False を返す + +最終結果: False ❌ +``` + +> 📖 **この章で登場した用語** +> +> - **`is None`**:PythonのイディオムでNoneとの比較に使う。`== None` より高速(同一性チェック) +> - **短絡評価**:`A and B` でAがFalseなら、Bをまったく評価せず即Falseを返す仕組み +> - **`deque`(デック)**:両端開きのキュー。C実装のため `list.pop(0)` のO(n)より `popleft()` のO(1)が高速 +> - **FIFO(先入れ先出し)**:First In First Out。キューの動作原則 +> - **docstringの `:type:` / `:rtype:`**:型アノテーションが使えないPython 2スタイルで型情報を伝えるコメント形式 + +--- + +

CPython最適化ポイント

+ +> 💡 **初学者向け補足**:この章では「同じ処理でもPythonの書き方によって速さが変わる理由」を説明します。最適化テクニックを紹介する際は、**最適化前のコード → 最適化後のコード → なぜ速くなるか** の3点セットで説明します。 + +### ① `self._is_mirror` vs ローカル関数 + +```python +# 最適化前: クラスメソッドとして定義(呼び出しのたびに self から辞書検索が発生) +class Solution(object): + def isSymmetric(self, root): + return self._is_mirror(root.left, root.right) # 毎回 self から検索 + + def _is_mirror(self, left, right): + ... + return (left.val == right.val + and self._is_mirror(left.left, right.right) # ← ここも毎回検索 + and self._is_mirror(left.right, right.left)) + +# 最適化後: ローカル関数として定義(self 参照が不要になる) +class Solution(object): + def isSymmetric(self, root): + def is_mirror(left, right): # ← ローカル関数 + ... + return (left.val == right.val + and is_mirror(left.left, right.right) # ← ローカル変数として高速アクセス + and is_mirror(left.right, right.left)) + + return root is None or is_mirror(root.left, root.right) + +# 理由: Pythonは `self._is_mirror` のたびに内部辞書(__dict__)を検索する。 +# ローカル変数(is_mirror)はより高速なローカルスコープから参照できる。 +# 再帰呼び出し回数が多い(最大n回)ほど、この差が積み重なる。 +``` + +### ② `list.pop(0)` vs `deque.popleft()` + +```python +# 最悪な書き方: リストを使ったキュー(先頭削除のたびに全要素をずらす) +queue = [] +queue.append((root.left, root.right)) +while queue: + left, right = queue.pop(0) # O(n): 全要素を1つずつ左にずらす処理が発生 + +# 正しい書き方: deque を使ったキュー(先頭削除がO(1)) +from collections import deque +queue = deque() +queue.append((root.left, root.right)) +while queue: + left, right = queue.popleft() # O(1): 先頭ポインタを1つ進めるだけ + +# 理由: list はメモリ上に連続した配列として保存されているため、 +# 先頭要素を削除すると残りの全要素を1つ左にコピーしなければならない(O(n))。 +# deque(双方向リスト)は先頭ポインタを進めるだけで先頭削除ができる(O(1))。 +# ノード数1000なら最大999回 popleft() が呼ばれるため、差が出やすい。 +``` + +### ③ `and` の短絡評価の活用 + +```python +# 短絡評価を活かせていない書き方 +outer_result = self._is_mirror(left.left, right.right) +inner_result = self._is_mirror(left.right, right.left) +return left.val == right.val and outer_result and inner_result +# ↑ outer が False でも inner の再帰が先に実行されてしまう! + +# 短絡評価を活かした書き方(このコードで採用) +return ( + left.val == right.val + and self._is_mirror(left.left, right.right) # False ならここで止まる + and self._is_mirror(left.right, right.left) # 上が True のときだけ実行 +) +# 理由: `and` は左辺が False の時点で右辺を評価しない。 +# 非対称の木では早い段階で False が確定することが多いため、 +# この書き方で不要な再帰呼び出しを大幅に削減できる。 +``` + +> 📖 **この章で登場した用語** +> +> - **ローカルスコープ**:関数内で定義された変数が存在する範囲。グローバルスコープより高速にアクセスできる +> - **`__dict__`**:Pythonオブジェクトが属性を保存する内部辞書。`self.x` のアクセスには辞書検索が発生する +> - **双方向リスト**:前後のノードへのポインタを持つリスト構造。先頭・末尾の挿入・削除がO(1)で可能 +> - **短絡評価**:`A and B` でAがFalseなら、Bをまったく評価せず即Falseを返す仕組み + +--- + +

エッジケースと検証観点

+ +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。 + +| ケース | 入力 | 期待出力 | なぜ問題になりうるか | +| ------------------------ | ----------------------- | ----------------- | ----------------------------------------------------------------------------------------- | +| **ノード1個** | `[1]` | `True` | 左右の子が両方 `None` → `_is_mirror(None, None)` が呼ばれる。基底条件① が正しく機能するか | +| **基本ケース(対称)** | `[1,2,2,3,4,4,3]` | `True` | 全条件を満たす正常ケース | +| **基本ケース(非対称)** | `[1,2,2,null,3,null,3]` | `False` | 内側ペアの位置が非対称。片方だけ `null` のケース | +| **値は同じ・構造が違う** | `[1,2,2,null,3,3,null]` | `True` | nullの位置が内側に揃っている(対称な構造) | +| **全て同じ値** | `[1,1,1,1,1,1,1]` | `True` | 値の比較だけでなく構造の比較も正しく行われるか | +| **一直線(左偏り)** | `[1,2,null,3]` | `False` | 右サブツリーが空。深さが非対称 | +| **一直線(深さ最大)** | 深さ1000の一直線の木 | `False` or `True` | 再帰深度が1000に到達。`RecursionError` が発生しないか | +| **値が負の数** | `[0,-1,-1]` | `True` | 制約内(-100〜100)の負の値を正しく比較できるか | +| **最大値・最小値** | `[100,-100,-100]` | `True` | 境界値(-100、100)での動作確認 | + +### 深さ1000への対処 + +```python +# もし深さ1000の一直線の木でRecursionErrorが発生する場合の対処法 +import sys +sys.setrecursionlimit(2000) # デフォルト1000を引き上げる + +# または反復版(isSymmetricIterative)を使う +# → dequeを使うためスタック深度に依存しない +``` + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件の入力 +> - **境界値**:制約の上限・下限にあたる値。例:ノード数1、ノード数1000 +> - **`RecursionError`**:Pythonの再帰呼び出し上限(デフォルト1000回)を超えたときのエラー +> - **`sys.setrecursionlimit()`**:再帰呼び出しの上限を変更する関数。競技プログラミングでよく使う + +--- + +

FAQ

+ +> 💡 **初学者向け補足**:FAQは「初学者がつまずきやすいポイント」を想定した質問と回答です。各回答は「**結論 → 理由 → 補足(具体例)**」の順で書きます。 + +--- + +**Q1. なぜ `left.val == right.val` だけでなく再帰も必要なのか?** + +**結論**:値の一致だけでは「構造が鏡写しかどうか」を確認できないからです。 + +**理由**:以下の木は全ノードの値が `2` で同じですが、構造が非対称です。 + +``` + 2 + / \ + 2 2 + \ + 2 +``` + +`left.val == right.val` (= `2 == 2`)は `True` になりますが、左の右子にノードがあり右の左子に対応するノードがないため、非対称です。再帰で「外側ペア・内側ペアの構造」まで確認することで、構造と値の両方を検証できます。 + +--- + +**Q2. なぜ `_is_mirror` に `root` を渡さないのか?** + +**結論**:`root` は「中心軸」なので、比較対象ではないからです。 + +**理由**:「対称」とは「中心軸を境に左右が鏡写し」という意味です。中心軸そのもの(`root`)は鏡の「軸」であり、左右どちらのサブツリーにも属しません。そのため `isSymmetric` では `root.left` と `root.right` の2つを最初のペアとして渡します。 + +``` + 1 ← この1はただの「軸」。比較対象ではない + / \ + 2 2 ← この2と2を最初のペアとして比較する +``` + +--- + +**Q3. `list.pop(0)` ではなく `deque.popleft()` を使うべき理由は?** + +**結論**:`list.pop(0)` はO(n)、`deque.popleft()` はO(1)だからです。 + +**理由**:Pythonの `list` はメモリ上に連続した配列として保存されています。先頭要素を削除すると、残りの全要素を1つずつ左にずらすコピー操作が必要です(O(n))。一方、`deque` は双方向リストのため、先頭ポインタを1つ進めるだけで先頭削除が完了します(O(1))。 + +**補足(具体例)**: + +``` +list.pop(0) でノード100個の場合: + [ノード1, ノード2, ..., ノード100] + ↑削除! + → [ノード2, ノード3, ..., ノード100] ← 99個のノードを全部移動! + +deque.popleft() でノード100個の場合: + head → [ノード1] → [ノード2] → ... → [ノード100] + 先頭ポインタを進めるだけ。移動ゼロ! +``` + +--- + +**Q4. `is None` と `== None` の違いは?** + +**結論**:`is None` の方が速く、Pythonのイディオム(推奨される書き方)です。 + +**理由**:`is` は「同一のオブジェクトかどうか」を確認する演算子です。`None` はPythonプロセス全体で1つしか存在しないオブジェクトなので、`is None` で確実に確認できます。`== None` は内部的に `__eq__` メソッドを呼び出すため、わずかに遅く、また `__eq__` を独自定義したクラスでは意図しない動作をする可能性があります。 + +**補足**:pylance(Pythonの型チェッカー)も `== None` より `is None` を推奨します。 + +--- + +**Q5. なぜ `and` の短絡評価がパフォーマンスに効くのか?** + +**結論**:非対称が判明した時点で残りの再帰をまったく実行しないからです。 + +**理由**:`A and B and C` という式で A が `False` になると、B も C も評価されません。この問題では「値が違う」ことが分かった瞬間に残りの再帰呼び出しが全てスキップされます。 + +**補足(具体例)**: + +``` +深さ5の一直線の木で根から1段目の値が違う場合: + 短絡評価なし: 全ノードを訪問(最大O(n)の呼び出し) + 短絡評価あり: 1段目のペアで即False → 残り全部スキップ ← ほぼO(1)! +``` + +> 📖 **この章で登場した用語** +> +> - **FAQ**:Frequently Asked Questions の略。よくある質問と回答のこと +> - **イディオム**:プログラミング言語における慣用的な(推奨される)書き方 +> - **`__eq__`**:Pythonオブジェクトの等値比較(`==`)を定義する特殊メソッド +> - **ポインタ**:メモリ上のアドレスを指す変数。`deque` の先頭ポインタを進めることで高速削除が実現する +> - **トレードオフ**:何かを得ると何かを失う関係。再帰は読みやすいがスタックを消費する、など diff --git a/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html new file mode 100644 index 00000000..62ee2c3f --- /dev/null +++ b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html @@ -0,0 +1,2021 @@ + + + + + + LeetCode #101 Symmetric Tree — 完全解説 + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ + +
+

+ 💡 + この問題を一言で言うと:「二分木の左右が完全に鏡写しかどうかを確認する問題」 +

+

+ 「鏡写し」とは、木の中心軸(ルート)を境に、左サブツリーを裏返すと右サブツリーとぴったり重なる状態です。 + 単に「左右の値が同じ」だけでは不十分で、構造(形)と値の両方が対応していなければなりません。 +

+
+ + +
+

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

+
    +
  • + 「左と右が同じ構造」ではなく「左の左 ↔ 右の右左の右 ↔ 右の左」という交差した対応関係を正確に追う必要がある +
  • +
  • + ノードが + None(存在しない)の場合のケースを3パターン(両方None・片方None・両方あり)に分けて処理しないとバグになる +
  • +
  • + 値が全て同じでも構造が非対称なケース(例:[2,2,2,null,2])で誤検知しやすい +
  • +
+
+ + +
+
+
+ ✅ 例1:対称な木 +
+
+入力: [1, 2, 2, 3, 4, 4, 3]
+出力: True
+
+       1
+      / \
+     2   2
+    / \ / \
+   3  4 4  3
+
+理由: 左右が完全に鏡写し
+  左の左子(3) ↔ 右の右子(3) ✅
+  左の右子(4) ↔ 右の左子(4) ✅
+
+
+
+ ❌ 例2:非対称な木 +
+
+入力: [1, 2, 2, null, 3, null, 3]
+出力: False
+
+       1
+      / \
+     2   2
+      \   \
+       3   3
+
+理由: left.leftがnullである一方で
+  right.rightは3なので
+  nullと3が一致せず非対称になる ❌
+
+
+ + +
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量(再帰)
+
+
+
+ 1〜1000 +
+
ノード数の制約
+
+
+
+ -100〜100 +
+
ノード値の範囲
+
+
+
+ + +
+

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

+

+ 各ステップをクリックすると詳細が表示されます。▶ Play で自動再生も可能です。 +

+
+
+ + +
+

+ Python 実装 +

+ + +
+

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

+
    +
  1. + isSymmetric:エントリーポイント。root が None なら即 + True、そうでなければヘルパーへ +
  2. +
  3. + _is_mirror:2つのノードを受け取り、3ケース(両方None・片方None・両方あり)で鏡写し判定 +
  4. +
  5. 値が等しく、外側ペア・内側ペアも再帰的に鏡写しなら True を返す
  6. +
  7. + isSymmetricIterative:deque を使う反復版(スタック深度制限を回避したい場合) +
  8. +
+
+ +
from collections import deque
+
+
+# LeetCode が提供する TreeNode クラス(提出時はコメント済みのものを使用)
+# class TreeNode(object):
+#     def __init__(self, val=0, left=None, right=None):
+#         self.val = val
+#         self.left = left
+#         self.right = right
+
+class Solution(object):
+
+    # =========================================================
+    # 解法①: 再帰版(メイン)
+    # =========================================================
+    def isSymmetric(self, root):
+        """
+        二分木が鏡写し(対称)かどうかを再帰で判定する。
+
+        :type  root: Optional[TreeNode]
+        :rtype: bool
+
+        Time:  O(n) - 全ノードを1回ずつ訪問する
+        Space: O(h) - 再帰スタックの深さ(h = 木の高さ)
+        """
+        # rootがNone(空の木)は「対称」と定義する
+        if root is None:
+            return True
+
+        # rootは中心軸なので比較しない。左子と右子を最初のペアとして渡す
+        return self._is_mirror(root.left, root.right)
+
+    def _is_mirror(self, left, right):
+        """
+        2つのノードが「鏡写し」の関係かどうかを再帰的に確認するヘルパー。
+
+        :type  left:  Optional[TreeNode]
+        :type  right: Optional[TreeNode]
+        :rtype: bool
+        """
+        # ── 基底条件① ──
+        # 両方Noneなら「空同士」= 対称 → True
+        # 例: 葉ノードの子(存在しない位置)同士を比較した場合
+        if left is None and right is None:
+            return True
+
+        # ── 基底条件② ──
+        # 片方だけNoneなら「一方だけ枝がある」= 非対称 → False
+        # `is None` を使う: `== None` より高速(同一性チェック)
+        if left is None or right is None:
+            return False
+
+        # ── 再帰ステップ ──
+        # 3条件を `and` で繋ぐ。短絡評価で値が違えば即Falseを返す
+        return (
+            left.val == right.val                          # 条件1: 値が同じか?
+            and self._is_mirror(left.left, right.right)   # 条件2: 外側ペア
+            and self._is_mirror(left.right, right.left)   # 条件3: 内側ペア
+        )
+
+    # =========================================================
+    # 解法②: 反復版(フォローアップ)
+    # =========================================================
+    def isSymmetricIterative(self, root):
+        """
+        二分木が鏡写しかどうかを反復(deque)で判定する。
+        RecursionError が心配な場合はこちらを使う。
+
+        deque を使う理由: list.pop(0) は O(n) だが
+                          deque.popleft() は O(1) で高速。
+
+        :type  root: Optional[TreeNode]
+        :rtype: bool
+
+        Time:  O(n)  Space: O(w) - wは木の最大幅
+        """
+        if root is None:
+            return True
+
+        # dequeに「鏡ペア」をタプルで格納して順番に比較する
+        queue = deque()
+        queue.append((root.left, root.right))
+
+        while queue:
+            # FIFO(先入れ先出し)でペアを取り出す
+            left, right = queue.popleft()
+
+            if left is None and right is None:
+                continue           # 両方None → OK、次のペアへ
+            if left is None or right is None:
+                return False       # 片方だけNone → 非対称
+            if left.val != right.val:
+                return False       # 値が違う → 非対称
+
+            # 次に確認すべき鏡ペアをキューに追加
+            queue.append((left.left, right.right))   # 外側ペア
+            queue.append((left.right, right.left))   # 内側ペア
+
+        return True
+ + +
+

+ ▶ 入力例 + root = [1, 2, 2, 3, 4, 4, 3] + での動作トレース(再帰版) +

+
+isSymmetric(root=1)
+  → root != None → _is_mirror(root.left=2, root.right=2)
+
+_is_mirror(left=Node(2), right=Node(2))
+  → 両方Noneでない、片方Noneでない
+  → 2 == 2 ✅
+  → _is_mirror(left.left=Node(3), right.right=Node(3))  ← 外側ペア
+
+    _is_mirror(left=Node(3), right=Node(3))
+      → 3 == 3 ✅
+      → _is_mirror(None, None) → True ✅
+      → _is_mirror(None, None) → True ✅
+      → True ✅
+
+  → _is_mirror(left.right=Node(4), right.left=Node(4))  ← 内側ペア
+
+    _is_mirror(left=Node(4), right=Node(4))
+      → 4 == 4 ✅
+      → _is_mirror(None, None) → True ✅
+      → _is_mirror(None, None) → True ✅
+      → True ✅
+
+最終結果: True and True and True = True ✅
+
+ + +
+

+ ▶ 入力例 + root = [1, 2, 2, null, 3, null, 3] + での動作トレース(短絡評価の効果) +

+
+_is_mirror(left=Node(2), right=Node(2))
+  → 2 == 2 ✅
+  → _is_mirror(left.left=None, right.right=Node(3))  ← 外側ペア
+
+    _is_mirror(left=None, right=Node(3))
+      → 片方だけ None → False ❌ ← ここで即終了!
+
+  → False なので and の短絡評価が働き
+    内側ペアの _is_mirror は呼ばれない(省エネ!)
+
+最終結果: False ❌
+
+
+ + +
+

+ 処理フローチャート +

+ + +
+

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

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+
+ はい + いいえ +
+
+
+
+ +

+ この図は + _is_mirror(left, right) + ヘルパー関数の処理の流れを表しています。 + 上から下へ読み進め、ひし形の分岐で「はい/いいえ」のどちらかの経路を進みます。 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + 開始 + + + + + + + + + root is None? + + + (空の木か?) + + + + + + + True を返す + + + はい + + + + + + いいえ + + + + + + _is_mirror(root.left, root.right) + + + 左子と右子を「最初の鏡ペア」として渡す + + + + + + + + + left と right + + + 両方 None? + + + + + + + True を返す + + + はい + + + + + + いいえ + + + + + + left または right + + + 片方だけ None? + + + + + + + False を返す + + + はい + + + + + + いいえ + + + + + + left.val + + + == right.val? + + + + + + + False を返す + + + いいえ + + + + + はい + + + + + 外側ペアを再帰確認 + + + _is_mirror(left.left, right.right) + + + + + + + + + 内側ペアを再帰確認 + + + _is_mirror(left.right, right.left) + + + + + + + + + 3条件の AND で結合 + + + 値一致 and 外側OK and 内側OK + + + + + + + + + 終了(結果を返す) + + +
+ + +
+

+ 🔎 入力例 + [1, 2, 2, 3, 4, 4, 3] + でのフロー追跡 +

+
    +
  1. 「開始」ノード → 入力 root=1 を受け取る
  2. +
  3. 「root is None?」ノード → root=1 なので「いいえ」の経路へ
  4. +
  5. + 「_is_mirror を呼ぶ」ノード → _is_mirror(left=2, right=2) を呼び出す +
  6. +
  7. 「両方 None?」ノード → 両方ノードが存在するので「いいえ」の経路へ
  8. +
  9. 「片方だけ None?」ノード → どちらもNoneでないので「いいえ」の経路へ
  10. +
  11. 「left.val == right.val?」ノード → 2 == 2 なので「はい」の経路へ
  12. +
  13. 「外側ペアを再帰」ノード → _is_mirror(3, 3) を呼び True が返る
  14. +
  15. 「内側ペアを再帰」ノード → _is_mirror(4, 4) を呼び True が返る
  16. +
  17. 「3条件の AND で結合」ノード → True and True and True = True
  18. +
  19. 「終了」ノード → True を返す ✅
  20. +
+
+
+ + +
+

+ 計算量分析 +

+ + +
+

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

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ n より少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ 解法 + + 時間計算量 + + 空間計算量 + + 最悪ケース(空間) +
+ 再帰(DFS) + + O(n) + + O(h) + + O(n)(一直線の木) +
+ 反復(BFS with deque) + + O(n) + + O(w) + + O(n)(完全二分木最下段) +
+
+ + +
+

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

+

+ 時間計算量 O(n):どちらの解法も、各ノードを最大1回ずつ訪問します。n個のノードがある木では、最大 + n/2 ペアを比較するため O(n/2) = O(n) です。
+ 空間計算量(再帰)O(h):h + は木の高さです。再帰呼び出しは「コールスタック(=関数の呼び出し履歴を記録するメモリ領域)」に積み重なります。最悪ケースの一直線の木では + h = n になるため O(n) です。バランスの取れた木では h = log n になります。
+ 空間計算量(反復)O(w):w は木の最大幅です。deque + には同じ深さのペアが格納されるため、完全二分木の最下段(= n/2 + 個のノード)が最悪ケースで O(n) になります。 +

+
+ + +
+

+ ⚡ 再帰 vs 反復:どちらを選ぶか +

+
+
+

🌀 再帰版を選ぶ場面

+
    +
  • コードの読みやすさを優先したい
  • +
  • バランスの取れた木(深さがスタック上限に達しない)
  • +
  • 「鏡写しの定義」をそのままコードに落としたい
  • +
+
+
+

🔁 反復版を選ぶ場面

+
    +
  • 深さが約1000前後になる退化した木(深さ ≈ スタック上限)では再帰が危険になるため反復版を検討する
  • +
  • スタックオーバーフローを完全に回避したい
  • +
  • 本番環境など安全性を最優先にしたい
  • +
+
+
+
+
+ + +
+

+ 📖 用語集 +

+

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

+
+
+ + BFS(幅優先探索) + +
+ Breadth-First Search + の略。同じ深さのノードを左から右へ横断していく探索方法。キューと相性が良い。 + この問題の反復版で使用。対義語はDFS(深さ優先探索)。 +
+
+ +
+ + DFS(深さ優先探索) + +
+ Depth-First Search + の略。根から葉へ向かって深く潜っていく探索方法。再帰と相性が良い。 + この問題の再帰版で使用。木を「縦に」探索するイメージ。 +
+
+ +
+ + collections.deque(デック) + +
+ Python + 標準ライブラリの両端キュー。「前からも後ろからも出し入れできる箱」のようなデータ構造。 + popleft() + がO(1)で高速(list.pop(0) + はO(n))。 C言語で実装されているため Pure Python より大幅に高速。 +
+
+ +
+ + コールスタック + +
+ 関数が呼び出されるたびに積み重なる「呼び出し履歴」のメモリ領域。 + 再帰呼び出しが深くなるほど消費するメモリが増える。 + Pythonはデフォルトで1000回の再帰呼び出しまで許容(sys.getrecursionlimit())。 +
+
+ +
+ + 基底条件 + +
+ 再帰の終了条件。「これ以上再帰しない」と判断して値を返す条件。 + 基底条件がないと無限再帰(スタックオーバーフロー)になる。 + この問題では「両方None → True」「片方None → False」の2つが基底条件。 +
+
+ +
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す仕組み。木の探索に非常に相性が良い。 + 「鏡写しかどうか」の定義(= + 値が同じで、さらに外側・内側ペアも鏡写し)をそのままコードに書き下せる。 + ロシアのマトリョーシカ人形のように「大きい問題を小さい同じ問題に分解する」イメージ。 +
+
+ +
+ + 短絡評価(Short-circuit + Evaluation) + +
+ A and B + でAが + False + なら、Bをまったく評価せず即座に + False + を返す仕組み。 + この問題では値が違えば外側・内側ペアの再帰呼び出しが省略される。 + 非対称が早い段階で分かるほど効果が大きい最適化テクニック。 +
+
+ +
+ + 二分木(Binary Tree) + +
+ 各ノードが最大2つの子(left と right)を持つ木構造のデータ構造。 + 家系図に例えると、各人物が最大2人の子供を持てる構造。 LeetCodeでは + TreeNode + クラスで表現され、.val, .left, + .right + の3つの属性を持つ。 +
+
+ +
+ + FIFO(先入れ先出し) + +
+ First In First Out + の略。最初に入れたものを最初に取り出すキューの動作原則。 + コンビニのおにぎり棚に例えると、奥から補充して手前から取り出す仕組み(賞味期限管理)と同じ。 + deque の + append() + で末尾追加、popleft() + で先頭取り出しにより実現する。 +
+
+ +
+ + RecursionError + +
+ Pythonの再帰呼び出し上限(デフォルト1000回)を超えたときに発生するエラー。 + 深さ1000の一直線の木で再帰版を使うと発生する可能性がある。 + sys.setrecursionlimit(n) + で上限を変更可能。または反復版を使うことで根本的に回避できる。 +
+
+
+
+ + + +
+ + + + + + + + + + + + + diff --git a/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Go.md b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Go.md new file mode 100644 index 00000000..0f1d46c2 --- /dev/null +++ b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Go.md @@ -0,0 +1,420 @@ +# 🌳 Symmetric Tree — Go 完全解説 + +--- + +## 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は一言で言うと「**二分木が中心軸を境に鏡写しになっているかを判定する問題**」です。 +> Goで解く際の特徴として、木のノードは `*TreeNode`(ポインタ型)で渡されます。`nil` チェックを怠ると実行時パニック(Go の強制終了)が発生するため、**ポインタへのアクセス前に必ず `nil` チェック**を行う設計が重要です。 + +--- + +### 競技プログラミング視点 + +- **制約分析**:ノード数 1〜1000 と小さいため、全ノードを1回訪問する O(n) で十分 +- **最速手法**:再帰 DFS(深さ優先探索)。関数呼び出しコストは低く、ノード数 1000 では問題なし +- **Go最適化**:`and` に相当する `&&` の短絡評価(左辺が `false` なら右辺を評価しない)で無駄な再帰を早期カット + +### 業務開発視点 + +- **型安全設計**:`*TreeNode` のポインタ型を正しく扱い、`nil` デリファレンス(= `nil` のポインタ経由でフィールドにアクセスしてパニックになること)を防ぐ +- **エラーハンドリング**:LeetCode の問題制約(ノード数 ≥ 1)が保証されているため `error` 戻り値は不要。ただし業務コードではバリデーションを追加する +- **可読性**:ヘルパー関数 `isMirror` を分離し責務(=その関数が担う役割)を明確にする + +### Go特有分析 + +- **ポインタ型**:`*TreeNode` は nil を持ち得るポインタ。Go では nil ポインタへのアクセスは即パニックになるため、`left == nil && right == nil` の順でチェックする +- **再帰 vs 反復**:再帰は読みやすく実装コストが低い。反復は `container/list` や `slice` をスタック代わりに使い、ゴルーチンのスタック深度制限(デフォルト 1GB まで自動拡張)を意識しなくて済む +- **エスケープ解析**:`isMirror` はローカル関数として定義するとクロージャ経由でヒープに逃げる場合がある。トップレベル関数として定義することでスタック割り当てを期待できる + +> 📖 **このセクションで登場した用語** +> +> - **ポインタ型 `*T`**:値が格納されているメモリアドレスを保持する型。Go では `nil`(無効なアドレス)も取り得る +> - **nil デリファレンス**:`nil` のポインタ経由でフィールドやメソッドにアクセスしようとすること。Go ではパニック(強制終了)になる +> - **パニック**:Go で回復不能なエラーが発生した際の強制終了。インデックス範囲外・nil デリファレンスなどで発生する +> - **エスケープ解析**:変数をスタック(高速・自動解放)に置くかヒープ(低速・GC管理)に置くかをコンパイラが判断する仕組み + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」に加え、Go では「アロケーション回数(ヒープへのメモリ確保)」も重要な評価軸です。 + +| アプローチ | 時間計算量 | 空間計算量 | Go実装コスト | 可読性 | 標準ライブラリ活用 | 備考 | +| -------------------------- | ---------- | ---------- | ------------ | ------ | ------------------ | -------------------------------- | +| **A: 再帰(DFS)** | O(n) | O(h)※ | 低 | ★★★ | なし | 定義に近く直感的 | +| **B: 反復(slice queue)** | O(n) | O(w)※※ | 中 | ★★☆ | なし | ゴルーチンスタックを使わない | +| **C: 配列シリアライズ** | O(n) | O(n) | 高 | ★☆☆ | なし | スライス生成アロケーションが多発 | + +> ※ **h = 木の高さ**。最悪 O(n)(一直線の木)、平均 O(log n)(バランス木) +> ※※ **w = 木の最大幅**。最悪 O(n)(完全二分木の最下段) + +--- + +> 💡 **Big-O記法の読み方**(初学者向け) +> | 記法 | 意味 | 直感的イメージ | +> | ---- | ---- | ---- | +> | `O(1)` | 入力サイズによらず一定 | 辞書の直引き | +> | `O(n)` | 入力に比例して増加 | リストを端から順に読む | +> | `O(n log n)` | n より少し多く増加 | ソートアルゴリズムの典型 | +> | `O(n²)` | 入力の2乗で増加 | 二重ループの総当たり | + +--- + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **再アロケーション**:スライスの容量が足りなくなったとき、より大きいメモリ領域に全要素をコピーする操作 +> - **DFS(深さ優先探索)**:根から葉へ向かって深く潜っていく探索方法。再帰と相性が良い + +--- + +## 3. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:「なぜこれを選ばなかったか」を対比で説明します。 + +- **選択したアプローチ**:**A(再帰)をメイン、B(反復)をフォローアップ**として両方実装 +- **理由**: + - **C(シリアライズ)は選ばない**:`[]*TreeNode` スライスの生成でアロケーションが多発し、nil 位置のエンコードも複雑になるため + - **A(再帰)を選ぶ**:「鏡写しの定義」がそのままコードになる直感的な構造。Go のゴルーチンスタックは動的に拡張(最大 1GB)されるため、ノード数 1000 程度では問題なし + - **B(反復)もフォローアップで実装**:スタック深度を明示的にコントロールしたい場面(深さ 10 万超の木など)への対応として有用 + +- **Go特有の最適化ポイント**: + - トップレベル関数 `isMirror` として定義することで、エスケープ解析がクロージャより有利になる + - `&&` の短絡評価(左辺が `false` なら右辺を評価しない)で余分な再帰呼び出しを省略 + - ポインタ比較 `left == nil` は Go の同一性チェックで O(1)、`==` より高速ではないが nil チェックの慣用句 + +> 📖 **このセクションで登場した用語** +> +> - **ゴルーチンスタック**:各ゴルーチンが持つ実行スタック。Go 1.4 以降は 2KB から始まり必要に応じて自動拡張される(最大 1GB) +> - **短絡評価**:`A && B` で A が `false` なら B を評価せず即 `false` を返す仕組み。不要な処理を省ける +> - **トレードオフ**:何かを得ると何かを失う関係。再帰は読みやすいがスタックを消費する、など + +--- + +## 4. 実装パターン + +> 💡 **コードの骨格(全体像)** +> +> 1. `isSymmetric`:エントリーポイント。`root` が `nil` なら即 `true` +> 2. `isMirror`(再帰版):2つのノードが鏡写しかを3ケースで再帰確認するヘルパー +> 3. `isSymmetricIterative`(反復版):`[]*TreeNode` スライスをキュー代わりに使って対称ペアを順番に確認 + +--- + +### 【業務開発版を使う場面】 + +チームで長期間メンテナンスするプロダクションコードに向きます。godoc コメント・エラーハンドリング設計・`go vet` / `golangci-lint` 対応を徹底し、後から読んだ人が意図を理解しやすい構造になっています。 + +```go +// Runtime 0 ms +// Beats 100.00% +// Memory 4.74 MB +// Beats 76.11% + +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +// isSymmetric は二分木 root が中心軸を境に鏡写し(対称)かどうかを返す。 +// +// なぜ root が nil のときに true を返すか: +// 空の木は定義上「対称」と見なせるため。LeetCode の制約ではノード数 ≥ 1 だが、 +// *TreeNode はポインタ型なので nil になり得る。型レベルで安全に扱うために先に確認する。 +// +// Time Complexity: O(n) — 全ノードを最大1回ずつ訪問する +// Space Complexity: O(h) — 再帰呼び出しのスタック深度(h = 木の高さ) +func isSymmetric(root *TreeNode) bool { + // root が nil(空の木)なら対称 + if root == nil { + return true + } + // root は中心軸なので比較しない。左子と右子を最初の鏡ペアとして渡す + return isMirror(root.Left, root.Right) +} + +// isMirror は2つのノード left・right が「鏡写し」の関係かどうかを再帰的に確認する。 +// +// 鏡写しの3条件(すべて満たす必要がある): +// 1. left.Val == right.Val (値が同じ) +// 2. isMirror(left.Left, right.Right) — 外側ペアが鏡写し +// 3. isMirror(left.Right, right.Left) — 内側ペアが鏡写し +// +// なぜトップレベル関数(メソッドでなく)にするか: +// クロージャにするとヒープへのエスケープが発生しやすくなるため、 +// トップレベル関数にしてコンパイラのインライン化・スタック割り当てを期待する。 +// +// Time Complexity: O(n) +// Space Complexity: O(h) +func isMirror(left, right *TreeNode) bool { + // ── 基底条件① ── + // 両方 nil なら「空同士」= 対称 → true + // 例: 葉ノードの子(存在しない位置)同士を比較した場合 + if left == nil && right == nil { + return true + } + + // ── 基底条件② ── + // 片方だけ nil なら「一方だけ枝がある」= 非対称 → false + // なぜ left == nil の後に right へアクセスするか: + // Go ではポインタが nil のまま .Val にアクセスするとパニックになる。 + // 基底条件①で「両方 nil」を除外済みなので、ここに来るのは「ちょうど1つが nil」の場合。 + if left == nil || right == nil { + return false + } + + // ── 再帰ステップ ── + // 両方 nil でない = 両方ノードが存在する場合。 + // && の短絡評価(左辺が false なら右辺を評価しない)を活かす: + // 値が違えば外側・内側ペアの isMirror は呼ばれない → 無駄な再帰を省く + return left.Val == right.Val && // 条件1: 値が同じか? + isMirror(left.Left, right.Right) && // 条件2: 外側ペア(左の左子 ↔ 右の右子) + isMirror(left.Right, right.Left) // 条件3: 内側ペア(左の右子 ↔ 右の左子) +} + +// isSymmetricIterative は反復(キュー)を使って二分木が対称かどうかを確認する。 +// +// なぜ反復版も提供するか: +// Go のゴルーチンスタックは自動拡張されるが、深さが数万を超える木では +// スタックメモリを明示的にコントロールしたい場面がある。 +// 反復版はヒープ上のスライスをキューとして使うため、スタック消費がほぼゼロ。 +// +// deque(両端キュー)の代わりにスライスを使う理由: +// Go 標準ライブラリに両端キューはない。`container/list` は doubly-linked list だが +// ポインタのアロケーションが多発する。スライスを先頭から消費するだけなら +// re-slice(`queue = queue[2:]`)が最も軽量。 +// +// Time Complexity: O(n) +// Space Complexity: O(w) — w = 木の最大幅(キューに格納されるペアの最大数) +func isSymmetricIterative(root *TreeNode) bool { + // root が nil なら対称 + if root == nil { + return true + } + + // queue はノードのペアを交互に格納するスライス。 + // [left1, right1, left2, right2, ...] の形式で2つずつ取り出す。 + // なぜプリアロケーションするか: + // make の第3引数でキャパシティを指定することで、 + // append による再アロケーション(=メモリコピー)を最初から防ぐ。 + // 最大幅 w は最悪 n/2 ≈ 500 ペアなので、初期キャパシティ 2 は小さいが + // ノード数 1000 程度では再アロケーションのコストは軽微。 + queue := make([]*TreeNode, 0, 2) + + // 最初のペア: root の左子と右子をキューに追加 + queue = append(queue, root.Left, root.Right) + + // キューが空になるまで(= 全ペアの確認が終わるまで)ループ + for len(queue) > 0 { + // キューの先頭から2つ(= 1ペア)を取り出す。 + // re-slice(queue = queue[2:])は新しいスライスを作らず、 + // 内部ポインタを2つ進めるだけなのでアロケーションが発生しない。 + left, right := queue[0], queue[1] + queue = queue[2:] + + // ケース①: 両方 nil → このペアはOK → 次のペアへ + if left == nil && right == nil { + continue + } + + // ケース②: 片方だけ nil → 非対称確定 + if left == nil || right == nil { + return false + } + + // ケース③: 値が違う → 非対称確定 + if left.Val != right.Val { + return false + } + + // 次に確認すべき「鏡ペア」をキューに追加する。 + // 外側ペア: 左の左子(left.Left) ↔ 右の右子(right.Right) + // 内側ペア: 左の右子(left.Right) ↔ 右の左子(right.Left) + queue = append(queue, left.Left, right.Right) // 外側ペア + queue = append(queue, left.Right, right.Left) // 内側ペア + } + + // 全ペアをパスしたので対称 + return true +} +``` + +--- + +> 💡 **再帰版のトレース** — Example 1: `root = [1,2,2,3,4,4,3]` + +``` + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + +isSymmetric(root=&{1, ...}) + → root != nil → isMirror(left=&{2,...}, right=&{2,...}) + +isMirror(left=&{2}, right=&{2}) + → 両方 nil でない、片方 nil でない + → 2 == 2 ✅ + → isMirror(left.Left=&{3}, right.Right=&{3}) ← 外側ペア + + isMirror(&{3}, &{3}) + → 3 == 3 ✅ + → isMirror(nil, nil) → true ✅ + → isMirror(nil, nil) → true ✅ + → true ✅ + + → isMirror(left.Right=&{4}, right.Left=&{4}) ← 内側ペア + + isMirror(&{4}, &{4}) + → 4 == 4 ✅ + → isMirror(nil, nil) → true ✅ + → isMirror(nil, nil) → true ✅ + → true ✅ + +最終結果: true && true && true = true ✅ +``` + +``` +Example 2: root = [1,2,2,null,3,null,3] + +isMirror(left=&{2}, right=&{2}) + → 2 == 2 ✅ + → isMirror(left.Left=nil, right.Right=&{3}) ← 外側ペア + → 片方だけ nil → false ❌ + → && の短絡評価: 外側ペアが false → 内側ペアの isMirror は呼ばれない + +最終結果: false ❌ +``` + +--- + +### 【競技プログラミング版を使う場面】 + +LeetCode などで制限時間内に正解を出すことが目的のコードに向きます。godoc コメントを最小限にし、実行速度・コードの短さを優先した書き方です。 + +```go +func isSymmetric(root *TreeNode) bool { + // root が nil なら対称。nil チェックを省くとパニックになるため必須 + if root == nil { + return true + } + // ローカル変数に isMirror の関数リテラルを代入する。 + // なぜローカル変数か:再帰関数を自分自身で呼ぶために変数に束縛する必要がある。 + // ただし Go ではクロージャはヒープにエスケープしやすいため、 + // 本番コードではトップレベル関数に切り出す方が望ましい。 + var mirror func(*TreeNode, *TreeNode) bool + mirror = func(l, r *TreeNode) bool { + // 基底条件①: 両方 nil → 対称 + if l == nil && r == nil { + return true + } + // 基底条件②: 片方 nil → 非対称 + if l == nil || r == nil { + return false + } + // 再帰ステップ: 値一致 + 外側・内側ペアの再帰確認 + return l.Val == r.Val && + mirror(l.Left, r.Right) && + mirror(l.Right, r.Left) + } + return mirror(root.Left, root.Right) +} +``` + +--- + +> 💡 **反復版のトレース** — Example 1: `root = [1,2,2,3,4,4,3]` + +``` +初期状態: queue = [&{2}, &{2}] + +── ループ1回目 ── +取り出し: left=&{2}, right=&{2} + 2 == 2 ✅ + 外側ペア追加: [&{3}, &{3}] + 内側ペア追加: [&{4}, &{4}] +queue = [&{3}, &{3}, &{4}, &{4}] + +── ループ2回目 ── +取り出し: left=&{3}, right=&{3} + 3 == 3 ✅ + 外側: [nil, nil] 追加 + 内側: [nil, nil] 追加 +queue = [&{4}, &{4}, nil, nil, nil, nil] + +── ループ3回目 ── +取り出し: left=&{4}, right=&{4} + 4 == 4 ✅ → [nil, nil, nil, nil] 追加 +queue = [nil, nil, nil, nil, nil, nil, nil, nil] + +── ループ4〜7回目 ── +(nil, nil) → continue(スキップ)× 4回 + +len(queue) == 0 → ループ終了 +→ return true ✅ +``` + +--- + +## 5. 検証 + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・特殊な構造」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。 + +| ケース | 入力 | 期待出力 | なぜ問題になりうるか | +| ------------------------ | ------------------------ | ---------- | ---------------------------------------------------------------------------------------- | +| **ノード1個** | `[1]` | `true` | `root.Left == nil && root.Right == nil` → `isMirror(nil, nil)` が基底条件①を正しく返すか | +| **対称** | `[1,2,2,3,4,4,3]` | `true` | 全条件を満たす正常ケース | +| **非対称(位置ずれ)** | `[1,2,2,null,3,null,3]` | `false` | 片方だけ nil のケース。基底条件②が正しく機能するか | +| **値は同じ・構造が違う** | `[1,2,2,null,3,3,null]` | `true` | null の位置が内側に揃っている(対称な構造) | +| **全て同じ値** | `[1,1,1,1,1,1,1]` | `true` | 値比較だけでなく構造比較も正しく行われるか | +| **一直線(左偏り)** | `[1,2,null,3]` | `false` | 右サブツリーが空。深さが非対称 | +| **最大ノード数** | ノード数1000の完全二分木 | 構造に依存 | 再帰深度が最大 log₂(1000) ≈ 10 程度。スタックは問題なし | +| **値が負の数** | `[0,-1,-1]` | `true` | 制約内(-100〜100)の負値を正しく比較できるか | + +### `go vet` / `golangci-lint` チェックポイント + +```go +// ✅ これらのチェックを通じてコード品質を担保する + +// 1. nilポインタデリファレンス防止 +// → left == nil && right == nil の順でチェックしているか +// → left.Val アクセス前に left != nil が保証されているか + +// 2. 未使用変数エラー防止 +// → すべての変数が使用されているか(使わない場合は _ で明示) + +// 3. 関数リテラルのクロージャキャプチャ +// → var mirror func(...) bool と宣言してから代入する2段階方式を守っているか +``` + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のスライス・要素1つ・最大サイズ入力など、境界的な条件の入力 +> - **パニック(panic)**:Go で回復不能なエラーが発生した際の強制終了。nil デリファレンスなどで発生する +> - **`go vet`**:コンパイルは通るが怪しいコードを検出するツール。未使用変数・nil デリファレンスリスクなどを警告する + +--- + +## 計算量まとめ + +| 解法 | 時間計算量 | 空間計算量 | アロケーション数 | +| ------------------- | ---------- | --------------------- | ------------------------------ | +| 再帰(DFS) | O(n) | O(h) — h は木の高さ | ほぼ 0(スタックフレームのみ) | +| 反復(slice queue) | O(n) | O(w) — w は木の最大幅 | スライス拡張時のみ | + +どちらも全ノードを最大1回ずつ訪問するため **O(n)** です。空間計算量は木の形状によりますが、最悪ケースはどちらも **O(n)** です(一直線の木 / 完全二分木の最下段にノードが集中する場合)。 + +> 📖 **最終用語まとめ** +> +> - **ポインタレシーバ `*T`**:メソッドのレシーバに `*` を付けること。スライス・フィールド自体を変更したい場合に必要 +> - **再アロケーション**:スライスの容量が足りなくなったとき、より大きいメモリ領域に全要素をコピーする操作 +> - **短絡評価**:`A && B` で A が `false` なら B を評価せず即 `false` を返す仕組み +> - **インライン化**:コンパイラが小さな関数の呼び出しをその中身に置き換える最適化。関数呼び出しのオーバーヘッドがなくなる +> - **クロージャ**:自分が定義されたスコープの変数を「覚えている」関数。Go では `var f func(...); f = func(...){ f(...) }` で再帰クロージャを作れるが、ヒープにエスケープしやすい diff --git a/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Python.md b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Python.md new file mode 100644 index 00000000..c0f0df64 --- /dev/null +++ b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Python.md @@ -0,0 +1,421 @@ +# 🌳 Symmetric Tree — Python 完全解説 + +--- + +## 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は一言で言うと「**二分木が中心軸に対して鏡写しになっているかを確認する問題**」です。 +> Pythonで解く際の特徴として、`TreeNode` はPure Pythonクラスのため属性アクセスは軽量ですが、**再帰呼び出しはCPythonのデフォルトスタック上限(`sys.getrecursionlimit()` = 1000)に近づく可能性があります**。ノード数が最大1000なので、最悪ケースの一直線の木では再帰深度が1000に達しうることを頭に入れておく必要があります。 + +--- + +### 🖼️ まず「対称」を視覚的に理解する + +``` +【対称な木 ✅】 【非対称な木 ❌】 + + 1 1 + / \ / \ + 2 2 2 2 + / \ / \ \ \ + 3 4 4 3 3 3 + +中心軸を境に 右の2にleftがなく +左右が完全に鏡写し rightしかないので非対称 +``` + +「鏡写し」の条件を3つに分解すると: + +1. 左右の値が同じか? +2. 左の「左子」↔ 右の「右子」が鏡写しか?(外側ペア) +3. 左の「右子」↔ 右の「左子」が鏡写しか?(内側ペア) + +--- + +### 競技プログラミング視点 + +- **制約分析**:ノード数1〜1000と小さいため、全ノードを1回訪問する O(n) で十分 +- **最速手法**:再帰DFS。再帰は可読性が高いですが、退化した木(最悪ケース)では再帰深度がPythonの再帰制限(デフォルト1000)に近づく可能性があるため、安全性を重視するなら反復版を推奨します +- **CPython最適化**:`and` の短絡評価(=左辺がFalseなら右辺を評価しない仕組み)で無駄な再帰を早期カット + +### 業務開発視点 + +- **型安全設計**:docstringの `:type:` / `:rtype:` で型情報を明示(Python 2スタイルテンプレートのため) +- **エラーハンドリング**:`None` チェックをパターンマッチングで網羅的に処理 +- **可読性**:ヘルパーメソッド `_is_mirror` を分離し、責務(=その関数が担う役割)を明確に分ける + +### Python特有分析 + +- **データ構造選択**:反復版は `collections.deque` を使う(`list.pop(0)` はO(n)だが `deque.popleft()` はO(1)) +- **再帰 vs 反復**:再帰はコードが直感的。反復は `deque` でスタック深度制限を回避できる +- **CPython最適化**:`deque` はC実装のため `list` の先頭削除より大幅に高速 + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、`deque`など標準ライブラリの多くがC実装のため高速 +> - **短絡評価**:`A and B` でAがFalseなら、Bをまったく評価しない仕組み。無駄な処理を省ける +> - **スタック深度制限**:Pythonの再帰呼び出しはデフォルトで1000回まで。それを超えると `RecursionError` が発生する +> - **O(n) vs O(1)**:`list.pop(0)` はリスト全体をずらすのでO(n)。`deque.popleft()` は先頭を直接取り出すのでO(1) + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。問題文のフォローアップが「再帰・反復の両方を実装せよ」なので、両方解説します。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ----------------------- | ---------- | ---------- | ---------------- | ------ | ---------------------------- | ------------- | ------------------------------------ | +| **A: 再帰(DFS)** | O(n) | O(h)※ | 低 | ★★★ | なし | 適 | コードが「定義そのもの」で読みやすい | +| **B: 反復(deque)** | O(n) | O(w)※※ | 中 | ★★☆ | `collections.deque`(C実装) | 適 | RecursionError回避に強い | +| **C: 配列シリアライズ** | O(n) | O(n) | 高 | ★☆☆ | なし | 不適 | list生成が多く非効率 | + +> ※ **h = 木の高さ**。最悪O(n)(一直線の木)、平均O(log n)(バランス木) +> ※※ **w = 木の最大幅**。最悪O(n)(完全二分木の最下段にノードが集中) + +- **選択理由**:A(再帰)は「鏡写しの定義」がコードに直接現れ可読性が最高。バランス木であれば問題ないが、一直線の木(最悪ケース)ではCPythonのデフォルト再帰上限(約1000)に達しRecursionErrorになるリスクがある(冒頭の説明や用語解説、反復版のコメントも参照)。フォローアップとしてB(反復)も実装 +- **Python最適化戦略**:反復版には `deque`(C実装・`popleft()` がO(1))を採用。`list.pop(0)` はO(n)のため不可 +- **トレードオフ**:再帰は読みやすいがスタック深度に依存。反復は少し複雑になるがスタック無制限 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n log n)`:入力が2倍になると、処理は約2倍強になる(ソートアルゴリズムに多い) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループに多い) + +--- + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **トレードオフ**:何かを得ると何かを失う関係。再帰は読みやすいがスタックを消費する、など +> - **C実装**:Pythonコードではなく、内部でC言語で実装された関数。Pure Pythonより大幅に高速 + +--- + +## 3. 実装パターン + +> 💡 **コードの骨格(全体像)** +> +> 1. `isSymmetric`:エントリーポイント。`root` を受け取りヘルパーに渡す +> 2. `_is_mirror`(再帰版):2つのノードが鏡写しかを再帰で確認するヘルパー +> 3. `isSymmetricIterative`(反復版):`deque` でペアを順番に確認する + +--- + +### 【業務開発版を使う場面】 + +チームで長期間メンテナンスするプロダクションコードに向きます。 +docstringを充実させ、エラーの原因が分かりやすく後から読んだ人が理解しやすい構造になっています。 + +```python +from collections import deque + + +# Definition for a binary tree node. +# class TreeNode(object): +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right +class Solution(object): + """ + Symmetric Tree 解決クラス(業務開発版) + + 二分木が中心軸に対して鏡写しかどうかを判定する。 + 再帰(DFS)と反復(BFS with deque)の2パターンを提供する。 + """ + + # ============================================= + # 解法①: 再帰版(メイン) + # ============================================= + def isSymmetric(self, root): + """ + 二分木が鏡写し(対称)かどうかを再帰で判定する。 + + :type root: Optional[TreeNode] + :rtype: bool + + Time: O(n) - 全ノードを1回ずつ訪問する + Space: O(h) - 再帰スタックの深さ(h = 木の高さ) + """ + # rootがNone(空の木)は定義上「対称」とする + # LeetCodeの制約では最低1ノードあるが、型上Noneがあり得るため処理する + if root is None: + return True + + # rootの左子と右子が「鏡写し」かをヘルパーで確認する + # rootそのものは中心軸なので比較対象にならない + return self._is_mirror(root.left, root.right) + + def _is_mirror(self, left, right): + """ + 2つのノードが「鏡写し」の関係かどうかを再帰的に確認するヘルパー。 + + 鏡写しの条件(3つ全てを満たす必要がある): + 1. 左右の値が等しい + 2. 左の「左子」と右の「右子」が鏡写し(外側ペア) + 3. 左の「右子」と右の「左子」が鏡写し(内側ペア) + + :type left: Optional[TreeNode] + :type right: Optional[TreeNode] + :rtype: bool + """ + # ケース①: 両方Noneなら「空同士」= 対称 → True + # 例: 葉ノードの子(存在しない位置)同士を比較した場合 + if left is None and right is None: + return True + + # ケース②: 片方だけNoneなら「一方だけ枝がある」= 非対称 → False + # `is None` を使う理由: `== None` より高速(同一性チェック)かつ + # Pythonの慣用的な書き方(イディオム) + if left is None or right is None: + return False + + # ケース③: 両方ノードが存在する場合 + # 3条件を and で繋いで確認する。 + # and の短絡評価(= 左辺がFalseなら右辺を評価しない仕組み)により + # 値が違った時点で即座にFalseを返し、無駄な再帰を省く + return ( + left.val == right.val # 条件1: 値が同じか? + and self._is_mirror(left.left, right.right) # 条件2: 外側ペア + and self._is_mirror(left.right, right.left) # 条件3: 内側ペア + ) + + # ============================================= + # 解法②: 反復版(フォローアップ) + # ============================================= + def isSymmetricIterative(self, root): + """ + 二分木が鏡写しかどうかを反復(deque使用)で判定する。 + 再帰の深さ制限(RecursionError)が心配な場合に使う代替実装。 + + dequeを使う理由: + list.pop(0) は O(n)(先頭削除のたびに全要素をずらす) + deque.popleft() は O(1)(C実装の双方向リストのため高速) + + :type root: Optional[TreeNode] + :rtype: bool + + Time: O(n) - 全ノードを1回ずつ訪問する + Space: O(w) - wは木の最大幅(dequeに入るペアの最大数) + """ + # rootがNoneなら空の木 → 対称 + if root is None: + return True + + # dequeに「比較すべきノードのペア」をタプルで格納する。 + # タプル (左ノード, 右ノード) を順番に取り出して比較していく。 + # deque(デック)= 両端開きの箱: popleft() がO(1) でlistより高速 + queue = deque() + + # 最初のペア: rootの左子と右子をキューに追加 + queue.append((root.left, root.right)) + + # キューが空になるまで(= 全ペアの確認が終わるまで)繰り返す + while queue: + # popleft() でキューの先頭からペアを取り出す + # FIFO(先入れ先出し)= 最初に追加したペアから順番に処理する + left, right = queue.popleft() + + # ケース①: 両方NoneならこのペアはOK → 次のペアへ + if left is None and right is None: + continue + + # ケース②: 片方だけNone → 非対称確定 + if left is None or right is None: + return False + + # ケース③: 値が違う → 非対称確定 + if left.val != right.val: + return False + + # 次に確認すべき「鏡ペア」をキューに積む + queue.append((left.left, right.right)) # 外側ペア + queue.append((left.right, right.left)) # 内側ペア + + # 全ペアをパスしたので対称 + return True +``` + +--- + +> 💡 **再帰版のトレース** — Example 1: `[1,2,2,3,4,4,3]` + +``` + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + +isSymmetric(root=1) +└─ _is_mirror(left=Node(2), right=Node(2)) + ├─ 2 == 2 ✅ + ├─ _is_mirror(left=Node(3), right=Node(3)) ← 外側ペア + │ ├─ 3 == 3 ✅ + │ ├─ _is_mirror(None, None) → True ✅ + │ └─ _is_mirror(None, None) → True ✅ + │ → True ✅ + └─ _is_mirror(left=Node(4), right=Node(4)) ← 内側ペア + ├─ 4 == 4 ✅ + ├─ _is_mirror(None, None) → True ✅ + └─ _is_mirror(None, None) → True ✅ + → True ✅ +→ True ✅ 最終結果: True + +Example 2: [1,2,2,null,3,null,3] +_is_mirror(left=Node(2), right=Node(2)) + ├─ 2 == 2 ✅ + └─ _is_mirror(left=None, right=Node(3)) ← 外側ペア + → 片方だけNone → False ❌ ← 短絡評価でここで即終了! +→ False ❌ 最終結果: False +``` + +> 💡 **反復版のトレース** — Example 1: `[1,2,2,3,4,4,3]` + +``` +初期状態: queue = [(Node(2), Node(2))] + +─── ループ1回目 ─── +取り出し: (Node(2), Node(2)) + left.val=2, right.val=2 → 2==2 ✅ + 外側ペア追加: (Node(3), Node(3)) + 内側ペア追加: (Node(4), Node(4)) +queue = [(Node(3),Node(3)), (Node(4),Node(4))] + +─── ループ2回目 ─── +取り出し: (Node(3), Node(3)) + 3==3 ✅ + 追加: (None,None) × 2 +queue = [(Node(4),Node(4)), (None,None), (None,None)] + +─── ループ3回目 ─── +取り出し: (Node(4), Node(4)) + 4==4 ✅ → (None,None) × 2 追加 +queue = [(None,None) × 4] + +─── ループ4〜7回目 ─── +(None, None) → continue(スキップ)× 4回 + +queue = [] → while が False → ループ終了 +→ return True ✅ +``` + +--- + +### 【競技プログラミング版を使う場面】 + +LeetCodeなどで制限時間内に正解を出すことが目的のコードに向きます。 +docstringを最小限にし、実行速度・コードの短さを優先した書き方になっています。 + +```python +from collections import deque + + +class Solution(object): + def isSymmetric(self, root): + """ + :type root: Optional[TreeNode] + :rtype: bool + """ + # ローカル関数にすることで self 参照のオーバーヘッドを削減する + # (毎回 self._is_mirror と辞書引きする分のコストを省く微小最適化) + def is_mirror(left, right): + # 両方Noneなら対称 + if left is None and right is None: + return True + # 片方だけNoneなら非対称 + if left is None or right is None: + return False + # 値比較 + 外側・内側ペアを再帰確認(and の短絡評価を活用) + return ( + left.val == right.val + and is_mirror(left.left, right.right) + and is_mirror(left.right, right.left) + ) + + # rootがNoneなら対称(空の木) + # `or` の短絡評価: rootがNoneなら is_mirror を呼ばずに True を返す + return root is None or is_mirror(root.left, root.right) +``` + +--- + +## 4. 検証 + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、 +> 通常とは異なる境界的な入力のことです。エッジケースのテストは、アルゴリズムが +> "ふつうの入力"だけでなく"極端な入力"でも正しく動くかを確かめるためのものです。 + +| ケース | 入力 | 期待出力 | 理由 | +| -------------------- | ----------------------- | -------- | ------------------------- | +| 基本ケース(対称) | `[1,2,2,3,4,4,3]` | `True` | 左右が完全に鏡写し | +| 基本ケース(非対称) | `[1,2,2,null,3,null,3]` | `False` | 内側の子の位置が非対称 | +| ノード1個 | `[1]` | `True` | 左右の子が両方None → 対称 | +| 値は同じ・構造が違う | `[1,2,2,null,3,3,null]` | `True` | 鏡写しの構造を満たす | +| 全て同じ値 | `[1,1,1,1,1,1,1]` | `True` | 値もノード数も対称 | +| 一直線(左偏り) | `[1,2,null,3]` | `False` | 右サブツリーが存在しない | + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **静的解析**:プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法 + +--- + +## 計算量まとめ + +| 解法 | 時間計算量 | 空間計算量 | +| ---------------------- | ---------- | -------------------- | +| 再帰(DFS) | O(n) | O(h) — hは木の高さ | +| 反復(BFS with deque) | O(n) | O(w) — wは木の最大幅 | + +どちらも全ノードを1回ずつ訪問するため **O(n)** です。 +空間計算量は木の形状によりますが、最悪ケースはどちらも **O(n)** です(一直線の木 / 完全二分木の最下段にノードが集中する場合)。 + +--- + +## Python特有の追加考慮事項 + +### このテンプレート(`class Solution(object)`)のルール + +``` +class Solution(object): ← Python 2 スタイル(object継承) + def isSymmetric(self, root): + """ + :type root: Optional[TreeNode] ← 型はdocstringに書く + :rtype: bool ← 戻り値の型もdocstringに + """ + +✅ 使える: from collections import deque(標準ライブラリ) +❌ 使えない: 引数の型アノテーション(root: Optional[TreeNode]) +❌ 使えない: 戻り値アノテーション(-> bool) +❌ 不要: from typing import Optional +❌ 禁止: from __future__ import annotations(先頭行にしか置けないため) +``` + +### `collections.deque` の仕組み + +``` +list.pop(0) の場合(O(n)): + [1, 2, 3, 4, 5] + ↑ 取り出す + → [_, 2, 3, 4, 5] → 全要素を左に1つずらす → [2, 3, 4, 5] + ノード数が増えるほど遅くなる! + +deque.popleft() の場合(O(1)): + 双方向リスト: 先頭ポインタを1つ進めるだけ + ノード数が増えても常に一定の速度! +``` + +> 📖 **最終まとめ用語集** +> +> - **`is None`**:PythonのイディオムでNoneとの比較に使う。`== None` より高速(同一性チェック)でpylance推奨 +> - **`collections.deque`**:両端開きのキュー。C実装のため `list.pop(0)` (O(n))より `popleft()` (O(1))が大幅に高速 +> - **FIFO(先入れ先出し)**:First In First Out。キューの動作原則。最初に入れたものを最初に取り出す +> - **短絡評価**:`A and B` でAがFalseなら、Bをまったく評価せず即Falseを返す。無駄な関数呼び出しを省ける +> - **ローカル関数**:関数の中に定義した関数。`self.` 参照が不要になりわずかに高速 +> - **docstringの `:type:` / `:rtype:`**:型アノテーションが使えない環境で型情報を伝えるコメント形式 diff --git a/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Rust.md b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Rust.md new file mode 100644 index 00000000..f25b3096 --- /dev/null +++ b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_Rust.md @@ -0,0 +1,454 @@ +# 🌳 Symmetric Tree — Rust 完全解説 + +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は一言で言うと「**二分木が中心軸に対して鏡写しになっているかを確認する問題**」です。 +> Rustで解く際の最大の特徴は、LeetCodeのノード型が `Option>>` という**複合型**になっている点です。「値があるかもしれない(Option)」「複数箇所から参照できる(Rc)」「中身を後から変更できる(RefCell)」という3層構造を正しく理解することが鍵になります。 + +--- + +### 🖼️ まず「対称」を視覚的に理解する + +``` +【対称な木 ✅】 【非対称な木 ❌】 + + 1 1 + / \ / \ + 2 2 2 2 + / \ / \ \ \ + 3 4 4 3 3 3 + +中心軸を境に 右の2に left がなく +左右が完全に鏡写し right しかないので非対称 +``` + +--- + +### 競技プログラミング視点での分析 + +- ノード数は最大1000と小さいため、全ノードを1回訪問する **O(n)** で十分 +- `Rc::clone()` は参照カウントを増やすだけで**ヒープコピーは発生しない**(O(1)) +- 再帰深度は最大1000なのでスタックオーバーフローの心配はほぼなし + +### 業務開発視点での分析 + +- `Option` で「ノードが存在しない(null)」を型レベルで表現できる +- `Rc>` の扱いで「いつ `borrow()` するか」「いつ `clone()` するか」を意識する必要がある +- LeetCodeの型定義はそのまま使うため、独自エラー型は定義しない(`bool` を直接返す) + +### Rust特有の考慮点 + +``` +Option>> を分解すると… + +Option<...> → ノードが「ある」か「ない」か(nullの代わり) + Rc<...> → 複数の変数から同じノードを「共有参照」できる + RefCell<...> → コンパイル時ではなく実行時に借用チェックを行う + TreeNode → 実際のノードデータ(val, left, right) +``` + +> 📖 **このセクションで登場した用語** +> +> - **所有権**:値を"誰が管理するか"をコンパイル時に決めるRust独自の仕組み +> - **`Rc`(Reference Counted)**:参照カウント式の共有所有権。木構造のように複数箇所から同じノードを参照する場面で使う。Javaの参照変数に近いが、カウントが0になった瞬間に自動でメモリ解放される +> - **`RefCell`**:通常Rustは「借用は1つの`&mut T`か複数の`&T`」というルールをコンパイル時に強制するが、`RefCell`はそのチェックを**実行時**に行う仕組み。木構造の再帰操作で必要になる +> - **`Option`**:値が「ある(`Some(T)`)」か「ない(`None`)」かを型で表現。他言語の`null`と違い、チェックを忘れるとコンパイルエラーになる + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。Rustでは特に「所有権の移動が発生するか」「ヒープアロケーションが必要か」がパフォーマンスに影響します。 + +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | +| ----------------------- | ---------- | ---------- | -------------- | ------ | ------ | ------------------------------------- | +| **A: 再帰(DFS)** | O(n) | O(h) | 低 | 高 | 最高 | `Rc::clone()`で所有権を移さず参照共有 | +| **B: 反復(VecDeque)** | O(n) | O(w) | 中 | 高 | 高 | スタックオーバーフロー耐性あり | +| **C: 配列シリアライズ** | O(n) | O(n) | 高 | 中 | 低 | `Vec`アロケーションが多発、非推奨 | + +> ※ **h = 木の高さ**、**w = 木の最大幅**。最悪ケースはどちらもO(n) + +--- + +> 💡 **Rust固有の観点** +> +> - **方法A**:`Rc::clone()` は参照カウントをインクリメントするだけ(ヒープコピーなし)。`borrow()` でスコープを限定して借用し、スコープを抜けると自動で解放される +> - **方法B**:`VecDeque` はヒープアロケーションが発生するが、深い木でもスタックを消費しない +> - **方法C**:`Vec>` を作るために多くのアロケーションが発生し非効率 + +--- + +> 📖 **このセクションで登場した用語** +> +> - **`Rc::clone()`**:`Rc` の参照カウントを1増やすだけの軽量操作。値の実体をコピーするわけではない +> - **`borrow()`**:`RefCell` の中身を読み取り専用で借用するメソッド。借用スコープを抜けると自動解放 +> - **`VecDeque`**:両端キュー(先頭・末尾どちらにも追加・削除できるコレクション) + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:「なぜこれを選ばなかったか」を対比で説明します。 + +- **選択したアプローチ**:**A(再帰)をメイン、B(反復)をフォローアップ**で両方実装 +- **理由**: + - **方法C(シリアライズ)は選ばない**:`Vec` のアロケーションが多発し、null位置のエンコードも複雑になるため + - **方法A(再帰)を選ぶ**:「鏡写しの定義」がコードにそのまま現れる直感的な構造。`Rc::clone()` の軽量コピーと `borrow()` の自動解放でメモリ安全に記述できる + - **方法B(反復)もフォローアップで実装**:深さ1000の一直線の木では再帰深度が1000になりうるため、万全を期す + +- **Rust特有の最適化ポイント**: + - `Rc::clone()` はポインタコピーのみ(ゼロコスト抽象化) + - `borrow()` が返す `Ref` はスコープを抜けた瞬間に借用が自動解放される(RAII) + - `Option` のパターンマッチングはコンパイル時に全ケースの網羅チェックが行われる + +> 📖 **このセクションで登場した用語** +> +> - **ゼロコスト抽象化**:便利な高レベルな書き方をしても、手書きの低レベルコードと同等の速さになるRustの特性 +> - **RAII(Resource Acquisition Is Initialization)**:変数がスコープを抜けると自動でリソース(メモリ・ロックなど)が解放される仕組み。`drop()` が自動で呼ばれる +> - **パターンマッチング**:`match` や `if let` を使って、値の「形(パターン)」に応じて処理を分岐させる仕組み + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:コードの骨格を先に示します。 +> +> 1. `is_symmetric`:エントリーポイント。`root` を受け取り `is_mirror` に渡す +> 2. `is_mirror`(再帰版):2つのノードが鏡写しかを再帰で確認 +> 3. `is_symmetric_iterative`(反復版):`VecDeque` で対称ペアを順番に確認 + +--- + +### ✅ 解法① 再帰(Recursive DFS) + +Runtime 0 ms +Beats 100.00% +Memory 2.25 MB +Beats 60.71% + +```rust +use std::rc::Rc; +use std::cell::RefCell; + +impl Solution { + pub fn is_symmetric(root: Option>>) -> bool { + // rootがNone(空の木)は定義上「対称」 + // LeetCodeの制約では最低1ノードあるが、型上Noneがあり得るため処理する + match root { + None => true, + // Someの場合はrootの左子・右子を鏡判定ヘルパーに渡す + Some(node) => { + // node.borrow() でRefCellの中身を読み取り専用借用する + // .left / .right はそれぞれ Option>> 型 + let borrowed = node.borrow(); + Self::is_mirror( + borrowed.left.clone(), // Rc::clone():参照カウント+1のみ(値コピーなし) + borrowed.right.clone(), // 同上 + ) + } + } + } + + /// 2つのノードが「鏡写し」の関係かどうかを再帰的に確認するヘルパー関数 + /// + /// # Rustの型上の設計ポイント + /// - 引数は `Option>>` を所有権ごと受け取る + /// - `Rc::clone()` で渡しているため呼び出し元のデータは消えない + /// + /// # Complexity + /// - Time: O(n) — 全ノードを1回ずつ訪問 + /// - Space: O(h) — 再帰呼び出しのスタック深度(h=木の高さ) + fn is_mirror( + left: Option>>, + right: Option>>, + ) -> bool { + match (left, right) { + // ケース①:両方None → 「空同士」は対称 → true + // 例:葉ノードの子(存在しない位置)同士を比較した場合 + (None, None) => true, + + // ケース②:片方だけNone → 「一方だけ枝がある」→ 非対称 → false + // (Some(_), None) と (None, Some(_)) の両方をまとめて捕捉 + (None, Some(_)) | (Some(_), None) => false, + + // ケース③:両方Some → 値を比較して、さらに再帰で内側・外側を確認 + (Some(l_node), Some(r_node)) => { + // borrow() でRefCellの中身を一時的に読み取り専用借用する + // この借用はブロックを抜けると自動で解放される(RAII) + let l = l_node.borrow(); + let r = r_node.borrow(); + + // 条件1: 左右の値が同じか? + l.val == r.val + // 条件2: 左の「左子」と右の「右子」が鏡写しか?(外側ペア) + && Self::is_mirror(l.left.clone(), r.right.clone()) + // 条件3: 左の「右子」と右の「左子」が鏡写しか?(内側ペア) + && Self::is_mirror(l.right.clone(), r.left.clone()) + // ↑ && の短絡評価(Short-circuit):条件1がfalseの時点で + // 条件2・3の再帰呼び出しは行われない → 無駄な処理を省く + } + } + } +} +``` + +--- + +> 💡 **再帰版のトレース** — Example 1: `[1,2,2,3,4,4,3]` + +``` + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + +is_symmetric(root=1) +└─ is_mirror(left=Some(2), right=Some(2)) + ├─ borrow: l.val=2, r.val=2 → 2==2 ✅ + ├─ is_mirror(l.left=Some(3), r.right=Some(3)) ← 外側ペア + │ ├─ l.val=3, r.val=3 → 3==3 ✅ + │ ├─ is_mirror(None, None) → true ✅ + │ └─ is_mirror(None, None) → true ✅ + │ → true ✅ + └─ is_mirror(l.right=Some(4), r.left=Some(4)) ← 内側ペア + ├─ l.val=4, r.val=4 → 4==4 ✅ + ├─ is_mirror(None, None) → true ✅ + └─ is_mirror(None, None) → true ✅ + → true ✅ +→ true ✅ 最終結果: true +``` + +``` +Example 2: [1,2,2,null,3,null,3] + + 1 + / \ + 2 2 + \ \ + 3 3 + +is_mirror(left=Some(2), right=Some(2)) + ├─ 2==2 ✅ + ├─ is_mirror(l.left=None, r.right=Some(3)) ← 外側ペア + │ → (None, Some(_)) にマッチ → false ❌ ← 短絡評価で即終了! + → false ❌ 最終結果: false +``` + +--- + +### ✅ 解法② 反復(Iterative BFS with VecDeque) + +> 💡 **なぜ `VecDeque` を使うのか**:比較すべき「鏡ペア」を順番に取り出すために**キュー(FIFO)**が必要です。Rustの標準ライブラリには両端キューの `VecDeque` があり、`push_back()` で追加・`pop_front()` で先頭から取り出せます。 + +Runtime 0 ms +Beats 100.00% +Memory 2.28 MB +Beats 60.71% + +```rust + +use std::collections::VecDeque; +use std::rc::Rc; +use std::cell::RefCell; + +impl Solution { + pub fn is_symmetric_iterative(root: Option>>) -> bool { + // rootがNoneの場合は対称 + let root = match root { + None => return true, + Some(n) => n, + }; + + // キュー:比較すべき「鏡ペア」を格納する + // タプル (左ノード, 右ノード) を並べていく + // VecDeque はヒープ上に確保される両端キュー + let mut queue: VecDeque<( + Option>>, + Option>>, + )> = VecDeque::new(); + + // 最初のペア:rootの左子と右子を追加 + { + // borrow() のスコープをブロックで限定する + // → このブロックを抜けると借用が解放され、後続の処理で再借用できる + let borrowed = root.borrow(); + queue.push_back((borrowed.left.clone(), borrowed.right.clone())); + } + + // キューが空になるまで(= 全ペアの確認が終わるまで)ループ + while let Some((left, right)) = queue.pop_front() { + // while let:Option が Some の間だけループを続ける構文 + // pop_front() は Option を返す(空のキューなら None) + + match (left, right) { + // 両方None → このペアはOK、次のペアへ + (None, None) => continue, + + // 片方だけNone → 非対称確定 + (None, Some(_)) | (Some(_), None) => return false, + + // 両方存在する → 値を確認して次のペアをキューに積む + (Some(l_node), Some(r_node)) => { + // 借用スコープをブロックで限定して + // clone()した後に借用が解放されるようにする + let (l_val, l_left, l_right, r_left, r_right) = { + let l = l_node.borrow(); + let r = r_node.borrow(); + ( + l.val, + l.left.clone(), // 外側ペア用:左の左子 + l.right.clone(), // 内側ペア用:左の右子 + r.left.clone(), // 内側ペア用:右の左子 + r.right.clone(), // 外側ペア用:右の右子 + ) + // ← このブロックを抜けると l と r の借用が自動解放(RAII) + }; + + // 値が異なれば即false + if l_val != r_node.borrow().val { + return false; + } + + // 次に確認すべき鏡ペアをキューに追加 + queue.push_back((l_left, r_right)); // 外側ペア + queue.push_back((l_right, r_left)); // 内側ペア + } + } + } + + // 全ペアの確認をパスしたので対称 + true + } +} +``` + +> ⚠️ **LeetCode提出はシングル `impl Solution` ブロックのみ可**。上記の `is_symmetric_iterative` は学習用です。LeetCode提出コードは下記の最終版をご利用ください。 + +--- + +> 💡 **反復版のトレース** — Example 1: `[1,2,2,3,4,4,3]` + +``` +初期状態: queue = [(Some(2), Some(2))] + +─── ループ1回目 ─── +取り出し: (Some(2), Some(2)) + l.val=2, r.val=2 → 2==2 ✅ + 外側ペア追加: (Some(3), Some(3)) + 内側ペア追加: (Some(4), Some(4)) +queue = [(Some(3),Some(3)), (Some(4),Some(4))] + +─── ループ2回目 ─── +取り出し: (Some(3), Some(3)) + 3==3 ✅ + 外側: (None, None) 追加 + 内側: (None, None) 追加 +queue = [(Some(4),Some(4)), (None,None), (None,None)] + +─── ループ3回目 ─── +取り出し: (Some(4), Some(4)) + 4==4 ✅ + → (None,None) × 2 追加 +queue = [(None,None) × 4] + +─── ループ4〜7回目 ─── +(None, None) → continue(両方Noneなのでスキップ)× 4回 + +queue = [] → while let が None → ループ終了 +→ return true ✅ +``` + +--- + +### 📦 LeetCode提出用コード(再帰版・最終版) + +```rust +// Runtime 0 ms +// Beats 100.00% +// Memory 2.19 MB +// Beats 91.96% + +use std::rc::Rc; +use std::cell::RefCell; + +impl Solution { + pub fn is_symmetric(root: Option>>) -> bool { + // rootがNoneなら対称(空の木) + match root { + None => true, + Some(node) => { + let borrowed = node.borrow(); + // rootの左子と右子が鏡写しかを確認 + Self::is_mirror(borrowed.left.clone(), borrowed.right.clone()) + } + } + } + + fn is_mirror( + left: Option>>, + right: Option>>, + ) -> bool { + match (left, right) { + // 両方Noneなら対称 + (None, None) => true, + // 片方だけNoneなら非対称 + (None, Some(_)) | (Some(_), None) => false, + // 両方Someなら値を比較し、外側・内側ペアを再帰確認 + (Some(l), Some(r)) => { + let l = l.borrow(); + let r = r.borrow(); + l.val == r.val + && Self::is_mirror(l.left.clone(), r.right.clone()) + && Self::is_mirror(l.right.clone(), r.left.clone()) + } + } + } +} +``` + +--- + +> 📖 **このセクションで登場した用語** +> +> - **`Rc::clone()`**:`Rc` の参照カウントを1増やすだけ。値の実体をコピーするわけではないため O(1) の軽量操作 +> - **`borrow()`**:`RefCell` の中身を読み取り専用で借用するメソッド。`Ref` という一時的な借用ガードを返し、スコープを抜けると自動解放(RAII) +> - **`while let`**:`Option` や `Result` が `Some`/`Ok` の間だけループを続ける構文。`loop` + `match` の糖衣構文 +> - **短絡評価(Short-circuit evaluation)**:`A && B` でAが `false` なら B を評価せず即 `false` を返す。Rustでも同様に動作し、不要な再帰呼び出しを省ける +> - **`VecDeque`**:標準ライブラリの両端キュー。`push_back` で末尾追加、`pop_front` で先頭取り出し(FIFO) +> - **`continue`**:ループの現在のイテレーションをスキップして次へ進む制御フロー + +--- + +## Rust固有の最適化観点 + +### `Rc>` パターンの理解 + +``` +┌─────────────────────────────────────────────────────┐ +│ なぜ Rc> が必要なのか? │ +│ │ +│ 通常のRust(所有権モデル): │ +│ ある値を持てるのは "1人の所有者" だけ │ +│ → 木構造で親・子・隣接ノードが互いを参照できない │ +│ │ +│ Rc(参照カウント): │ +│ 複数の変数が同じデータを共有所有できる │ +│ → 木のノードを複数箇所から参照可能に │ +│ │ +│ RefCell(実行時借用チェック): │ +│ 通常の &mut T は "1つだけ" という制約がある │ +│ → RefCell で実行時チェックに緩和して柔軟に操作 │ +└─────────────────────────────────────────────────────┘ +``` + +### 計算量まとめ + +| 解法 | 時間計算量 | 空間計算量 | +| ----------- | ---------- | -------------------- | +| 再帰(DFS) | O(n) | O(h) — hは木の高さ | +| 反復(BFS) | O(n) | O(w) — wは木の最大幅 | + +どちらも全ノードを1回ずつ訪問するため **O(n)** です。`Rc::clone()` はO(1)なので比較回数に影響しません。空間計算量は木の形状によりますが、最悪ケースはどちらも **O(n)** です(一直線の木 / 完全二分木の最下段にノードが集中する場合)。 diff --git a/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_TypeScript.md b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_TypeScript.md new file mode 100644 index 00000000..e6e7c4c9 --- /dev/null +++ b/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/Symmetric_Tree_TypeScript.md @@ -0,0 +1,351 @@ +# 🌳 Symmetric Tree — TypeScript 完全解説 + +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は一言で言うと、「**二分木が左右対称(鏡写し)になっているかを確認する問題**」です。木の中心を軸にして、左サブツリーと右サブツリーが完全に対称かどうかを調べます。 + +--- + +### 🖼️ まず「対称」とは何かを視覚的に理解する + +``` +【対称な木 ✅】 【非対称な木 ❌】 + + 1 1 + / \ / \ + 2 2 2 2 + / \ / \ \ \ + 3 4 4 3 3 3 + +左サブツリーを 右の2にleftがないので +鏡に映すと右サブツリーと 左右が一致しない +完全に一致する! +``` + +「鏡写し」の条件を分解すると: + +1. 左の子の値 = 右の子の値 +2. 左の子の「左」 ↔ 右の子の「右」 が鏡写し +3. 左の子の「右」 ↔ 右の子の「左」 が鏡写し + +--- + +### 競技プログラミング視点での分析 + +- **ノード数は最大1000**と小さいため、最悪計算量 O(n) で全ノードを1回ずつ訪問すれば十分 +- 再帰・反復どちらもO(n)時間 / O(n)空間(スタック/キューのため) +- 早期終了(値が違えばすぐfalse返却)で定数倍の高速化が可能 + +### 業務開発視点での分析 + +- `TreeNode | null` という **Union型**(=複数の型を `|` でつなげた型)を正しく扱うためのnullチェックが必須 +- 再帰版は「意図が読みやすい」、反復版は「スタックオーバーフローに強い」というトレードオフがある +- LeetCodeのクラス定義をそのまま使うため、型の追加定義は最小限 + +### TypeScript特有の考慮点 + +- `TreeNode | null` に対するアクセス前に **型ガード**(=実行時に型を絞り込むチェック)が必須 +- `null` と `undefined` を厳密に区別する `strictNullChecks` が前提 +- 再帰ヘルパー関数を内部に閉じ込めることで、外部APIをシンプルに保てる + +> 📖 **このセクションで登場した用語** +> +> - **Union型**:`A | B` のように複数の型を`|`でつなげた型。「AまたはB」を表す +> - **型ガード**:`if (node !== null)` のように実行時に型を絞り込むチェック処理 +> - **strictNullChecks**:`null`と`undefined`を他の型と混在させないようにするTypeScriptの設定 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。問題文のフォローアップが「再帰・反復の両方を実装せよ」なので、両方解説します。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| ------------------------- | ---------- | ---------- | ------------ | -------- | ------ | ---------------------------------------- | +| **A: 再帰(DFS)** | O(n) | O(h)※ | 低 | 高 | 最高 | コードが「定義そのもの」に近く読みやすい | +| **B: 反復(BFS/キュー)** | O(n) | O(w)※※ | 中 | 高 | 高 | スタックオーバーフローのリスクなし | +| **C: 配列シリアライズ** | O(n) | O(n) | 高 | 中 | 低 | 一度配列化して比較。実装コスト高でNG | + +> ※ **h = 木の高さ**。最悪ケース(一直線の木)でO(n)、平均的なバランス木でO(log n) +> ※※ **w = 木の最大幅**。最悪ケースでO(n)(最下段に全ノードが集中する場合) + +--- + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(n)`:ノード数nが2倍になると処理も約2倍(全ノードを1回ずつ訪問) +> - `O(h)`:木の「深さ」に比例したメモリ。再帰呼び出しがスタックに積まれる分 + +--- + +> 📖 **このセクションで登場した用語** +> +> - **DFS(深さ優先探索)**:根から葉へ向かって深く潜っていく探索方法。再帰と相性が良い +> - **BFS(幅優先探索)**:同じ深さのノードを左から右へ横断する探索方法。キューと相性が良い +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:「なぜこれを選んだか」を「他の方法のどこが惜しかったか」と対比して説明します。 + +- **選択したアプローチ**: **A(再帰)をメイン、B(反復)をフォローアップ** として両方実装 +- **理由**: + - **方法C(シリアライズ)は選ばない**:実装コストが高く、null位置のエンコードが複雑になるため + - **方法A(再帰)を選ぶ**:「鏡写しの定義」がそのままコードになる直感的な構造。再帰は可読性が高いですが、非常に深い木(退化した木)ではコールスタックの上限に達するリスクがあります。そのため、極端な深さや異なる実行環境(ブラウザ/LeetCode/Node等)での安全性を考慮し、反復版(方法B)も代替案として用意します。 + - **方法B(反復)もフォローアップで実装**:深さが極端に大きいケースへの対応として有用 + +- **TypeScript特有の最適化ポイント**: + - `TreeNode | null` を受け取るヘルパーに **型ガード** を入れてnull安全性を担保 + - ヘルパー関数を `const` のアロー関数として内側に閉じることで、外部から呼び出せない設計にする(**クロージャ**=関数が自分の外側の変数を「覚えている」仕組み) + +> 📖 **このセクションで登場した用語** +> +> - **クロージャ**:関数が自分が定義されたスコープの変数を「閉じ込めて」使える仕組み +> - **スタックオーバーフロー**:再帰が深くなりすぎてプログラムがクラッシュする現象 +> - **null安全性**:`null`や`undefined`へのアクセスによる予期しないクラッシュを防ぐ仕組み + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:コード全体の骨格を先に示します。 +> +> 1. `isSymmetric`:エントリーポイント(入口)。rootを受け取りヘルパーに渡す +> 2. `isMirror`(再帰版):2つのノードが「鏡写し」かを再帰で確認する +> 3. `isSymmetricIterative`(反復版):キューを使って対称ペアを順番に確認する + +--- + +### ✅ 解法① 再帰(Recursive DFS) + +```typescript +// Runtime 0 ms +// Beats 100.00% +// Memory 57.96 MB +// Beats 79.32% + +function isSymmetric(root: TreeNode | null): boolean { + // 木が空(nullの木)は定義上「対称」とする + // (LeetCodeの制約では最低1ノードあるが、型上nullがあり得るため) + if (root === null) return true; + + /** + * 2つのノードが「鏡写し」の関係かどうかを再帰的に確認するヘルパー関数 + * ── なぜ内部関数にするか:外部から呼ばれる必要がなく、isSymmetricの + * ロジックに強く依存しているため、スコープを閉じ込める + * + * @param left - 左側のノード(TreeNode または null) + * @param right - 右側のノード(TreeNode または null) + * @returns 鏡写しならtrue、そうでなければfalse + * @complexity Time: O(n), Space: O(h) ─ h=木の高さ + */ + const isMirror = (left: TreeNode | null, right: TreeNode | null): boolean => { + // ─── ケース①:両方nullなら「空同士で対称」→ true + // 例:葉ノードの子(存在しない位置)同士を比較した場合 + if (left === null && right === null) return true; + + // ─── ケース②:片方だけnullなら「片方だけ枝がある」→ 非対称でfalse + // 例:左の子はあるのに右の子がない場合 + if (left === null || right === null) return false; + + // ─── ケース③:両方存在する場合は以下の3条件を全て満たすか確認 + return ( + left.val === right.val && // 条件1: 値が同じか? + isMirror(left.left, right.right) && // 条件2: 左の「左子」と右の「右子」が鏡か? + isMirror(left.right, right.left) // 条件3: 左の「右子」と右の「左子」が鏡か? + ); + // ↑ &&(AND)で繋ぐことで、1つでも条件が外れたら即falseを返す(短絡評価) + }; + + // rootの左サブツリーと右サブツリーが鏡写しかをチェックする + return isMirror(root.left, root.right); +} +``` + +--- + +> 💡 **再帰版のトレース** ─ Example 1: `[1,2,2,3,4,4,3]` + +``` + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 + +isSymmetric(root=1) + └─ isMirror(left=2, right=2) + ├─ 2.val === 2.val ✅ + ├─ isMirror(left=3, right=3) ← 左の左子 vs 右の右子 + │ ├─ 3.val === 3.val ✅ + │ ├─ isMirror(null, null) → true ✅ + │ └─ isMirror(null, null) → true ✅ + │ → true ✅ + └─ isMirror(left=4, right=4) ← 左の右子 vs 右の左子 + ├─ 4.val === 4.val ✅ + ├─ isMirror(null, null) → true ✅ + └─ isMirror(null, null) → true ✅ + → true ✅ + → true ✅ 最終結果: true +``` + +``` +Example 2: [1,2,2,null,3,null,3] + + 1 + / \ + 2 2 + \ \ + 3 3 + +isMirror(left=2, right=2) + ├─ 2.val === 2.val ✅ + ├─ isMirror(null, 3) ← 左の左子(null) vs 右の右子(3) + │ → 片方がnull、もう片方が3 → false ❌ ← ここで即終了! + → false ❌ 最終結果: false +``` + +--- + +### ✅ 解法② 反復(Iterative BFS with Queue) + +> 💡 **なぜキューを使うのか**:「鏡対称ペア」を順番に取り出して比較するためです。キューはFIFO(先入れ先出し)なので、同じ深さのペアを順番に処理できます。 + +```typescript +function isSymmetricIterative(root: TreeNode | null): boolean { + // 空の木は対称 + if (root === null) return true; + + // キュー(比較すべき「ノードのペア」を順番に格納する列) + // ─ タプル型 [TreeNode|null, TreeNode|null] でペアを型安全に管理 + const queue: Array<[TreeNode | null, TreeNode | null]> = []; + + // 最初のペア:rootの左子と右子を比較対象として追加 + queue.push([root.left, root.right]); + + // 先頭インデックス(shift()のO(n)を避けるためのO(1)ポインタ) + let head = 0; + + // キューが空になるまで(= 全ペアの比較が終わるまで)ループ + while (head < queue.length) { + // キューの先頭からペアを取り出す(分割代入で左・右に分ける) + const [left, right] = queue[head]; + head++; + // ↑ [head]で先頭要素をO(1)で読み取り、headを進める + + // ケース①:両方nullならこのペアはOK → 次のペアへ + if (left === null && right === null) continue; + + // ケース②:片方だけnullか、値が異なる → 非対称確定でfalse + if (left === null || right === null) return false; + if (left.val !== right.val) return false; + + // ケース③:次に比較すべき「鏡ペア」をキューに追加 + // ─ 外側ペア:左の「左子」と右の「右子」 + queue.push([left.left, right.right]); + // ─ 内側ペア:左の「右子」と右の「左子」 + queue.push([left.right, right.left]); + } + + // 全ペアがOKだったので対称 + return true; +} +``` + +--- + +> 💡 **反復版のトレース** ─ Example 1: `[1,2,2,3,4,4,3]` + +``` +初期状態: queue = [(2, 2)] ← rootの左子・右子ペア + +─── ループ1回目 ─── +取り出し: (left=2, right=2) + 2.val === 2.val ✅ + 追加: (2.left=3, 2.right=3) → 外側ペア + 追加: (2.right=4, 2.left=4) → 内側ペア +queue = [(3,3), (4,4)] + +─── ループ2回目 ─── +取り出し: (left=3, right=3) + 3.val === 3.val ✅ + 追加: (null, null) → 外側ペア + 追加: (null, null) → 内側ペア +queue = [(4,4), (null,null), (null,null)] + +─── ループ3回目 ─── +取り出し: (left=4, right=4) + 4.val === 4.val ✅ + 追加: (null,null), (null,null) +queue = [(null,null), (null,null), (null,null), (null,null)] + +─── ループ4〜7回目 ─── +(null,null) → continue(両方nullなのでスキップ)× 4回 + +queue = [] → ループ終了 → return true ✅ +``` + +--- + +### 📦 LeetCode提出用コード(再帰版) + +```typescript +function isSymmetric(root: TreeNode | null): boolean { + if (root === null) return true; + + const isMirror = (left: TreeNode | null, right: TreeNode | null): boolean => { + if (left === null && right === null) return true; + if (left === null || right === null) return false; + return ( + left.val === right.val && + isMirror(left.left, right.right) && + isMirror(left.right, right.left) + ); + }; + + return isMirror(root.left, root.right); +} +``` + +> 📖 **このセクションで登場した用語** +> +> - **短絡評価(Short-circuit evaluation)**:`A && B` でAがfalseなら、Bを評価せず即falseを返す仕組み。無駄な計算を省ける +> - **タプル型**:`[number, string]` のように要素数と各位置の型が決まった固定長配列の型 +> - **分割代入(Destructuring)**:`const [a, b] = array` のように配列/オブジェクトから値を取り出す構文 +> - **non-null assertion(!)**:TypeScriptに「この値はnullでない」と伝える演算子。乱用すると危険だが、直前のチェックで安全が保証されている場合に使う +> - **FIFO**:First In First Out(先入れ先出し)。キューの動作原則 + +--- + +## TypeScript固有の最適化観点 + +### 型安全性の活用 + +1. **コンパイル時エラー防止** + - `TreeNode | null` というUnion型を使うことで、「nodeにアクセスする前にnullチェックが必要」とコンパイラが教えてくれる。実行前にバグを発見できる + - `isMirror` の引数型を明示することで、呼び出し時の型ミスを防止 + +2. **タプル型によるペア管理** + - `Array<[TreeNode|null, TreeNode|null]>` という型で「必ず2要素のペア」を保証。3要素を誤って追加しようとするとコンパイルエラーになる + +3. **const アロー関数でのヘルパー閉じ込め** + - `const isMirror = (...) => ...` とすることで再代入不可。意図せず上書きされるバグを防ぐ + +### コンパイル時最適化 + +1. **インデックスによるキュー操作の最適化**:`queue.shift()` による O(n) の配列再インデックスを避けるため、`head` インデックスを使用しています。`queue[head]` で要素を参照し、`head++` でポインタを進めることで、キューからの取り出しを O(1) で実現し、大量のノードがある場合でも高いパフォーマンスを維持できます。 +2. **readonly修飾子**:今回はLeetCodeの既存クラス定義を使うため追加はしないが、自前のデータ構造では `readonly` を付けてイミュータブルにするのが理想 + +### 計算量まとめ + +| 解法 | 時間計算量 | 空間計算量 | +| ----------- | ---------- | -------------------- | +| 再帰(DFS) | O(n) | O(h) ─ hは木の高さ | +| 反復(BFS) | O(n) | O(w) ─ wは木の最大幅 | + +どちらも全ノードを1回ずつ訪問するため **O(n)** です。空間計算量は木の形状によって異なりますが、最悪ケースはどちらも **O(n)** です(一直線の木 / 完全二分木の最下段にノードが集中する場合)。 diff --git a/DataStructures/Trees/Other/README.md b/DataStructures/Trees/Other/README.md index 680e0712..d1703427 100644 --- a/DataStructures/Trees/Other/README.md +++ b/DataStructures/Trees/Other/README.md @@ -115,7 +115,7 @@ for a, b in edges: 📌 各タプルに対し `adj[a-1][b-1] = 1` & `adj[b-1][a-1] = 1` -### 🔍 各ステップを図で見る: +### 🔍 各ステップを図で見る #### ✅ (1, 2) 処理後 diff --git a/public/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html b/public/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html new file mode 100644 index 00000000..a2c2411d --- /dev/null +++ b/public/DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html @@ -0,0 +1,2021 @@ + + + + + + LeetCode #101 Symmetric Tree — 完全解説 + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ + +
+

+ 💡 + この問題を一言で言うと:「二分木の左右が完全に鏡写しかどうかを確認する問題」 +

+

+ 「鏡写し」とは、木の中心軸(ルート)を境に、左サブツリーを裏返すと右サブツリーとぴったり重なる状態です。 + 単に「左右の値が同じ」だけでは不十分で、構造(形)と値の両方が対応していなければなりません。 +

+
+ + +
+

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

+
    +
  • + 「左と右が同じ構造」ではなく「左の左 ↔ 右の右左の右 ↔ 右の左」という交差した対応関係を正確に追う必要がある +
  • +
  • + ノードが + None(存在しない)の場合のケースを3パターン(両方None・片方None・両方あり)に分けて処理しないとバグになる +
  • +
  • + 値が全て同じでも構造が非対称なケース(例:[2,2,2,null,2])で誤検知しやすい +
  • +
+
+ + +
+
+
+ ✅ 例1:対称な木 +
+
+入力: [1, 2, 2, 3, 4, 4, 3]
+出力: True
+
+       1
+      / \
+     2   2
+    / \ / \
+   3  4 4  3
+
+理由: 左右が完全に鏡写し
+  左の左子(3) ↔ 右の右子(3) ✅
+  左の右子(4) ↔ 右の左子(4) ✅
+
+
+
+ ❌ 例2:非対称な木 +
+
+入力: [1, 2, 2, null, 3, null, 3]
+出力: False
+
+       1
+      / \
+     2   2
+      \   \
+       3   3
+
+理由: left.leftがnullである一方で
+  right.rightは3なので
+  nullと3が一致せず非対称になる ❌
+
+
+ + +
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量(再帰)
+
+
+
+ 1〜1000 +
+
ノード数の制約
+
+
+
+ -100〜100 +
+
ノード値の範囲
+
+
+
+ + +
+

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

+

+ 各ステップをクリックすると詳細が表示されます。▶ Play で自動再生も可能です。 +

+
+
+ + +
+

+ Python 実装 +

+ + +
+

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

+
    +
  1. + isSymmetric:エントリーポイント。root が None なら即 + True、そうでなければヘルパーへ +
  2. +
  3. + _is_mirror:2つのノードを受け取り、3ケース(両方None・片方None・両方あり)で鏡写し判定 +
  4. +
  5. 値が等しく、外側ペア・内側ペアも再帰的に鏡写しなら True を返す
  6. +
  7. + isSymmetricIterative:deque を使う反復版(スタック深度制限を回避したい場合) +
  8. +
+
+ +
from collections import deque
+
+
+# LeetCode が提供する TreeNode クラス(提出時はコメント済みのものを使用)
+# class TreeNode(object):
+#     def __init__(self, val=0, left=None, right=None):
+#         self.val = val
+#         self.left = left
+#         self.right = right
+
+class Solution(object):
+
+    # =========================================================
+    # 解法①: 再帰版(メイン)
+    # =========================================================
+    def isSymmetric(self, root):
+        """
+        二分木が鏡写し(対称)かどうかを再帰で判定する。
+
+        :type  root: Optional[TreeNode]
+        :rtype: bool
+
+        Time:  O(n) - 全ノードを1回ずつ訪問する
+        Space: O(h) - 再帰スタックの深さ(h = 木の高さ)
+        """
+        # rootがNone(空の木)は「対称」と定義する
+        if root is None:
+            return True
+
+        # rootは中心軸なので比較しない。左子と右子を最初のペアとして渡す
+        return self._is_mirror(root.left, root.right)
+
+    def _is_mirror(self, left, right):
+        """
+        2つのノードが「鏡写し」の関係かどうかを再帰的に確認するヘルパー。
+
+        :type  left:  Optional[TreeNode]
+        :type  right: Optional[TreeNode]
+        :rtype: bool
+        """
+        # ── 基底条件① ──
+        # 両方Noneなら「空同士」= 対称 → True
+        # 例: 葉ノードの子(存在しない位置)同士を比較した場合
+        if left is None and right is None:
+            return True
+
+        # ── 基底条件② ──
+        # 片方だけNoneなら「一方だけ枝がある」= 非対称 → False
+        # `is None` を使う: `== None` より高速(同一性チェック)
+        if left is None or right is None:
+            return False
+
+        # ── 再帰ステップ ──
+        # 3条件を `and` で繋ぐ。短絡評価で値が違えば即Falseを返す
+        return (
+            left.val == right.val                          # 条件1: 値が同じか?
+            and self._is_mirror(left.left, right.right)   # 条件2: 外側ペア
+            and self._is_mirror(left.right, right.left)   # 条件3: 内側ペア
+        )
+
+    # =========================================================
+    # 解法②: 反復版(フォローアップ)
+    # =========================================================
+    def isSymmetricIterative(self, root):
+        """
+        二分木が鏡写しかどうかを反復(deque)で判定する。
+        RecursionError が心配な場合はこちらを使う。
+
+        deque を使う理由: list.pop(0) は O(n) だが
+                          deque.popleft() は O(1) で高速。
+
+        :type  root: Optional[TreeNode]
+        :rtype: bool
+
+        Time:  O(n)  Space: O(w) - wは木の最大幅
+        """
+        if root is None:
+            return True
+
+        # dequeに「鏡ペア」をタプルで格納して順番に比較する
+        queue = deque()
+        queue.append((root.left, root.right))
+
+        while queue:
+            # FIFO(先入れ先出し)でペアを取り出す
+            left, right = queue.popleft()
+
+            if left is None and right is None:
+                continue           # 両方None → OK、次のペアへ
+            if left is None or right is None:
+                return False       # 片方だけNone → 非対称
+            if left.val != right.val:
+                return False       # 値が違う → 非対称
+
+            # 次に確認すべき鏡ペアをキューに追加
+            queue.append((left.left, right.right))   # 外側ペア
+            queue.append((left.right, right.left))   # 内側ペア
+
+        return True
+ + +
+

+ ▶ 入力例 + root = [1, 2, 2, 3, 4, 4, 3] + での動作トレース(再帰版) +

+
+isSymmetric(root=1)
+  → root != None → _is_mirror(root.left=2, root.right=2)
+
+_is_mirror(left=Node(2), right=Node(2))
+  → 両方Noneでない、片方Noneでない
+  → 2 == 2 ✅
+  → _is_mirror(left.left=Node(3), right.right=Node(3))  ← 外側ペア
+
+    _is_mirror(left=Node(3), right=Node(3))
+      → 3 == 3 ✅
+      → _is_mirror(None, None) → True ✅
+      → _is_mirror(None, None) → True ✅
+      → True ✅
+
+  → _is_mirror(left.right=Node(4), right.left=Node(4))  ← 内側ペア
+
+    _is_mirror(left=Node(4), right=Node(4))
+      → 4 == 4 ✅
+      → _is_mirror(None, None) → True ✅
+      → _is_mirror(None, None) → True ✅
+      → True ✅
+
+最終結果: True and True and True = True ✅
+
+ + +
+

+ ▶ 入力例 + root = [1, 2, 2, null, 3, null, 3] + での動作トレース(短絡評価の効果) +

+
+_is_mirror(left=Node(2), right=Node(2))
+  → 2 == 2 ✅
+  → _is_mirror(left.left=None, right.right=Node(3))  ← 外側ペア
+
+    _is_mirror(left=None, right=Node(3))
+      → 片方だけ None → False ❌ ← ここで即終了!
+
+  → False なので and の短絡評価が働き
+    内側ペアの _is_mirror は呼ばれない(省エネ!)
+
+最終結果: False ❌
+
+
+ + +
+

+ 処理フローチャート +

+ + +
+

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

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+
+ はい + いいえ +
+
+
+
+ +

+ この図は + _is_mirror(left, right) + ヘルパー関数の処理の流れを表しています。 + 上から下へ読み進め、ひし形の分岐で「はい/いいえ」のどちらかの経路を進みます。 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + 開始 + + + + + + + + + root is None? + + + (空の木か?) + + + + + + + True を返す + + + はい + + + + + + いいえ + + + + + + _is_mirror(root.left, root.right) + + + 左子と右子を「最初の鏡ペア」として渡す + + + + + + + + + left と right + + + 両方 None? + + + + + + + True を返す + + + はい + + + + + + いいえ + + + + + + left または right + + + 片方だけ None? + + + + + + + False を返す + + + はい + + + + + + いいえ + + + + + + left.val + + + == right.val? + + + + + + + False を返す + + + いいえ + + + + + はい + + + + + 外側ペアを再帰確認 + + + _is_mirror(left.left, right.right) + + + + + + + + + 内側ペアを再帰確認 + + + _is_mirror(left.right, right.left) + + + + + + + + + 3条件の AND で結合 + + + 値一致 and 外側OK and 内側OK + + + + + + + + + 終了(結果を返す) + + +
+ + +
+

+ 🔎 入力例 + [1, 2, 2, 3, 4, 4, 3] + でのフロー追跡 +

+
    +
  1. 「開始」ノード → 入力 root=1 を受け取る
  2. +
  3. 「root is None?」ノード → root=1 なので「いいえ」の経路へ
  4. +
  5. + 「_is_mirror を呼ぶ」ノード → _is_mirror(left=2, right=2) を呼び出す +
  6. +
  7. 「両方 None?」ノード → 両方ノードが存在するので「いいえ」の経路へ
  8. +
  9. 「片方だけ None?」ノード → どちらもNoneでないので「いいえ」の経路へ
  10. +
  11. 「left.val == right.val?」ノード → 2 == 2 なので「はい」の経路へ
  12. +
  13. 「外側ペアを再帰」ノード → _is_mirror(3, 3) を呼び True が返る
  14. +
  15. 「内側ペアを再帰」ノード → _is_mirror(4, 4) を呼び True が返る
  16. +
  17. 「3条件の AND で結合」ノード → True and True and True = True
  18. +
  19. 「終了」ノード → True を返す ✅
  20. +
+
+
+ + +
+

+ 計算量分析 +

+ + +
+

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

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ n より少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ 解法 + + 時間計算量 + + 空間計算量 + + 最悪ケース(空間) +
+ 再帰(DFS) + + O(n) + + O(h) + + O(n)(一直線の木) +
+ 反復(BFS with deque) + + O(n) + + O(w) + + O(n)(完全二分木最下段) +
+
+ + +
+

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

+

+ 時間計算量 O(n):どちらの解法も、各ノードを最大1回ずつ訪問します。n個のノードがある木では、最大 + n/2 ペアを比較するため O(n/2) = O(n) です。
+ 空間計算量(再帰)O(h):h + は木の高さです。再帰呼び出しは「コールスタック(=関数の呼び出し履歴を記録するメモリ領域)」に積み重なります。最悪ケースの一直線の木では + h = n になるため O(n) です。バランスの取れた木では h = log n になります。
+ 空間計算量(反復)O(w):w は木の最大幅です。deque + には同じ深さのペアが格納されるため、完全二分木の最下段(= n/2 + 個のノード)が最悪ケースで O(n) になります。 +

+
+ + +
+

+ ⚡ 再帰 vs 反復:どちらを選ぶか +

+
+
+

🌀 再帰版を選ぶ場面

+
    +
  • コードの読みやすさを優先したい
  • +
  • バランスの取れた木(深さがスタック上限に達しない)
  • +
  • 「鏡写しの定義」をそのままコードに落としたい
  • +
+
+
+

🔁 反復版を選ぶ場面

+
    +
  • 深さが約1000前後になる退化した木(深さ ≈ スタック上限)では再帰が危険になるため反復版を検討する
  • +
  • スタックオーバーフローを完全に回避したい
  • +
  • 本番環境など安全性を最優先にしたい
  • +
+
+
+
+
+ + +
+

+ 📖 用語集 +

+

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

+
+
+ + BFS(幅優先探索) + +
+ Breadth-First Search + の略。同じ深さのノードを左から右へ横断していく探索方法。キューと相性が良い。 + この問題の反復版で使用。対義語はDFS(深さ優先探索)。 +
+
+ +
+ + DFS(深さ優先探索) + +
+ Depth-First Search + の略。根から葉へ向かって深く潜っていく探索方法。再帰と相性が良い。 + この問題の再帰版で使用。木を「縦に」探索するイメージ。 +
+
+ +
+ + collections.deque(デック) + +
+ Python + 標準ライブラリの両端キュー。「前からも後ろからも出し入れできる箱」のようなデータ構造。 + popleft() + がO(1)で高速(list.pop(0) + はO(n))。 C言語で実装されているため Pure Python より大幅に高速。 +
+
+ +
+ + コールスタック + +
+ 関数が呼び出されるたびに積み重なる「呼び出し履歴」のメモリ領域。 + 再帰呼び出しが深くなるほど消費するメモリが増える。 + Pythonはデフォルトで1000回の再帰呼び出しまで許容(sys.getrecursionlimit())。 +
+
+ +
+ + 基底条件 + +
+ 再帰の終了条件。「これ以上再帰しない」と判断して値を返す条件。 + 基底条件がないと無限再帰(スタックオーバーフロー)になる。 + この問題では「両方None → True」「片方None → False」の2つが基底条件。 +
+
+ +
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す仕組み。木の探索に非常に相性が良い。 + 「鏡写しかどうか」の定義(= + 値が同じで、さらに外側・内側ペアも鏡写し)をそのままコードに書き下せる。 + ロシアのマトリョーシカ人形のように「大きい問題を小さい同じ問題に分解する」イメージ。 +
+
+ +
+ + 短絡評価(Short-circuit + Evaluation) + +
+ A and B + でAが + False + なら、Bをまったく評価せず即座に + False + を返す仕組み。 + この問題では値が違えば外側・内側ペアの再帰呼び出しが省略される。 + 非対称が早い段階で分かるほど効果が大きい最適化テクニック。 +
+
+ +
+ + 二分木(Binary Tree) + +
+ 各ノードが最大2つの子(left と right)を持つ木構造のデータ構造。 + 家系図に例えると、各人物が最大2人の子供を持てる構造。 LeetCodeでは + TreeNode + クラスで表現され、.val, .left, + .right + の3つの属性を持つ。 +
+
+ +
+ + FIFO(先入れ先出し) + +
+ First In First Out + の略。最初に入れたものを最初に取り出すキューの動作原則。 + コンビニのおにぎり棚に例えると、奥から補充して手前から取り出す仕組み(賞味期限管理)と同じ。 + deque の + append() + で末尾追加、popleft() + で先頭取り出しにより実現する。 +
+
+ +
+ + RecursionError + +
+ Pythonの再帰呼び出し上限(デフォルト1000回)を超えたときに発生するエラー。 + 深さ1000の一直線の木で再帰版を使うと発生する可能性がある。 + sys.setrecursionlimit(n) + で上限を変更可能。または反復版を使うことで根本的に回避できる。 +
+
+
+
+ + +
+ LeetCode #101 Symmetric Tree — Python 解説ページ +
+
+ + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index a4b0ea3e..03bb0e76 100644 --- a/public/index.html +++ b/public/index.html @@ -416,7 +416,7 @@

🧪 Algorithm Study Index

-

167 interactive lessons across 6 domains

+

168 interactive lessons across 6 domains

@@ -431,11 +431,11 @@

- + @@ -552,6 +552,7 @@

  • 🏗️Largest Rectangle in Histogram - 単調スタックアルゴリズム解説(Tailwind CDN リファクタ)DataStructures/Stacks/leetcode/84. Largest Rectangle in Histogram/Claude/README_tailwind.html
  • 🏗️Largest Rectangle in Histogram — 技術解説(単調増加スタック法 / Python, Tailwind版)DataStructures/Stacks/leetcode/84. Largest Rectangle in Histogram/GPT/README_tailwind.html
  • 🏗️Largest Rectangle in Histogram — 技術解説(単調増加スタック法 / Python)DataStructures/Stacks/leetcode/84. Largest Rectangle in Histogram/GPT/README.html
  • +
  • 🏗️LeetCode #101 Symmetric Tree — 完全解説DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html
  • 🏗️LeetCode 87: Scramble String - Top-down Memoized DFSDataStructures/Trees/BFS・DFS/leetcode/87. Scramble String/Claude/README.html
  • 🏗️LeetCode 87: Scramble String — Top-down recursion with memoization & pruningDataStructures/Trees/BFS・DFS/leetcode/87. Scramble String/GPT/README.html
  • 🏗️LeetCode 92: Reverse Linked List II - 部分区間反転アルゴリズムDataStructures/LinkedLists/leetcode/92. Reverse Linked List II/Claude/README.html
  • @@ -736,6 +737,7 @@

  • 🏗️Largest Rectangle in Histogram - 単調スタックアルゴリズム解説(Tailwind CDN リファクタ)DataStructures/Stacks/leetcode/84. Largest Rectangle in Histogram/Claude/README_tailwind.html
  • 🏗️Largest Rectangle in Histogram — 技術解説(単調増加スタック法 / Python, Tailwind版)DataStructures/Stacks/leetcode/84. Largest Rectangle in Histogram/GPT/README_tailwind.html
  • 🏗️Largest Rectangle in Histogram — 技術解説(単調増加スタック法 / Python)DataStructures/Stacks/leetcode/84. Largest Rectangle in Histogram/GPT/README.html
  • +
  • 🏗️LeetCode #101 Symmetric Tree — 完全解説DataStructures/Trees/Other/101. Symmetric Tree/claude sonnet 4.6 extended/README_react.html
  • 🏗️LeetCode 87: Scramble String - Top-down Memoized DFSDataStructures/Trees/BFS・DFS/leetcode/87. Scramble String/Claude/README.html
  • 🏗️LeetCode 87: Scramble String — Top-down recursion with memoization & pruningDataStructures/Trees/BFS・DFS/leetcode/87. Scramble String/GPT/README.html
  • 🏗️LeetCode 92: Reverse Linked List II - 部分区間反転アルゴリズムDataStructures/LinkedLists/leetcode/92. Reverse Linked List II/Claude/README.html
  • @@ -817,7 +819,7 @@

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