Day 4: ループ - 解説
学習の目的
このDayで達成すること
- 繰り返し処理ができる
- ループ制御ができる
- ネストしたループが書ける
---
学習の手引き
推奨学習順序
- 背景知識を読む(20分)
- 課題1に取り組む(30分)
- 課題2に取り組む(40分)
- 課題3に取り組む(60分)
- 解答と比較(20分)
効果的な学習のコツ
実験的アプローチ
// ループの動きを可視化
for i := 0; i < 5; i++ {
fmt.Printf("[DEBUG] i=%d, i*2=%d\n", i, i*2)
}
出力:
[DEBUG] i=0, i*2=0
[DEBUG] i=1, i*2=2
[DEBUG] i=2, i*2=4
[DEBUG] i=3, i*2=6
[DEBUG] i=4, i*2=8
ループ回数の予測
// 質問: このループは何回実行される?
for i := 1; i <= 10; i++ {
fmt.Println(i)
}
// 答え: 10回(1, 2, 3, ..., 10)
// 質問: このループは何回実行される?
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 答え: 10回(0, 1, 2, ..., 9)
// 質問: このループは何回実行される?
for i := 0; i <= 10; i++ {
fmt.Println(i)
}
// 答え: 11回(0, 1, 2, ..., 10)
---
概念の深掘り
ループの実行フロー
forループの解剖
for 初期化; 条件; 更新 {
// ループ本体
}
実行順序:
1. 初期化(1回だけ)
2. 条件チェック
3. 条件がtrueなら本体実行、falseならループ終了
4. 更新
5. ステップ2に戻る
具体例での詳細追跡
for i := 0; i < 3; i++ {
fmt.Println(i)
}
ステップバイステップ:
ステップ1: i := 0 (初期化)
ステップ2: i < 3? → true (0 < 3)
ステップ3: fmt.Println(0) (本体実行)
ステップ4: i++ (i = 1)
ステップ5: i < 3? → true (1 < 3)
ステップ6: fmt.Println(1) (本体実行)
ステップ7: i++ (i = 2)
ステップ8: i < 3? → true (2 < 3)
ステップ9: fmt.Println(2) (本体実行)
ステップ10: i++ (i = 3)
ステップ11: i < 3? → false (3 < 3は偽)
ステップ12: ループ終了
Off-by-One エラーの理解
よくある間違いパターン
// パターン1: 最後の要素を処理しない
arr := []int{10, 20, 30, 40, 50}
// ❌ 間違い
for i := 0; i < len(arr)-1; i++ { // 0,1,2,3 のみ(4つ)
fmt.Println(arr[i])
}
// 出力: 10, 20, 30, 40(50が出力されない)
// ✅ 正しい
for i := 0; i < len(arr); i++ { // 0,1,2,3,4(5つ)
fmt.Println(arr[i])
}
// 出力: 10, 20, 30, 40, 50
// パターン2: 1回多く実行
// ❌ 間違い
for i := 0; i <= 10; i++ { // 11回実行
fmt.Println(i)
}
// 出力: 0, 1, 2, ..., 10(11個)
// ✅ 正しい(10回実行したい場合)
for i := 0; i < 10; i++ { // 10回実行
fmt.Println(i)
}
// 出力: 0, 1, 2, ..., 9(10個)
境界値の考え方
目的: 1から10まで表示(10回実行)
オプション1: i := 1; i <= 10
i=1: 1<=10 → true
i=2: 2<=10 → true
...
i=10: 10<=10 → true
i=11: 11<=10 → false(終了)
→ 正解
オプション2: i := 0; i < 10
i=0: 0<10 → true → 出力1
i=1: 1<10 → true → 出力2
...
i=9: 9<10 → true → 出力10
i=10: 10<10 → false(終了)
→ 正解(出力を i+1 にする必要あり)
breakとcontinueの詳細
break: ループからの脱出
// 例1: 特定条件で終了
for i := 0; i < 100; i++ {
if i == 5 {
break // i=5でループを抜ける
}
fmt.Println(i)
}
// 出力: 0, 1, 2, 3, 4
メモリイメージ:
i=0 → 条件false → 出力
i=1 → 条件false → 出力
i=2 → 条件false → 出力
i=3 → 条件false → 出力
i=4 → 条件false → 出力
i=5 → 条件true → break(ループ終了)
continue: 次の繰り返しへスキップ
// 例1: 偶数のみ処理
for i := 0; i < 10; i++ {
if i%2 == 1 {
continue // 奇数はスキップ
}
fmt.Println(i)
}
// 出力: 0, 2, 4, 6, 8
実行フロー:
i=0: 0%2=0 → continueしない → 出力
i=1: 1%2=1 → continue(fmt.Printlnをスキップ)
i=2: 2%2=0 → continueしない → 出力
i=3: 3%2=1 → continue
i=4: 4%2=0 → continueしない → 出力
...
breakとcontinueの比較
// break の例
for i := 0; i < 10; i++ {
if i == 5 {
break // ループを完全に終了
}
fmt.Println(i)
}
// 出力: 0, 1, 2, 3, 4
// continue の例
for i := 0; i < 10; i++ {
if i == 5 {
continue // i=5 だけスキップ
}
fmt.Println(i)
}
// 出力: 0, 1, 2, 3, 4, 6, 7, 8, 9(5が抜けている)
ネストループの理解
二重ループの実行回数
for i := 0; i < 3; i++ {
for j := 0; j < 4; j++ {
fmt.Printf("(%d, %d) ", i, j)
}
fmt.Println()
}
実行順序の詳細:
i=0:
j=0 → (0, 0)
j=1 → (0, 1)
j=2 → (0, 2)
j=3 → (0, 3)
改行
i=1:
j=0 → (1, 0)
j=1 → (1, 1)
j=2 → (1, 2)
j=3 → (1, 3)
改行
i=2:
j=0 → (2, 0)
j=1 → (2, 1)
j=2 → (2, 2)
j=3 → (2, 3)
改行
合計: 3 × 4 = 12回実行
ネストループのパフォーマンス
// O(n²) の例
n := 100
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
// 処理
}
}
// 実行回数: 100 × 100 = 10,000回
時間計算量:
1重ループ: O(n) → n=100なら100回
2重ループ: O(n²) → n=100なら10,000回
3重ループ: O(n³) → n=100なら1,000,000回
結論: ネストが増えると指数関数的に遅くなる
---
アルゴリズム分析
時間計算量の実例
例1: 単純なカウント
for i := 0; i < n; i++ {
fmt.Println(i)
}
時間計算量: O(n)
- ループ本体: O(1)(定数時間)
- ループ回数: n回
- 合計: O(1) × n = O(n)
例2: 線形探索
func find(arr []int, target int) int {
for i := 0; i < len(arr); i++ {
if arr[i] == target {
return i
}
}
return -1
}
時間計算量: O(n)
- 最良ケース: O(1)(最初の要素)
- 最悪ケース: O(n)(最後の要素または見つからない)
- 平均ケース: O(n/2) = O(n)
例3: バブルソート
func bubbleSort(arr []int) {
n := len(arr)
for i := 0; i < n; i++ {
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}
時間計算量: O(n²)
- 外側ループ: n回
- 内側ループ: 平均(n/2)回
- 合計: n × (n/2) ≈ n²/2 = O(n²)
空間計算量の分析
例1: 基本ループ
sum := 0
for i := 0; i < n; i++ {
sum += i
}
空間計算量: O(1)
- 変数
sum: O(1) - 変数
i: O(1) - 追加メモリなし
例2: 結果を保存
results := make([]int, 0)
for i := 0; i < n; i++ {
results = append(results, i*2)
}
空間計算量: O(n)
- スライス
results: n個の要素 = O(n)
---
設計原則
DRY原則の適用
// ❌ 悪い例: 重複コード
for i := 1; i <= 9; i++ {
fmt.Printf("2 x %d = %d\n", i, 2*i)
}
fmt.Println()
for i := 1; i <= 9; i++ {
fmt.Printf("3 x %d = %d\n", i, 3*i)
}
fmt.Println()
for i := 1; i <= 9; i++ {
fmt.Printf("4 x %d = %d\n", i, 4*i)
}
// ✅ 良い例: ネストループで汎用化
for n := 2; n <= 4; n++ {
for i := 1; i <= 9; i++ {
fmt.Printf("%d x %d = %d\n", n, i, n*i)
}
fmt.Println()
}
// ✅ さらに良い例: 関数化
func printTable(n int) {
for i := 1; i <= 9; i++ {
fmt.Printf("%d x %d = %d\n", n, i, n*i)
}
}
for n := 2; n <= 4; n++ {
printTable(n)
fmt.Println()
}
KISS原則
// ❌ 複雑: 不要な変数
found := false
index := -1
for i := 0; i < len(arr); i++ {
if arr[i] == target {
found = true
index = i
break
}
}
if found {
return index
} else {
return -1
}
// ✅ シンプル: 直接返す
for i := 0; i < len(arr); i++ {
if arr[i] == target {
return i
}
}
return -1
---
メンタルモデル
ループをテープレコーダーで考える
テープ: [1] [2] [3] [4] [5]
ヘッド: ^(現在位置)
for i := 0; i < 5; i++ {
process(tape[i])
}
動作:
1. ヘッドを[1]に移動 → 処理
2. ヘッドを[2]に移動 → 処理
3. ヘッドを[3]に移動 → 処理
4. ヘッドを[4]に移動 → 処理
5. ヘッドを[5]に移動 → 処理
6. テープ終了 → 停止
ビジュアル図解(ASCII Art)
forループの構造
┌──────────────┐
│ 初期化 │
│ i := 0 │
└──────┬───────┘
│
v
┌──────────────┐
│ 条件チェック │
┌───│ i < 10? │
│ └──────┬───────┘
│ true│ false
│ │ └────────→ [ループ終了]
│ v
│ ┌──────────────┐
│ │ ループ本体 │
│ │ fmt.Println │
│ └──────┬───────┘
│ │
│ v
│ ┌──────────────┐
│ │ 更新 │
│ │ i++ │
│ └──────┬───────┘
│ │
└───────────┘
break の動作
i=0 → 処理 → i++
i=1 → 処理 → i++
i=2 → 処理 → i++
i=3 → 処理 → i++
i=4 → 処理 → i++
i=5 → break! ────┐
i=6 (実行されない)│
i=7 (実行されない)│
... │
v
[ループ終了]
continue の動作
i=0 → 処理 → i++
i=1 → continue ──┐
│(処理スキップ)
v
i++
i=2 → 処理 → i++
i=3 → continue ──┐
│(処理スキップ)
v
i++
i=4 → 処理 → i++
...
---
よくある質問(FAQ)
Q1: for文とrange、どちらを使うべき?
A: 状況に応じて使い分けます。
// インデックスが不要な場合: range
arr := []int{10, 20, 30}
for _, value := range arr {
fmt.Println(value)
}
// インデックスが必要な場合: 通常のfor
for i := 0; i < len(arr); i++ {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
// ステップが1でない場合: 通常のfor
for i := 0; i < 10; i += 2 { // 2ずつ増加
fmt.Println(i)
}
Q2: 無限ループの使い道は?
A: サーバーやイベントループで使用します。
// Webサーバーの例
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConnection(conn)
}
// タイマーの例
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
fmt.Println("1秒経過")
case <-done:
return // ループ終了
}
}
Q3: ループ内で変数を宣言すべき?
A: パフォーマンスへの影響は微小です。可読性を優先しましょう。
// パターン1: ループ外で宣言
var result int
for i := 0; i < 10; i++ {
result = process(i)
fmt.Println(result)
}
// パターン2: ループ内で宣言
for i := 0; i < 10; i++ {
result := process(i)
fmt.Println(result)
}
// パフォーマンス差: ほぼなし
// 推奨: パターン2(スコープが限定される)
Q4: ++i と i++ の違いは?
A: Goでは i++ のみサポートされています。
// ✅ OK
i++
// ❌ エラー
++i // syntax error
// 理由: Goの設計思想「シンプルさ」
// 前置と後置の違いによる混乱を避ける
Q5: ループの最大回数は?
A: 理論上は整数の最大値(int64で2^63-1)ですが、実用上は時間制約があります。
// 10億回ループ
for i := 0; i < 1000000000; i++ {
// 簡単な処理でも数秒かかる
}
// 無限ループ
for {
// 永遠に続く(Ctrl+Cで停止)
}
---
セルフチェック質問
基礎理解度チェック
for i := 0; i < 5; i++ {
fmt.Println(i)
}
A: 5回(i=0,1,2,3,4)- Q: 以下のコードの出力は?
for i := 0; i < 5; i++ {
if i == 3 {
break
}
fmt.Println(i)
}
A: 0, 1, 2- Q: 以下のコードの出力は?
for i := 0; i < 5; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i)
}
A: 1, 3(偶数がスキップされる)応用力チェック
- Q: 1から100までの偶数の合計を求めるコードを書けますか?
sum := 0
for i := 2; i <= 100; i += 2 {
sum += i
}
fmt.Println(sum) // 2550
- Q: 二重ループの時間計算量は?
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
// ...
}
}
A: O(n²)実務応用チェック
- Q: 配列から特定の値を探す最も効率的な方法は?
- Q: 無限ループから安全に抜ける方法は?
---
まとめ
今日学んだこと
1. 基本構文
- forループ:
for 初期化; 条件; 更新 { } - while風:
for 条件 { } - 無限ループ:
for { }
2. 制御構文
- break: ループを終了
- continue: 次の繰り返しへ
- ネストループ: ループの中にループ
3. 重要概念
- Off-by-One エラー: 境界値の扱い
- 時間計算量: ループのパフォーマンス
- 早期終了: break による最適化
4. 実務スキル
- アルゴリズム設計: 効率的なループ
- デバッグ: ループの動作確認
- 最適化: パフォーマンスチューニング
- [ ] 基本forループが書ける
- [ ] カウントアップ/ダウンができる
- [ ] break/continueが使える
- [ ] 二重ループが理解できる
- [ ] 素数判定ができる
- [ ] 時間計算量を意識できる
- [ ] Off-by-Oneエラーを避けられる
- [ ] ループ最適化ができる
- 配列とスライス: データ構造との組み合わせ
- 関数: ループ処理の再利用化
- 並列処理: goroutineによる高速化
- アルゴリズム: ソート・探索の実装
今日のキーワード
| 用語 | 意味 | 重要度 |
|---|---|---|
| forループ | 繰り返し処理の基本構文 | ★★★ |
| break | ループの終了 | ★★★ |
| continue | 次の繰り返しへスキップ | ★★★ |
| ネストループ | 多次元の繰り返し | ★★☆ |
| Off-by-One | 境界値エラー | ★★☆ |
| O(n) | 線形時間計算量 | ★★☆ |
| O(n²) | 二次時間計算量 | ★★☆ |