0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】階乗計算を再帰的に行うサンプルコードを書いてみた

Posted at

概要

  • Goで階乗計算を再帰的に実装
  • 階乗(n!)は1からnまでの自然数の積。再帰的定義がそのままコードに落とし込みやすく、再帰アルゴリズムの基本例として最適。

特徴

  • シンプルな実装: 数学的定義をそのままコード化
  • 再帰の基本パターン: ベースケースと再帰ケースの明確な分離
  • メモ化による最適化: 計算結果のキャッシュで高速化可能
  • 大きな数への対応: big.Intパッケージで巨大な階乗値も計算可能
  • 末尾再帰の非対応: Goは末尾再帰最適化をサポートしないため、スタックオーバーフローに注意

動作原理

基本的な流れ:

  1. ベースケース:0! = 1, 1! = 1
  2. 再帰ケース:n! = n × (n-1)!
  3. 各関数呼び出しが1つ小さい問題に分割
  4. ベースケースに到達後、結果を順次掛け合わせて返す

具体例

5! の計算過程:

factorial(5)
├── 5 × factorial(4)
    ├── 4 × factorial(3)
        ├── 3 × factorial(2)
            ├── 2 × factorial(1)
                └── 1 (ベースケース)
            └── 2 × 1 = 2
        └── 3 × 2 = 6
    └── 4 × 6 = 24
└── 5 × 24 = 120

サンプルコード

package main

import (
	"fmt"
	"math/big"
)

// 基本的な再帰による階乗計算
func factorial(n int) uint64 {
	// エラーチェック
	if n < 0 {
		panic("factorial: negative number not allowed")
	}
	// ベースケース
	if n == 0 || n == 1 {
		return 1
	}
	// 再帰ケース
	return uint64(n) * factorial(n-1)
}

// メモ化を使った階乗計算
func factorialWithMemo(n int, memo map[int]uint64) uint64 {
	if n < 0 {
		panic("factorial: negative number not allowed")
	}
	// ベースケース
	if n == 0 || n == 1 {
		return 1
	}
	// メモにあればそれを返す
	if val, exists := memo[n]; exists {
		return val
	}
	// 再帰計算してメモに保存
	result := uint64(n) * factorialWithMemo(n-1, memo)
	memo[n] = result
	return result
}

// 大きな数に対応した階乗計算(big.Int使用)
func factorialBigInt(n int) *big.Int {
	if n < 0 {
		panic("factorial: negative number not allowed")
	}
	// ベースケース
	if n == 0 || n == 1 {
		return big.NewInt(1)
	}
	// 再帰計算
	result := big.NewInt(int64(n))
	return result.Mul(result, factorialBigInt(n-1))
}

// 計算過程を可視化する階乗計算
func visualizeFactorial(n int, depth int) uint64 {
	indent := ""
	for i := 0; i < depth; i++ {
		indent += "  "
	}

	// ベースケース
	if n == 0 || n == 1 {
		fmt.Printf("%sfactorial(%d) = 1\\n", indent, n)
		return 1
	}

	// 再帰呼び出しを表示
	fmt.Printf("%sfactorial(%d) = %d * factorial(%d)\\n", indent, n, n, n-1)
	result := uint64(n) * visualizeFactorial(n-1, depth+1)
	fmt.Printf("%sfactorial(%d) = %d\\n", indent, n, result)
	return result
}

func main() {
	// 基本的な階乗計算
	fmt.Println("基本的な再帰による階乗計算:")
	for i := 0; i <= 10; i++ {
		result := factorial(i)
		fmt.Printf("%d! = %d\\n", i, result)
	}

	// メモ化を使った階乗計算
	fmt.Println("\\nメモ化を使った階乗計算:")
	memo := make(map[int]uint64)
	for i := 15; i <= 20; i++ {
		result := factorialWithMemo(i, memo)
		fmt.Printf("%d! = %d\\n", i, result)
	}

	// 大きな数の階乗計算
	fmt.Println("\\n大きな数の階乗計算 (big.Int使用):")
	for _, n := range []int{25, 50, 100} {
		result := factorialBigInt(n)
		fmt.Printf("%d! = %s\\n", n, result.String())
	}

	// 計算過程の可視化
	fmt.Println("\\n計算過程の可視化 (5!):")
	visualizeFactorial(5, 0)
}

時間計算量と空間計算量

時間計算量

  • 基本実装: O(n) - n回の再帰呼び出し
  • メモ化版: O(n) - 各値を1回だけ計算

空間計算量

  • 基本実装: O(n) - 再帰スタックの深さ
  • メモ化版: O(n) - スタック深さ + メモのストレージ

実装上の注意点

1. オーバーフロー対策

階乗は急速に大きくなるため、uint64でも20!までしか扱えない:

  • 20! = 2,432,902,008,176,640,000 (uint64の範囲内)
  • 21! = 51,090,942,171,709,440,000 (uint64を超える)

大きな値を扱う場合はmath/bigパッケージを使用:

func factorialBigInt(n int) *big.Int {
    if n == 0 || n == 1 {
        return big.NewInt(1)
    }
    result := big.NewInt(int64(n))
    return result.Mul(result, factorialBigInt(n-1))
}

2. スタックオーバーフロー対策

Goは末尾再帰最適化をサポートしないため、大きなnでスタックオーバーフローの可能性がある。
必要に応じて反復実装を検討:

func factorialIterative(n int) uint64 {
    result := uint64(1)
    for i := 2; i <= n; i++ {
        result *= uint64(i)
    }
    return result
}

3. エラーハンドリング

負の数に対する適切なエラー処理:

func factorial(n int) (uint64, error) {
    if n < 0 {
        return 0, fmt.Errorf("factorial of negative number is undefined")
    }
    // ...
}

応用例

組み合わせ計算(nCr)

階乗を使った組み合わせの計算:

func combination(n, r int) *big.Int {
    if r > n || r < 0 {
        return big.NewInt(0)
    }
    // nCr = n! / (r! * (n-r)!)
    nFact := factorialBigInt(n)
    rFact := factorialBigInt(r)
    nrFact := factorialBigInt(n - r)

    denominator := new(big.Int).Mul(rFact, nrFact)
    return new(big.Int).Div(nFact, denominator)
}

順列計算(nPr)

階乗を使った順列の計算:

func permutation(n, r int) *big.Int {
    if r > n || r < 0 {
        return big.NewInt(0)
    }
    // nPr = n! / (n-r)!
    nFact := factorialBigInt(n)
    nrFact := factorialBigInt(n - r)
    return new(big.Int).Div(nFact, nrFact)
}

まとめ

再帰的な階乗計算は:

  • 再帰の基本を学ぶ最適な例題
  • 数学的定義との対応が明確
  • メモ化による最適化の効果を実感しやすい
  • 大きな数の扱いを学ぶ良い機会

実用的には反復実装の方が効率的だが、再帰の概念理解と応用(組み合わせ・順列計算など)において重要な基礎となる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?