Keito

© 2025 Keito

技術ブログとポートフォリオ

【Go】フィボナッチ数列を再帰的に求めるサンプルコードを書いてみた

更新日2025/8/23/(土) 03:38
タグ
Goアルゴリズム

概要

  • Goでフィボナッチ数列を再帰実装
  • フィボナッチ数列は前の2つの数を足した値が次の数になる数列。再帰は数学的定義をそのままコードに表現できるが、計算量の問題がある典型例。

特徴

  • 直感的実装: 数学的定義をそのままコード化
  • 関数型プログラミング: 副作用のない純粋関数
  • 指数的計算量: 同じ計算を何度も繰り返すため非効率
  • メモ化で改善可能: 計算結果を保存することで高速化
  • 再帰の典型例: 再帰アルゴリズムの学習に最適

動作原理

基本的な流れ:

  1. ベースケース:Fib(0) = 0, Fib(1) = 1
  2. 再帰ケース:Fib(n) = Fib(n-1) + Fib(n-2)
  3. 各関数呼び出しが2つの子問題に分割
  4. 結果を合算して返す

具体例

Fib(5) の計算過程:

Fib(5) ├── Fib(4) │ ├── Fib(3) │ │ ├── Fib(2) │ │ │ ├── Fib(1) = 1 │ │ │ └── Fib(0) = 0 │ │ │ = 1 + 0 = 1 │ │ └── Fib(1) = 1 │ │ = 1 + 1 = 2 │ └── Fib(2) │ ├── Fib(1) = 1 │ └── Fib(0) = 0 │ = 1 + 0 = 1 │ = 2 + 1 = 3 └── Fib(3) ├── Fib(2) │ ├── Fib(1) = 1 │ └── Fib(0) = 0 │ = 1 + 0 = 1 └── Fib(1) = 1 = 1 + 1 = 2 = 3 + 2 = 5 結果: Fib(5) = 5

サンプルコード

基本実装

package main import "fmt" // FibonacciRecursive は再帰でフィボナッチ数列のn番目を計算 func FibonacciRecursive(n int) int { if n <= 1 { return n } return FibonacciRecursive(n-1) + FibonacciRecursive(n-2) }

ステップ表示版

// FibonacciRecursiveWithSteps は再帰の呼び出し過程を表示 func FibonacciRecursiveWithSteps(n int, depth int) int { indent := "" for i := 0; i < depth; i++ { indent += " " } fmt.Printf("%sFib(%d) を計算開始\\\\n", indent, n) if n <= 1 { fmt.Printf("%sFib(%d) = %d (ベースケース)\\\\n", indent, n, n) return n } fmt.Printf("%sFib(%d) = Fib(%d) + Fib(%d)\\\\n", indent, n, n-1, n-2) left := FibonacciRecursiveWithSteps(n-1, depth+1) right := FibonacciRecursiveWithSteps(n-2, depth+1) result := left + right fmt.Printf("%sFib(%d) = %d + %d = %d\\\\n", indent, n, left, right, result) return result }

メモ化版(改善)

// FibonacciMemoization はメモ化を使った再帰実装 func FibonacciMemoization(n int) int { memo := make(map[int]int) return fibMemo(n, memo) } func fibMemo(n int, memo map[int]int) int { if n <= 1 { return n } // メモに結果があれば使用 if val, exists := memo[n]; exists { return val } // 計算してメモに保存 result := fibMemo(n-1, memo) + fibMemo(n-2, memo) memo[n] = result return result }

反復版(比較用)

// FibonacciIterative は反復による実装 func FibonacciIterative(n int) int { if n <= 1 { return n } a, b := 0, 1 for i := 2; i <= n; i++ { a, b = b, a+b } return b }

計算量

時間計算量

  • 純粋再帰: O(2^n) - 指数的増加
  • メモ化版: O(n) - 各値を1回だけ計算
  • 反復版: O(n) - 線形時間

空間計算量

  • 純粋再帰: O(n) - 再帰スタックの深さ
  • メモ化版: O(n) - メモテーブル + スタック
  • 反復版: O(1) - 定数空間

関数呼び出し回数

Fib(n)の計算における関数呼び出し回数:

  • Fib(5): 15回
  • Fib(10): 177回
  • Fib(20): 21,891回
  • Fib(30): 2,692,537回

使いどころ

向いてる場面

  • 小さなn(n < 30程度)
  • 数学的概念の理解・教育
  • 再帰アルゴリズムの学習
  • プロトタイピング

実例:数列生成

// FibonacciSequence はn番目までのフィボナッチ数列を生成 func FibonacciSequence(n int) []int { if n < 0 { return []int{} } sequence := make([]int, n+1) for i := 0; i <= n; i++ { sequence[i] = FibonacciRecursive(i) } return sequence } // 使用例 func main() { sequence := FibonacciSequence(10) fmt.Printf("フィボナッチ数列: %v\\\\n", sequence) // 出力: [0 1 1 2 3 5 8 13 21 34 55] }

他の実装との比較

実装方法時間計算量空間計算量可読性実用性
純粋再帰O(2^n)O(n)×
メモ化再帰O(n)O(n)
反復O(n)O(1)
行列べき乗O(log n)O(1)×

最適化アイデア

パフォーマンス測定

import "time" func measureTime(name string, fn func()) { start := time.Now() fn() duration := time.Since(start) fmt.Printf("%s の実行時間: %v\\\\n", name, duration) } func compareImplementations(n int) { fmt.Printf("n = %d での比較:\\\\n", n) measureTime("純粋再帰", func() { FibonacciRecursive(n) }) measureTime("メモ化", func() { FibonacciMemoization(n) }) measureTime("反復", func() { FibonacciIterative(n) }) }

末尾再帰版

// FibonacciTailRecursive は末尾再帰による実装 func FibonacciTailRecursive(n int) int { return fibTail(n, 0, 1) } func fibTail(n, a, b int) int { if n == 0 { return a } return fibTail(n-1, b, a+b) }

チャネルを使った並行実装

func FibonacciChannel(n int) <-chan int { ch := make(chan int) go func() { defer close(ch) a, b := 0, 1 for i := 0; i < n; i++ { ch <- a a, b = b, a+b } }() return ch } // 使用例 func main() { for fib := range FibonacciChannel(10) { fmt.Printf("%d ", fib) } // 出力: 0 1 1 2 3 5 8 13 21 34 }

まとめ

メリット

  • 直感的で理解しやすい
  • 数学的定義に忠実
  • 再帰の良い学習例
  • コードが簡潔

デメリット

  • 指数的計算量で非効率
  • 大きなnで実用不可
  • スタックオーバーフローの危険
  • 同じ計算の重複実行

使うべき時

  • 小さなn(< 30)
  • 教育・学習目的
  • プロトタイピング
  • 再帰の理解

改善案

  • メモ化: 計算結果をキャッシュ
  • 反復版: ボトムアップで計算
  • 行列べき乗: O(log n)で計算

実用的にはメモ化や反復版を使うべきだが、純粋再帰版は再帰アルゴリズムの典型例として重要。計算量の違いを実感できる良い教材。