|
| 1 | +> 🎯 **[algo-beginner スキル発火]** |
| 2 | +> 言語/カテゴリ: Rust |
| 3 | +> 適用ルールセット: 共通5ルール + Rust固有5ルール |
| 4 | +> 参照ファイル: references/common.md + references/rust.md |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +# LeetCode 102 · Binary Tree Level Order Traversal — Rust版 |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 1. 問題の分析 |
| 13 | + |
| 14 | +> 💡 **この問題は一言で言うと「木を上から下へ、同じ高さのノードをまとめてグループ化する問題」です。** |
| 15 | +
|
| 16 | +``` |
| 17 | + 3 ← 深さ0: [3] |
| 18 | + / \ |
| 19 | + 9 20 ← 深さ1: [9, 20] |
| 20 | + / \ |
| 21 | + 15 7 ← 深さ2: [15, 7] |
| 22 | +
|
| 23 | +出力: [[3], [9, 20], [15, 7]] |
| 24 | +``` |
| 25 | + |
| 26 | +**Rustで解く際に特に気をつけるべき点:** |
| 27 | +LeetCode の Rust 環境では、ツリーノードは `Option<Rc<RefCell<TreeNode>>>` という複合型で表現されています。`Rc<T>`(=参照カウント型の共有ポインタ)と `RefCell<T>`(=実行時に借用チェックを行う内部可変性コンテナ)の組み合わせで「1つのノードを複数の親から共有できる」構造を表現しています。この型を安全に扱うパターンを正確に押さえることが、この問題のRust実装の核心です。 |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +### 競技プログラミング視点での分析 |
| 32 | + |
| 33 | +- ノード数は最大 2000 なので O(n) 単一パスで十分 |
| 34 | +- キュー(`VecDeque<T>`、=両端から追加・取り出しができるキュー)を使い、各ノードをちょうど1回だけ処理する |
| 35 | +- `Rc::clone()` は参照カウントのインクリメントのみで、データのコピーは行わないためオーバーヘッドは小さい |
| 36 | + |
| 37 | +### 業務開発視点での分析 |
| 38 | + |
| 39 | +- `Option<Rc<RefCell<TreeNode>>>` は「値がある/ない」を型で安全に表現する。Javaの `null` と異なり、取り出す前に `None` チェックが強制されるためNullPointerException相当のバグが起きない |
| 40 | +- `borrow()` の呼び出しで実行時借用チェックが行われ、同時ミュータブルアクセスが自動的に防止される |
| 41 | + |
| 42 | +### Rust特有の考慮点 |
| 43 | + |
| 44 | +- **所有権の移動(move)を避ける**: `Rc::clone(&node)` は所有権を移さず参照カウントをインクリメントするだけ。`node.clone()` と書いても同じ意味だが `Rc::clone()` と書くことでRustの慣習では「安価なクローン」であることを明示できる |
| 45 | +- **`borrow()` vs `borrow_mut()`**: 今回は読み取りのみなので `borrow()`(共有参照)を使う。`borrow_mut()`(排他参照)は不要 |
| 46 | + |
| 47 | +> 📖 **このセクションで登場した用語** |
| 48 | +> |
| 49 | +> - **`Rc<T>`**:Reference Counted。参照カウント(=何箇所から参照されているかを数える)によって、1つの値を複数の場所から所有できるスマートポインタ。スレッドをまたぐ場合は `Arc<T>` を使う |
| 50 | +> - **`RefCell<T>`**:通常はコンパイル時に行う借用チェックを実行時に行うコンテナ。これにより「コンパイル時には分からない条件分岐に応じた可変借用」を安全に実現できる |
| 51 | +> - **内部可変性**:外から見ると不変(`&T`)なのに、内部だけ変更できる仕組み。`RefCell` がその代表例 |
| 52 | +> - **参照カウント**:ある値を指しているポインタの数を記録し、0になったら自動的にメモリを解放する仕組み |
| 53 | +
|
| 54 | +--- |
| 55 | + |
| 56 | +## 2. アルゴリズムアプローチ比較 |
| 57 | + |
| 58 | +> 💡 同じ問題でも解き方は複数あります。「速さ(時間計算量)」「メモリ」「Rustの所有権モデルとの相性」を合わせて比べます。 |
| 59 | +
|
| 60 | +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | |
| 61 | +| -------------------- | ---------- | ---------- | -------------- | ------ | ------ | ------------------------------------------------ | |
| 62 | +| **BFS + `VecDeque`** | O(n) | O(n) | 低 | 高 | 高 | ✅ 今回の選択 | |
| 63 | +| DFS(再帰) | O(n) | O(n) | 中 | 高 | 中 | スタックオーバーフローリスク、深さ管理が必要 | |
| 64 | +| DFS(反復) | O(n) | O(n) | 中 | 高 | 中 | `Vec` をスタックとして使う。深さ情報の追跡が必要 | |
| 65 | + |
| 66 | +**Rust固有の観点**: |
| 67 | + |
| 68 | +- BFS + `VecDeque` は「今の階のノード数分だけ `pop_front()` する」パターンで自然に階層分けができ、Rcの借用スコープも短く済む |
| 69 | +- DFS再帰版は `Rc::clone` の呼び出し回数は同じだが、再帰の深さがツリーの高さに比例してスタックを消費するため、バランスの悪い木では不利 |
| 70 | + |
| 71 | +> 📖 **このセクションで登場した用語** |
| 72 | +> |
| 73 | +> - **`VecDeque<T>`**:`std::collections::VecDeque`。両端キュー(=前からも後ろからも要素を追加・取り出しできるデータ構造)。`Vec<T>` の先頭への挿入は O(n) だが `VecDeque` は O(1) でできる |
| 74 | +> - **スタックオーバーフロー**:再帰が深くなりすぎてプログラムが使えるスタックメモリを使い尽くしてしまうエラー |
| 75 | +
|
| 76 | +--- |
| 77 | + |
| 78 | +## 3. 選択したアルゴリズムと理由 |
| 79 | + |
| 80 | +- **選択したアプローチ**: **BFS(幅優先探索)+ `VecDeque`** |
| 81 | +- **理由**: |
| 82 | + - **DFS再帰を選ばなかった理由**: 深さ情報を引数として持ち回る設計が必要になり、`Rc::clone` のスコープが広がって借用チェッカーとの格闘が増える |
| 83 | + - **DFS反復を選ばなかった理由**: `Vec` をスタックとして使うと「今の階」の境界を別途追跡しなければならず、コードの見通しが悪くなる |
| 84 | + - **BFSを選んだ理由**: `VecDeque` の長さを「今の階のサイズ」として使うことで、深さ情報を持ち回らずに階層分けが自然に実現できる |
| 85 | + |
| 86 | +- **Rust特有の最適化ポイント**: |
| 87 | + - `Rc::clone(&node)` は参照カウントのインクリメントのみ。ヒープアロケーション(=ヒープ上に新しくメモリを確保する操作)は発生しない |
| 88 | + - `borrow()` の返す `Ref<T>`(=`RefCell` の共有借用ガード)はスコープを出ると自動的に解放されるため、借用の管理が明確 |
| 89 | + |
| 90 | +> 📖 **このセクションで登場した用語** |
| 91 | +> |
| 92 | +> - **ゼロコスト抽象化**:便利な高レベルな書き方をしても、手書きの低レベルコードと同じ速さになるRustの特性 |
| 93 | +> - **`Ref<T>`**:`RefCell<T>` の `borrow()` が返す型。スコープを抜けると自動的に借用が解放されるRAIIガード |
| 94 | +
|
| 95 | +--- |
| 96 | + |
| 97 | +## 4. 実装コード |
| 98 | + |
| 99 | +> 💡 **コードの大まかな骨格** |
| 100 | +> |
| 101 | +> 1. `root` が `None` なら空 `Vec` を即返す |
| 102 | +> 2. `VecDeque` にルートを積んで BFS 開始 |
| 103 | +> 3. ループ毎に「今の階のサイズ」を `len()` で固定し、その分だけ `pop_front()` |
| 104 | +> 4. 各ノードの値を今の階の配列に追加し、`left`/`right` の子があれば次の階としてキューに積む |
| 105 | +> 5. 階ごとの配列を `result` に追加して返す |
| 106 | +
|
| 107 | +```rust |
| 108 | +use std::cell::RefCell; |
| 109 | +use std::collections::VecDeque; |
| 110 | +use std::rc::Rc; |
| 111 | + |
| 112 | +// LeetCode が提供する TreeNode 定義(提出時はそのまま使う) |
| 113 | +// #[derive(Debug, PartialEq, Eq)] |
| 114 | +// pub struct TreeNode { |
| 115 | +// pub val: i32, |
| 116 | +// pub left: Option<Rc<RefCell<TreeNode>>>, |
| 117 | +// pub right: Option<Rc<RefCell<TreeNode>>>, |
| 118 | +// } |
| 119 | + |
| 120 | +impl Solution { |
| 121 | + pub fn level_order(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> { |
| 122 | + // ─────────────────────────────────────────────────────────────── |
| 123 | + // ① root が None(木が空)の場合は空ベクタを返す |
| 124 | + // Option<T> を「中身があるかないかが分かる箱」として扱う。 |
| 125 | + // Javaの null と異なり、None チェックを忘れるとコンパイルエラーになるため |
| 126 | + // ここで弾き忘れることが構造上ありえない。 |
| 127 | + // ─────────────────────────────────────────────────────────────── |
| 128 | + let root = match root { |
| 129 | + None => return vec![], // None なら即終了 |
| 130 | + Some(node) => node, // Some なら中身(Rc<RefCell<TreeNode>>)を取り出す |
| 131 | + }; |
| 132 | + |
| 133 | + // ─────────────────────────────────────────────────────────────── |
| 134 | + // ② 最終的な結果を格納する2次元ベクタ |
| 135 | + // result[0] = 深さ0のノード値の配列、result[1] = 深さ1 ... となる |
| 136 | + // ─────────────────────────────────────────────────────────────── |
| 137 | + let mut result: Vec<Vec<i32>> = Vec::new(); |
| 138 | + |
| 139 | + // ─────────────────────────────────────────────────────────────── |
| 140 | + // ③ 両端キュー(VecDeque)を用意し、ルートノードを入れる |
| 141 | + // VecDeque を使う理由:Vec の先頭取り出しは O(n) だが |
| 142 | + // VecDeque の pop_front() は O(1) で済むため、キューとして最適 |
| 143 | + // ─────────────────────────────────────────────────────────────── |
| 144 | + let mut queue: VecDeque<Rc<RefCell<TreeNode>>> = VecDeque::new(); |
| 145 | + queue.push_back(root); // ルートをキューに追加 |
| 146 | + |
| 147 | + // ─────────────────────────────────────────────────────────────── |
| 148 | + // ④ キューが空になるまでループ(= 全ノードを処理し終えるまで) |
| 149 | + // ─────────────────────────────────────────────────────────────── |
| 150 | + while !queue.is_empty() { |
| 151 | + // 今この瞬間のキューの長さ = 「現在の階のノード数」 |
| 152 | + // この値を先に固定するのが BFS の核心。 |
| 153 | + // ループ中に queue.len() は変化するため、変数に保存しておく。 |
| 154 | + let level_size = queue.len(); |
| 155 | + |
| 156 | + // 今の階のノード値を格納する一時ベクタ |
| 157 | + let mut level_values: Vec<i32> = Vec::with_capacity(level_size); |
| 158 | + // with_capacity(n) は「n個分のメモリを事前に確保」する。 |
| 159 | + // push のたびに再アロケーションが起きるのを防ぐため。 |
| 160 | + |
| 161 | + // 今の階のノードを「level_size 個分」だけ取り出す |
| 162 | + for _ in 0..level_size { |
| 163 | + // pop_front() でキューの先頭からノードを O(1) で取り出す |
| 164 | + // while !is_empty() のループ内なので必ず Some になるが、 |
| 165 | + // 安全のため unwrap() ではなく if let で受け取る |
| 166 | + let node_rc = queue.pop_front().unwrap(); |
| 167 | + // unwrap() を使う根拠:直上の is_empty() チェックにより |
| 168 | + // pop_front() が None を返すことはこのスコープでありえない |
| 169 | + |
| 170 | + // ────────────────────────────────────────────────────── |
| 171 | + // ⑤ RefCell の borrow() で共有参照を取得する |
| 172 | + // borrow() は「読み取り専用の貸し出し」を意味する。 |
| 173 | + // borrow_mut() は「書き込み可能な貸し出し」で、同時に |
| 174 | + // 1つしか存在できない(実行時に panic する)。 |
| 175 | + // 今回は読み取りのみなので borrow() で十分。 |
| 176 | + // ────────────────────────────────────────────────────── |
| 177 | + let node = node_rc.borrow(); |
| 178 | + // node は Ref<TreeNode> 型。スコープを抜けると自動解放される。 |
| 179 | + |
| 180 | + // ノードの値を今の階の配列に追加 |
| 181 | + level_values.push(node.val); |
| 182 | + |
| 183 | + // ────────────────────────────────────────────────────── |
| 184 | + // ⑥ 子ノードをキューに追加する(次の階の準備) |
| 185 | + // Rc::clone(&child) は所有権を移さず参照カウントを増やすだけ。 |
| 186 | + // 「clone」という名前だがヒープ上のデータはコピーされない(安価)。 |
| 187 | + // JavaやPythonでの参照コピーと同じ感覚で使える。 |
| 188 | + // ────────────────────────────────────────────────────── |
| 189 | + if let Some(left) = &node.left { |
| 190 | + queue.push_back(Rc::clone(left)); |
| 191 | + } |
| 192 | + if let Some(right) = &node.right { |
| 193 | + queue.push_back(Rc::clone(right)); |
| 194 | + } |
| 195 | + // node(Ref<TreeNode>)はここでスコープを抜け、borrow が解放される |
| 196 | + } |
| 197 | + |
| 198 | + // 今の階の値配列を結果に追加 |
| 199 | + result.push(level_values); |
| 200 | + } |
| 201 | + |
| 202 | + result |
| 203 | + } |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +--- |
| 208 | + |
| 209 | +### 🔍 動作トレース(`root = [3, 9, 20, null, null, 15, 7]`) |
| 210 | + |
| 211 | +``` |
| 212 | +ツリー: |
| 213 | + 3 |
| 214 | + / \ |
| 215 | + 9 20 |
| 216 | + / \ |
| 217 | + 15 7 |
| 218 | +
|
| 219 | +初期状態: |
| 220 | + queue = [Node(3)] |
| 221 | + result = [] |
| 222 | +
|
| 223 | +━━━━━━━━━━ while ループ 1回目(深さ0) ━━━━━━━━━━ |
| 224 | + level_size = 1 |
| 225 | + level_values = [] |
| 226 | +
|
| 227 | + i=0: pop_front() → node_rc = Rc<Node(3)> queue = [] |
| 228 | + node = node_rc.borrow() → val=3 |
| 229 | + level_values.push(3) → [3] |
| 230 | + left = Some(Node(9)) → Rc::clone → queue.push_back queue = [Node(9)] |
| 231 | + right = Some(Node(20)) → Rc::clone → queue.push_back queue = [Node(9), Node(20)] |
| 232 | + node(Ref)スコープ終了 → borrow 解放 |
| 233 | +
|
| 234 | + result.push([3]) → result = [[3]] |
| 235 | +
|
| 236 | +━━━━━━━━━━ while ループ 2回目(深さ1) ━━━━━━━━━━ |
| 237 | + level_size = 2 |
| 238 | + level_values = [] |
| 239 | +
|
| 240 | + i=0: pop_front() → node_rc = Rc<Node(9)> queue = [Node(20)] |
| 241 | + node.val = 9 level_values = [9] |
| 242 | + left = None → スキップ |
| 243 | + right= None → スキップ |
| 244 | +
|
| 245 | + i=1: pop_front() → node_rc = Rc<Node(20)] queue = [] |
| 246 | + node.val = 20 level_values = [9, 20] |
| 247 | + left = Some(Node(15)) → queue = [Node(15)] |
| 248 | + right = Some(Node(7)) → queue = [Node(15), Node(7)] |
| 249 | +
|
| 250 | + result.push([9,20]) → result = [[3], [9, 20]] |
| 251 | +
|
| 252 | +━━━━━━━━━━ while ループ 3回目(深さ2) ━━━━━━━━━━ |
| 253 | + level_size = 2 |
| 254 | + level_values = [] |
| 255 | +
|
| 256 | + i=0: pop_front() → Node(15) queue = [Node(7)] |
| 257 | + node.val = 15 level_values = [15] |
| 258 | + left = None, right = None → スキップ |
| 259 | +
|
| 260 | + i=1: pop_front() → Node(7) queue = [] |
| 261 | + node.val = 7 level_values = [15, 7] |
| 262 | + left = None, right = None → スキップ |
| 263 | +
|
| 264 | + result.push([15,7]) → result = [[3], [9, 20], [15, 7]] |
| 265 | +
|
| 266 | +━━━━━━━━━━ queue が空 → ループ終了 ━━━━━━━━━━ |
| 267 | +戻り値: [[3], [9, 20], [15, 7]] ✅ |
| 268 | +``` |
| 269 | + |
| 270 | +--- |
| 271 | + |
| 272 | +### 💡 `Rc::clone()` の動きを図で理解する |
| 273 | + |
| 274 | +``` |
| 275 | +Rc::clone(&node) を呼ぶ前: |
| 276 | + Rc<Node(20)> ← 参照カウント = 1(queue が所持) |
| 277 | +
|
| 278 | +Rc::clone(&node.left) 後: |
| 279 | + Rc<Node(9)> ← 参照カウント = 2(旧 queue + 新 queue が所持) |
| 280 | +
|
| 281 | +pop_front() で旧 queue から取り出され、スコープ終了時: |
| 282 | + Rc<Node(9)> ← 参照カウント = 1(新 queue のみ) |
| 283 | + → カウントが 0 にならない限りメモリは解放されない |
| 284 | + → カウントが 0 になったとき初めて Drop(メモリ解放)が呼ばれる |
| 285 | +``` |
| 286 | + |
| 287 | +--- |
| 288 | + |
| 289 | +### ✅ LeetCode 提出フォーマット(そのままコピー可) |
| 290 | + |
| 291 | +```rust |
| 292 | +// Runtime 0 ms |
| 293 | +// Beats 100.00% |
| 294 | +// Memory 2.39 MB |
| 295 | +// Beats 87.85% |
| 296 | + |
| 297 | +use std::cell::RefCell; |
| 298 | +use std::collections::VecDeque; |
| 299 | +use std::rc::Rc; |
| 300 | + |
| 301 | +impl Solution { |
| 302 | + pub fn level_order(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> { |
| 303 | + let root = match root { |
| 304 | + None => return vec![], |
| 305 | + Some(node) => node, |
| 306 | + }; |
| 307 | + |
| 308 | + let mut result: Vec<Vec<i32>> = Vec::new(); |
| 309 | + let mut queue: VecDeque<Rc<RefCell<TreeNode>>> = VecDeque::new(); |
| 310 | + queue.push_back(root); |
| 311 | + |
| 312 | + while !queue.is_empty() { |
| 313 | + let level_size = queue.len(); |
| 314 | + let mut level_values: Vec<i32> = Vec::with_capacity(level_size); |
| 315 | + |
| 316 | + for _ in 0..level_size { |
| 317 | + let node_rc = queue.pop_front().unwrap(); |
| 318 | + let node = node_rc.borrow(); |
| 319 | + level_values.push(node.val); |
| 320 | + |
| 321 | + if let Some(left) = &node.left { |
| 322 | + queue.push_back(Rc::clone(left)); |
| 323 | + } |
| 324 | + if let Some(right) = &node.right { |
| 325 | + queue.push_back(Rc::clone(right)); |
| 326 | + } |
| 327 | + } |
| 328 | + |
| 329 | + result.push(level_values); |
| 330 | + } |
| 331 | + |
| 332 | + result |
| 333 | + } |
| 334 | +} |
| 335 | +``` |
| 336 | + |
| 337 | +> 📖 **このセクションで登場した用語** |
| 338 | +> |
| 339 | +> - **`VecDeque::pop_front()`**:キューの先頭要素を O(1) で取り出し、`Option<T>` で返すメソッド。`Vec::remove(0)` は O(n) なので絶対に使わない |
| 340 | +> - **`Vec::with_capacity(n)`**:n 個分のメモリを事前確保してベクタを作る。後から `push` しても再アロケーションが発生しない |
| 341 | +> - **`Ref<T>`**:`RefCell<T>::borrow()` が返す型。RAII(=スコープを抜けると自動でリソースを解放する仕組み)により、スコープ終了時に借用ガードが自動解放される |
| 342 | +> - **`if let Some(x) = &opt`**:`Option` が `Some` のときだけ中身を取り出してブロックを実行するパターンマッチの省略形。`match` の代替として読みやすい |
| 343 | +> - **RAII**:Resource Acquisition Is Initialization。変数のスコープと資源(メモリ・ファイル・ロックなど)の有効期間を一致させるRustの根本的な設計思想 |
| 344 | +
|
| 345 | +--- |
| 346 | + |
| 347 | +## 5. 計算量まとめ |
| 348 | + |
| 349 | +| 項目 | 値 | 理由 | |
| 350 | +| -------------- | ---- | ------------------------------------------------------------------------------- | |
| 351 | +| **時間計算量** | O(n) | 各ノードをキューへの追加・取り出しで1回ずつ処理。`Rc::clone` は O(1) | |
| 352 | +| **空間計算量** | O(n) | キューに最大で「最も広い階のノード数」が入る。最悪 n/2 個(完全二分木の最下層) | |
0 commit comments