Day 4: ループ - 解説

学習の目的

このDayで達成すること

  • 繰り返し処理ができる
- 同じ処理を効率的に実行 - ループの基本構文をマスター - プログラムに「反復」の概念を実装

  • ループ制御ができる
- break, continueの使い方 - 適切なタイミングでのループ終了 - 効率的なループ設計

  • ネストしたループが書ける
- 二次元の処理 - 複雑なデータ構造の走査 - パフォーマンスを意識した実装

---

学習の手引き

推奨学習順序

  • 背景知識を読む(20分)
- ループの歴史と進化 - 実世界での活用事例 - プロダクション環境での考慮事項

  • 課題1に取り組む(30分)
- 基本的なforループ - カウントアップ/ダウン - 累積計算

  • 課題2に取り組む(40分)
- ループと条件分岐の組み合わせ - 二重ループ - continue/break の活用

  • 課題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で停止)
    }
    

    ---

    セルフチェック質問

    基礎理解度チェック

  • Q: 以下のコードは何回ループする?
   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までの偶数の合計を求めるコードを書けますか?
A:
   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: 配列から特定の値を探す最も効率的な方法は?
A: 線形探索(ソート済みなら二分探索)

  • Q: 無限ループから安全に抜ける方法は?
A: break文、return文、またはコンテキストによるキャンセル

---

まとめ

今日学んだこと

1. 基本構文

  • forループ: for 初期化; 条件; 更新 { }
  • while風: for 条件 { }
  • 無限ループ: for { }

2. 制御構文

  • break: ループを終了
  • continue: 次の繰り返しへ
  • ネストループ: ループの中にループ

3. 重要概念

  • Off-by-One エラー: 境界値の扱い
  • 時間計算量: ループのパフォーマンス
  • 早期終了: break による最適化

4. 実務スキル

  • アルゴリズム設計: 効率的なループ
  • デバッグ: ループの動作確認
  • 最適化: パフォーマンスチューニング
  • 今日のキーワード

    用語 意味 重要度
    forループ 繰り返し処理の基本構文 ★★★
    break ループの終了 ★★★
    continue 次の繰り返しへスキップ ★★★
    ネストループ 多次元の繰り返し ★★☆
    Off-by-One 境界値エラー ★★☆
    O(n) 線形時間計算量 ★★☆
    O(n²) 二次時間計算量 ★★☆

    達成度チェックリスト

  • [ ] 基本forループが書ける
  • [ ] カウントアップ/ダウンができる
  • [ ] break/continueが使える
  • [ ] 二重ループが理解できる
  • [ ] 素数判定ができる
  • [ ] 時間計算量を意識できる
  • [ ] Off-by-Oneエラーを避けられる
  • [ ] ループ最適化ができる
  • 次のステップ

  • 配列とスライス: データ構造との組み合わせ
  • 関数: ループ処理の再利用化
  • 並列処理: goroutineによる高速化
  • アルゴリズム: ソート・探索の実装