第 5 章 再帰アルゴリズム¶
はじめに¶
再帰(Recursion)とは、自分自身を呼び出す関数・メソッドを使ってアルゴリズムを表現する手法です。この章では、階乗・GCD・ハノイの塔・8 王妃問題を TDD で実装します。
1. 階乗(Factorial)¶
n! = n × (n-1) × ... × 2 × 1 を再帰的に計算します。
Red — テストを書く¶
describe "Algorithm.factorial" do
it "5の階乗" do
expect(Algorithm.factorial(5)).to eq(120)
end
it "0の階乗" do
expect(Algorithm.factorial(0)).to eq(1)
end
end
Green — 実装¶
def self.factorial(n)
return 1 if n <= 0
n * factorial(n - 1)
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| ガード節 | if n <= 0: return 1 |
return 1 if n <= 0 |
Ruby の後置 if でガード節を簡潔に書けます。
2. 最大公約数(GCD)¶
ユークリッドの互除法で最大公約数を求めます。
Green — 実装¶
def self.gcd(x, y)
return x if y == 0
gcd(y, x % y)
end
3. 再帰的な総和¶
1 から n までの和を再帰的に計算します。
def self.recursive_sum(n)
return 0 if n <= 0
n + recursive_sum(n - 1)
end
4. ハノイの塔¶
n 枚の円盤を src から dst へ via を経由して移動する手順を返します。
Red — テストを書く¶
describe "Algorithm.hanoi" do
it "円盤1枚のハノイ" do
expect(Algorithm.hanoi(1, "A", "C", "B")).to eq([["A", "C"]])
end
it "円盤3枚のハノイの手順数" do
expect(Algorithm.hanoi(3, "A", "C", "B").length).to eq(7)
end
end
Green — 実装¶
def self.hanoi(n, src, dst, via)
return [[src, dst]] if n == 1
moves = []
moves.concat(hanoi(n - 1, src, via, dst))
moves << [src, dst]
moves.concat(hanoi(n - 1, via, dst, src))
moves
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| リストの連結 | moves.extend(hanoi(...)) |
moves.concat(hanoi(...)) |
| 要素追加 | moves.append((src, dst)) |
moves << [src, dst] |
| タプル | (src, dst) |
[src, dst](配列で代替) |
Ruby にはタプルがないため、配列 [src, dst] で代替します。<< は配列への末尾追加演算子です。
4. 再帰アルゴリズムの解析¶
真に再帰的な関数¶
「真に再帰的な関数」とは、複数の再帰呼び出しを含む関数のことです。
Red — テストを書く¶
describe "Algorithm.recure" do
it "真に再帰的な関数: recure(4, []) => [1, 2, 3, 1, 4, 1, 2]" do
expect(Algorithm.recure(4, [])).to eq([1, 2, 3, 1, 4, 1, 2])
end
end
Green — 実装¶
def self.recure(n, list)
if n > 0
recure(n - 1, list) # 最初の再帰呼び出し
list << n
recure(n - 2, list) # 2番目の再帰呼び出し
end
list
end
実行の流れ¶
recure(4, []) を実行すると、以下の呼び出しの連鎖が発生します:
recure(4, [])
→ recure(3, [])
→ recure(2, [])
→ recure(1, [])
→ recure(0, []) → 何もしない
list << 1 → [1]
→ recure(-1, [1]) → 何もしない
list << 2 → [1, 2]
→ recure(0, [1, 2]) → 何もしない
list << 3 → [1, 2, 3]
→ recure(1, [1, 2, 3])
list << 1 → [1, 2, 3, 1]
list << 4 → [1, 2, 3, 1, 4]
→ recure(2, [1, 2, 3, 1, 4])
list << 1 → [1, 2, 3, 1, 4, 1]
list << 2 → [1, 2, 3, 1, 4, 1, 2]
最終結果: [1, 2, 3, 1, 4, 1, 2]
再帰アルゴリズムの非再帰表現¶
再帰は概念的に理解しやすいですが、深い再帰呼び出しはスタックオーバーフローを引き起こす可能性があります。そのため、再帰アルゴリズムを非再帰(反復的)な形に変換することが重要です。
末尾再帰の除去¶
末尾再帰(tail recursion)とは、関数の最後の操作が再帰呼び出しである場合の再帰のことです。以下は recure 関数の末尾にある再帰呼び出しをループに変換した例です:
def self.recure2(n, list)
while n > 0
recure2(n - 1, list) # 最初の再帰呼び出しはそのまま
list << n
n -= 2 # 2番目の再帰呼び出しをループに変換
end
list
end
テスト¶
describe "Algorithm.recure2" do
it "末尾再帰除去版: recure2(4, []) => [1, 2, 3, 1, 4, 1, 2]" do
expect(Algorithm.recure2(4, [])).to eq([1, 2, 3, 1, 4, 1, 2])
end
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| リストへの追加 | list.append(n) |
list << n |
| ガード節 | if n > 0: 内に処理 |
if n > 0 内に処理(同様) |
| 末尾再帰の除去 | while True: + continue |
while n > 0 で簡潔に表現 |
5. 8 王妃問題¶
8×8 のチェス盤に 8 個の王妃を互いに攻撃できない位置に配置する問題です。
第 1 版(全組み合わせ)¶
class EightQueen
attr_reader :result
def initialize
@result = []
@pos = Array.new(8, 0)
end
def put
@result << @pos.dup
end
def set(i)
8.times do |j|
@pos[i] = j
if i == 7
put
else
set(i + 1)
end
end
end
end
it "全組み合わせは 8^8 = 16777216 通り" do
q = Algorithm::EightQueen.new
q.set(0)
expect(q.result.length).to eq(8**8)
end
第 3 版(行・対角線制約あり — 92 通り)¶
class EightQueen3
def initialize
@result = []
@pos = Array.new(8, 0)
@flag_a = Array.new(8, false) # 各行のフラグ
@flag_b = Array.new(15, false) # 右上がり対角線
@flag_c = Array.new(15, false) # 右下がり対角線
end
def set(i)
8.times do |j|
next if @flag_a[j] || @flag_b[i + j] || @flag_c[i - j + 7]
@pos[i] = j
if i == 7
@result << @pos.dup
else
@flag_a[j] = @flag_b[i + j] = @flag_c[i - j + 7] = true
set(i + 1)
@flag_a[j] = @flag_b[i + j] = @flag_c[i - j + 7] = false
end
end
end
end
it "完全な8王妃問題: 92 通り" do
q = Algorithm::EightQueen3.new
q.set(0)
expect(q.result.length).to eq(92)
end
Python 版との違い¶
| 概念 | Python | Ruby |
|---|---|---|
| インスタンス変数 | self.__pos |
@pos |
| プライベート変数 | __ プレフィックス |
private でメソッド単位 |
| クラス定義 | class EightQueen: |
class EightQueen |
| 配列の深いコピー | self.__pos[:] |
@pos.dup |
| 多重代入 | 個別に代入 | @flag_a[j] = @flag_b[i+j] = @flag_c[i-j+7] = true |
まとめ¶
| アルゴリズム | メソッド | 計算量 |
|---|---|---|
| 階乗 | factorial |
O(n) |
| GCD | gcd |
O(log min(x,y)) |
| 再帰的総和 | recursive_sum |
O(n) |
| ハノイの塔 | hanoi |
O(2^n) |
| 8 王妃問題 | EightQueen3#set |
O(n!) |
Ruby の特徴¶
@変数でインスタンス変数(クラス全体で共有)attr_reader :resultで読み取り専用のアクセサを定義next if 条件でループのスキップArray.new(n, 初期値)で初期値付き配列を作成arr.dupで配列の浅いコピー