Skip to content

Commit 2ffb28f

Browse files
committed
fix(Trees): optimize TS BFS performance and restructure documentation to 5-section format
1 parent e868f02 commit 2ffb28f

2 files changed

Lines changed: 99 additions & 832 deletions

File tree

Algorithm/BinaryTree/claude sonnet 4.6 extended/102. Binary Tree Level Order Traversal/Binary_Tree_Level_Order_Traversal_Typescript.md

Lines changed: 61 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
66
---
77

8-
# LeetCode 102 · Binary Tree Level Order Traversal
8+
# LeetCode 102 · Binary Tree Level Order Traversal — TypeScript版
99

1010
---
1111

12-
## 1. 問題の分析
12+
## 目次(Table of Contents)
1313

14-
> 💡 **この問題は一言で言うと「木を上から下へ、同じ高さのノードをまとめてグループ化する問題」です。**
15-
> 木の「同じ深さ(階層)」にあるすべての値をひとつの配列にまとめ、その配列を深さ順に並べた2次元配列を返します。
14+
- [1. Overview](#overview)
15+
- [2. Algorithm](#algorithm)
16+
- [3. Complexity](#complexity)
17+
- [4. Implementation](#implementation)
18+
- [5. Optimization](#optimization)
1619

1720
---
1821

19-
### 🌳 問題が要求していること(視覚的確認)
22+
<h2 id="overview">1. Overview</h2>
23+
24+
> 💡 **この問題は一言で言うと「木を上から下へ、同じ高さのノードをまとめてグループ化する問題」です。**
25+
> 木の「同じ深さ(階層)」にあるすべての値をひとつの配列にまとめ、その配列を深さ順に並べた2次元配列を返します。
2026
2127
```
2228
3 ← 深さ0: [3]
@@ -28,24 +34,11 @@
2834
出力: [[3], [9, 20], [15, 7]]
2935
```
3036

31-
---
32-
33-
### 競技プログラミング視点での分析
34-
35-
- ノード数は最大 2000 なので、O(n) のアルゴリズムで十分に余裕がある
36-
- 各ノードを **ちょうど1回だけ** 訪問する手法が理想
37-
- 追加メモリは出力配列のみに抑えたい
38-
39-
### 業務開発視点での分析
37+
### 競技プログラミング・業務開発視点
4038

41-
- `TreeNode | null` という **Union型(=複数の型のうちどれかを表す型)** を安全に扱う必要がある
42-
- `null` チェックを怠ると実行時エラーになるため、型ガード(=実行時に型を絞り込む条件分岐)が必須
43-
- 結果配列はイミュータブル(=変更不可)に構築して副作用を防ぐ
44-
45-
### TypeScript特有の考慮点
46-
47-
- `TreeNode | null` の null 安全性を TypeScript のコンパイラに保証させる
48-
- キュー(後述)の要素型を明示することで、取り出した要素が必ず `TreeNode` 型であると保証できる
39+
- **ノード数**: 最大 2000 なので O(n) が必須。
40+
- **型安全性**: `TreeNode | null` という **Union型** を安全に扱う必要がある。
41+
- **JSの特性**: `Array.shift()` は $O(n)$ のため、キューとして使う場合はポインタ管理($O(1)$)を行う必要がある。
4942

5043
> 📖 **このセクションで登場した用語**
5144
>
@@ -55,237 +48,99 @@
5548
5649
---
5750

58-
## 2. アルゴリズムアプローチ比較
51+
<h2 id="algorithm">2. Algorithm</h2>
5952

60-
> 💡 同じ問題でも解き方は複数あります。「速さ(時間計算量)」と「メモリの使い方(空間計算量)」を比べて最適なものを選びます。
53+
### アプローチ比較
6154

62-
| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 |
63-
| -------------------------- | ---------- | ---------- | ------------ | -------- | ------ | -------------------------------- |
64-
| **BFS(幅優先探索)** | O(n) | O(n) | ||| ✅ 今回の選択 |
65-
| DFS(深さ優先探索) | O(n) | O(n) | ||| 再帰でも実装可能だが直感的でない |
66-
| 総当たり(各深さでループ) | O(n²) | O(n) | ||| 非推奨 |
55+
| アプローチ | 時間計算量 | 空間計算量 | 備考 |
56+
| -------------------------- | ---------- | ---------- | -------------------------------- |
57+
| **BFS(幅優先探索)** | O(n) | O(n) | ✅ 今回の選択 |
58+
| DFS(深さ優先探索) | O(n) | O(n) | 再帰でも実装可能だが直感的でない |
59+
| 総当たり(各深さでループ) | O(n²) | O(n) | 非推奨 |
6760

68-
> 💡 **BFS(幅優先探索)を例え話で理解する**
69-
> BFS は「同じ階のすべての部屋を開けてから、次の階に進むエレベーター」のようなものです。
70-
> 木でいえば「同じ深さのノードをすべて処理してから、次の深さへ進む」動き方です。
71-
> これを実現するのが **キュー(=先に入れたものを先に出す「行列」のデータ構造)** です。
72-
> 📖 **このセクションで登場した用語**
73-
>
74-
> - **BFS(幅優先探索)**:グラフや木を「横方向に広がりながら」探索する方法
75-
> - **DFS(深さ優先探索)**:グラフや木を「縦方向に深く潜りながら」探索する方法
76-
> - **キュー**:先に入れたものを先に出す(FIFO: First In, First Out)データ構造。銀行の窓口の行列と同じ
61+
### BFS(幅優先探索)の仕組み
7762

78-
---
63+
BFS は「同じ階のすべての部屋を開けてから、次の階に進むエレベーター」のようなものです。
64+
これを実現するのが **キュー(FIFO: First In, First Out)** です。
7965

80-
## 3. 選択したアルゴリズムと理由
66+
- **核心テクニック**: キューから「今の階のノード数分だけ」取り出すことで、自然に「1階ぶんのグループ」が作れる。
67+
- **TypeScript特有の工夫**: キューの型を `TreeNode[]` と明示することで、取り出した要素が必ず `TreeNode` 型になりコンパイル時に安全を保証。
8168

82-
- **選択したアプローチ**: **BFS(幅優先探索)+ キュー**
83-
- **理由**:
84-
- **DFS を選ばなかった理由**: DFS は縦に深く潜るため、「同じ階のノードをまとめる」処理と相性が悪く、深さ情報を別途管理する手間が増える
85-
- **総当たりを選ばなかった理由**: ノードを重複して走査するため O(n²) になり、ノード数 2000 でも無駄が大きい
86-
- **BFS を選んだ理由**: キューから「今の階のノード数分だけ」取り出すことで、自然に「1階ぶんのグループ」が作れる
69+
---
8770

88-
- **TypeScript特有の最適化ポイント**:
89-
- キューの型を `TreeNode[]` と明示することで、取り出した要素が必ず `TreeNode` 型になりコンパイル時に安全を保証
90-
- `node.left` / `node.right``TreeNode | null` なので、`!== null` チェックで null を排除してからキューに追加する
71+
<h2 id="complexity">3. Complexity</h2>
9172

92-
> 📖 **このセクションで登場した用語**
93-
>
94-
> - **コンパイル時保証**:TypeScript がコードを変換する段階でエラーを検出すること。実行前にバグを防げる
95-
> - **FIFO**:First In, First Out。最初に入れたものを最初に取り出す順序
73+
| 項目 || 理由 |
74+
| -------------- | ---- | -------------------------------------------------------------------------------------------- |
75+
| **時間計算量** | O(n) | 各ノードをキューへの追加・取り出しでちょうど1回ずつ処理するため(ポインタ管理で各操作 O(1)) |
76+
| **空間計算量** | O(n) | キューに最大で「木の最も広い階のノード数」が入る。最悪ケースは全ノード数 n に比例 |
9677

9778
---
9879

99-
## 4. 実装コード
80+
<h2 id="implementation">4. Implementation</h2>
10081

101-
> 💡 **コードの大まかな骨格**(コードを読む前にこの構造を頭に入れてください)
102-
>
103-
> 1. `root``null` なら即座に空配列 `[]` を返す
104-
> 2. `root` をキューに入れて探索開始
105-
> 3. キューが空になるまで繰り返す
106-
> - 今の階のノード数(`levelSize`)を記録する
107-
> - ちょうど `levelSize` 個のノードを取り出し、その値を今の階の配列に追加
108-
> - 取り出したノードの左・右の子があればキューに追加(次の階の準備)
109-
> 4. 各階の配列を `result` に追加して返す
82+
### 業務開発版(型安全・パフォーマンス最適化)
11083

11184
```typescript
112-
// Runtime 2 ms
113-
// Beats 39.72%
114-
// Memory 60.20 MB
115-
// Beats 57.23%
116-
11785
function levelOrder(root: TreeNode | null): number[][] {
118-
// ─────────────────────────────────────────────────────────
119-
// ① root が null(木が空)の場合は空配列を返す
120-
// 後続でキューにアクセスするため、ここで弾かないと
121-
// null に対して .left などへアクセスしてクラッシュする
122-
// ─────────────────────────────────────────────────────────
12386
if (root === null) return [];
12487

125-
// ─────────────────────────────────────────────────────────
126-
// ② 結果を格納する2次元配列を用意する
127-
// result[0] = 深さ0のノード値の配列、
128-
// result[1] = 深さ1のノード値の配列、... となる
129-
// ─────────────────────────────────────────────────────────
13088
const result: number[][] = [];
131-
132-
// ─────────────────────────────────────────────────────────
133-
// ③ キュー(行列)を用意し、root を最初の要素として入れる
134-
// 型を TreeNode[] と明示することで、取り出した要素が
135-
// null でないことをコンパイラが保証してくれる
136-
// (null をキューに入れないよう、後続の追加時にチェックする)
137-
// ─────────────────────────────────────────────────────────
13889
const queue: TreeNode[] = [root];
90+
let head = 0; // shift() の O(n) を避けるための先頭ポインタ
13991

140-
// ─────────────────────────────────────────────────────────
141-
// ④ キューが空になるまでループ(= 全ノードを処理し終えるまで)
142-
// ─────────────────────────────────────────────────────────
143-
while (queue.length > 0) {
92+
while (head < queue.length) {
14493
// ── 今この瞬間のキューの長さ = 「現在の階のノード数」 ──
145-
// この値を先に固定するのが BFS の核心。
146-
// ループ中に queue.length は変化するため、
147-
// 先に変数に保存しておかないと「今の階」の範囲がズレる。
148-
const levelSize: number = queue.length;
149-
150-
// 今の階のノード値を格納する一時配列
94+
// 未処理分(queue.length - head)を変数に保存して固定する。
95+
const levelSize: number = queue.length - head;
15196
const levelValues: number[] = [];
15297

153-
// 今の階のノードを「levelSize 個分」だけ取り出す
15498
for (let i = 0; i < levelSize; i++) {
155-
// shift() でキューの先頭からノードを取り出す(FIFO)
156-
// queue の要素は TreeNode[] なので null にならない(③で保証)
157-
const node = queue.shift()!; // ! は「nullでないことをここで断言」
158-
159-
// 取り出したノードの値を今の階の配列に追加
99+
// head インデックスでポインタを進めることで O(1) で取り出す
100+
const node = queue[head++]!;
160101
levelValues.push(node.val);
161102

162-
// ── 次の階の準備:子ノードをキューの末尾に追加する ──
163-
// null でない場合だけ追加する(null をキューに入れないルール)
164-
if (node.left !== null) {
165-
queue.push(node.left);
166-
}
167-
if (node.right !== null) {
168-
queue.push(node.right);
169-
}
103+
// 次の階の準備
104+
if (node.left !== null) queue.push(node.left);
105+
if (node.right !== null) queue.push(node.right);
170106
}
171107

172-
// 今の階分の値配列を結果に追加
173108
result.push(levelValues);
174109
}
175110

176111
return result;
177112
}
178113
```
179114

180-
---
181-
182-
### 🔍 動作トレース(具体的な入力例での変数変化)
183-
184-
**入力**: `root = [3, 9, 20, null, null, 15, 7]`(ツリー構造は下記)
185-
186-
```
187-
3
188-
/ \
189-
9 20
190-
/ \
191-
15 7
192-
```
115+
### 🔍 動作トレース(`root = [3, 9, 20, null, null, 15, 7]`
193116

194117
```
195-
初期状態:
196-
queue = [Node(3)]
197-
result = []
198-
199118
━━━━━━━━━━ while ループ 1回目(深さ0) ━━━━━━━━━━
200-
levelSize = 1 ← 今の階のノード数は1
201-
levelValues = []
202-
203-
i=0: node = queue.shift() → Node(3) queue = []
204-
levelValues.push(3) → [3]
205-
node.left = Node(9) → queue.push(Node(9)) queue = [Node(9)]
206-
node.right = Node(20) → queue.push(Node(20)) queue = [Node(9), Node(20)]
207-
208-
result.push([3]) → result = [[3]]
119+
levelSize = 1 ← queue.length(1) - head(0) = 1
120+
i=0: node = queue[head++] → Node(3) queue = [3], head = 1
121+
node.left = 9, node.right = 20 を queue に追加
122+
result.push([3])
209123
210124
━━━━━━━━━━ while ループ 2回目(深さ1) ━━━━━━━━━━
211-
levelSize = 2 ← 今の階のノード数は2
212-
levelValues = []
213-
214-
i=0: node = queue.shift() → Node(9) queue = [Node(20)]
215-
levelValues.push(9) → [9]
216-
node.left = null → スキップ
217-
node.right = null → スキップ
218-
219-
i=1: node = queue.shift() → Node(20) queue = []
220-
levelValues.push(20) → [9, 20]
221-
node.left = Node(15) → queue.push(Node(15)) queue = [Node(15)]
222-
node.right = Node(7) → queue.push(Node(7)) queue = [Node(15), Node(7)]
223-
224-
result.push([9,20]) → result = [[3], [9, 20]]
225-
226-
━━━━━━━━━━ while ループ 3回目(深さ2) ━━━━━━━━━━
227-
levelSize = 2 ← 今の階のノード数は2
228-
levelValues = []
229-
230-
i=0: node = queue.shift() → Node(15) queue = [Node(7)]
231-
levelValues.push(15) → [15]
232-
node.left = null → スキップ
233-
node.right = null → スキップ
234-
235-
i=1: node = queue.shift() → Node(7) queue = []
236-
levelValues.push(7) → [15, 7]
237-
node.left = null → スキップ
238-
node.right = null → スキップ
239-
240-
result.push([15,7]) → result = [[3], [9, 20], [15, 7]]
241-
242-
━━━━━━━━━━ queue が空 → ループ終了 ━━━━━━━━━━
243-
戻り値: [[3], [9, 20], [15, 7]] ✅
125+
levelSize = 2 ← queue.length(3) - head(1) = 2
126+
i=0, 1: Node(9), Node(20) を処理
127+
result.push([9, 20])
244128
```
245129

246130
---
247131

248-
### ✅ LeetCode 提出フォーマット(そのままコピー可)
249-
250-
```typescript
251-
function levelOrder(root: TreeNode | null): number[][] {
252-
if (root === null) return [];
253-
254-
const result: number[][] = [];
255-
const queue: TreeNode[] = [root];
256-
257-
while (queue.length > 0) {
258-
const levelSize: number = queue.length;
259-
const levelValues: number[] = [];
260-
261-
for (let i = 0; i < levelSize; i++) {
262-
const node = queue.shift()!;
263-
levelValues.push(node.val);
132+
<h2 id="optimization">5. Optimization</h2>
264133

265-
if (node.left !== null) queue.push(node.left);
266-
if (node.right !== null) queue.push(node.right);
267-
}
134+
### パフォーマンス最適化:`Array.shift()` の回避
268135

269-
result.push(levelValues);
270-
}
136+
JavaScript の `Array.shift()` は、先頭要素を削除した後に残り全要素のインデックスを 1 つずつ前にずらすため、**$O(n)$ の計算量**がかかります。
137+
キュー操作を伴う BFS で `shift()` をループ内で使用すると、全体の計算量が **$O(n^2)$** に悪化してしまいます。
271138

272-
return result;
273-
}
274-
```
139+
**解決策:**
140+
`head` 変数を用いて現在の先頭位置を指し示し、要素を取り出すたびに `head++` する方式を採用します。これにより、実質的な削除操作を伴わずに $O(1)$ で先頭要素を取得できます。
275141

276142
> 📖 **このセクションで登場した用語**
277143
>
278-
> - **`shift()`**:配列の先頭要素を取り出して返すメソッド。キューの「取り出し」操作に使う
279-
> - **`push()`**:配列の末尾に要素を追加するメソッド。キューの「追加」操作に使う
280-
> - **`!`(非null断言)**:TypeScriptに「この値は絶対 null でない」と伝える記号。型チェックを強制的に通す代わりに、間違えると実行時エラーになるため、確信がある箇所にのみ使う
281-
> - **FIFO**:First In First Out。先に入れたものを先に取り出す順序。キューはこの性質を持つ
282-
> - **`readonly`(今回不使用だが参考)**:変数を変更不可にする TypeScript の修飾子。JavaScript には存在せず、コンパイル時の書き換えを防ぐために使う
283-
284-
---
285-
286-
## 5. 計算量まとめ
287-
288-
| 項目 || 理由 |
289-
| -------------- | ---- | --------------------------------------------------------------------------------- |
290-
| **時間計算量** | O(n) | 各ノードをキューへの追加・取り出しでちょうど1回ずつ処理するため |
291-
| **空間計算量** | O(n) | キューに最大で「木の最も広い階のノード数」が入る。最悪ケースは全ノード数 n に比例 |
144+
> - **`head` インデックス**:配列の先頭を指すポインタ。
145+
> - **FIFO**:First In First Out。先に入れたものを先に取り出す順序。
146+
> - **`!`(非null断言)**:TypeScriptに「この値は絶対 null でない」と伝える記号。

0 commit comments

Comments
 (0)