FreeBASIC マニュアルのトップに戻る

FreeBASIC ProPgRecursion

目次→教本→プログラマーのための案内Recursion←オリジナル・サイト

再帰 左にメニュー・フレームが表示されていない場合は、ここをクリックして下さい

←リンク元に戻る プログラム開発関連に戻る

再帰的手続き(サブルーチンと関数)

序文:
「繰り返し」と「再帰」の二つは、プログラミングで、特定の定型手続きを特定の回数実行するときに、コードを最適化できる、非常に便利な方法です。
「繰り返し」は比較的理解しやすいですが、「再帰」は最初は明白には納得できない概念です。

「再帰」手続き(サブルーチンと関数)とは、構文の特性を意味します:「再帰」の定義は、手続きが、その手続き自体を参照します(自分自身を呼び出します)。
ただ、線形またはツリーの再帰処理過程を言うときは、手続きの記述の構文ではなく、処理過程の流れに着目します。
したがって、手続きは、再帰的な定義を持つことができますが、繰り返し処理過程に対応しています。

一部の処理は、再帰解法として自然に実装されます(ただし、再帰が常に最適な解法であるとは限りません)。
再帰を使う処理の主な問題は、実行スタックで潜在的に多くのスペースを消費することです: 再帰の特定のレベルの「深さ」から、スレッドの実行スタックに割り当てられたスペースが使い果たされ、"stack overflow" のエラーが発生します。
同じ手順を繰り返し呼び出すと、実行が遅くなる可能性があります。けれども、再帰を使うことでコードは簡単になり得るのです。

単純な再帰解法を、実行速度を改善するために、はるかに高速に実行されるループを使って、少し複雑な繰り返し解法で作り直すことができます。

繰り返し解法と比較して、実行時間とメモリ空間が増加しても、再帰を使う理由は何ですか?
再帰以外の方法では不可能な場合、繰り返しへの変更ができないない場合、または変更できても実装がはるかに難しい場合があります(たとえば、実行スタックの代わりに動的ストレージ容量が必要になるなど)。

「繰り返し」と「再帰」
「繰り返し」と「再帰」は、両方とも、命令セットを繰り返し実行します:
「繰り返し」と「再帰」の主な違いは、「繰り返し」は、反復実行する「一連の命令」に適用される処理過程であるのに対して、「再帰」は常に「手続き」に適用される処理過程だということです。

「繰り返し」の定義
「繰り返し」は、反復条件が偽になるまで、一連の命令を繰り返し実行する処理過程です。
「繰り返し」ブロックは、初期化、比較、反復される命令の実行、最後に制御変数の更新、で構成されます。
制御変数が更新されると、再度比較され、反復の条件が偽になるまで、処理過程が繰り返されます。
「繰り返し」ブロックは、For ループ、While ループ、...です。

「繰り返し」ブロックは、各サイクルで「変数」を保存するための、実行スタックを使いません。
したがって、繰り返しブロックの実行は、再帰ブロックよりも高速です。
さらに、繰り返しには、繰り返される手続きを呼ぶためのオーバーヘッドがありません。これは実行を再帰よりも高速にします。
制御条件が偽になると、繰り返しが完了します。

整数の階乗を返す繰り返し関数の簡単な例:

'' この反復関数は、階乗関数を繰り返す定義を使って、定義されます:
''    Case (n = 0) : factorial(0) = 1
''    Case (n > 0) : factorial(n) = (1) * ..... * (n - 2) * (n - 1) * (n)
''    最初の行は、累積変数の初期値を決定します : 'result = 1'
''    2 行目は、蓄積する命令文の構文を決定します: 'result = result * I'

Function iterativeFactorial (ByVal n As Integer) As Integer
    Dim As Integer result = 1  '' 変数の初期化
    For I As Integer = 1 To n  '' 繰り返しループ
        result = result * I    '' 反復累積
    Next I
    Return result
End Function

Dim N As Integer

N = 6

Print Str(N) & "! =" ; iterativeFactorial(6)

Sleep


「再帰」の定義
FreeBASIC では、手続きが、コード内で自分自身を呼び出すことができます。
これは、手続き定義に、それ自体への手続き呼び出しがあることを意味します。
手続きで使われるローカル変数とパラメータのセットは、手続きが呼び出されるたびに新しく作成され、実行スタックの最上部に保存されます。
ただし、手続きがそれ自体を呼び出すときに、その手続きの新しいコピーは作成されません。
再帰的手順は、コードのサイズを大幅に削減したり、メモリ使用量を改善したりするわけではありませんが、繰り返しと比べると、少し改善します。

再帰を終了するには、条件をテストして、再帰呼び出しを行わずに、手続きを強制的に戻す必要があります。
再帰的手続きの定義に、条件のテストがないと、一度呼び出された手続きは、無限に再帰のままになります。

注: 再帰手続きのパラメーターが参照で渡される場合、コード本体で値を変更する必要がある場合は、ローカル変数を使うように注意してください。

整数の階乗を返す、再帰関数の簡単な例:

'' 再帰関数のコードは、階乗関数の再帰定義を使って定義されます:
''    Case (n = 0) : factorial(0) = 1
''    Case (n > 0) : factorial(n) = n * factorial(n-1)
''    最初の行は、終了条件を決定します: 'If (n = 0) Then Return 1'
''    2 行目は、関数自体を呼び出す命令文の構文を決定します: 'Return n * factorial(n - 1)'

Function recursiveFactorial (ByVal n As Integer) As Integer
    If (n = 0) Then                           '' 終了条件
        Return 1
    Else                                      '' 再帰ループ
        Return n * recursiveFactorial(n - 1)  '' 再帰呼び出し
    End If
End Function

Dim N As Integer

N = 6

Print Str(N) & "! =" ; recursiveFactorial(6)

Sleep


再帰構造
さまざまな形式の再帰構造があります:

末尾再帰
唯一の再帰呼び出しが、再帰の最後にあり、したがって他の命令文がその後に続かない場合は、再帰手続きは末尾再帰手続きです:
  • 再帰サブルーチンの場合、唯一の再帰呼び出しは、再帰の最後にあります。
  • 再帰関数の場合、唯一の再帰呼び出しは再帰の最後にあり、他の追加操作なしで関数の戻り値を取得します。
末尾再帰手続きは、簡単に反復手続きに書き変えできます。

単純な「階乗」再帰関数を使った例(既に上記に示しています):
  • この関数は、再帰呼び出しが関数の最後にあるにもかかわらず、再帰呼び出しが、'recursiveFactorial(n - 1)' が取得されたときに 'n' を再度乗算する必要があるため、関数の最後の命令ではないので、非末尾再帰形式になっています。
  • この計算は、実行スタックから文脈を取り出すときに行われます。

この関数を、再帰が末尾再帰になるように変換するのは、非常に簡単です。
これを実現するには、関数に新しいパラメーターを追加する必要があります:累算器として機能する 'result' パラメーターです:

Function tailRecursiveFactorial (ByVal n As Integer, ByVal result As Integer = 1) As Integer
    If (n = 0) Then                                       '' end condition
        Return result
    Else                                                  '' recursion loop
        Return tailRecursiveFactorial(n - 1, result * n)  '' tail recursive call
    End If
End Function

Print tailRecursiveFactorial(6)

Sleep

今回は、計算は、実行スタックに文脈を送るときに行われます。
以下は、単純な再帰関数「逆文字列」の、同様の変換手順:

Function recursiveReverse (ByVal s As String) As String
    If (s = "") Then                                     '' end condition
        Return s
    Else                                                 '' recursion loop
        Return recursiveReverse(Mid(s, 2)) & Left(s, 1)  '' recursive call
    End If
End Function

Print recursiveReverse("9876543210")

Sleep

Function tailRecursiveReverse (ByVal s As String, ByVal cumul As String = "") As String
    If (s = "") Then                                                '' end condition
        Return cumul
    Else                                                            '' recursion loop
        Return tailRecursiveReverse(Mid(s, 2), Left(s, 1) & cumul)  '' tail recursive call
    End If
End Function

Print tailRecursiveReverse("9876543210")

Sleep

注: "&" 演算子(文字列の連結)は対称演算子ではない( '(a & b) <> (b & a)', 一方 '(x * y) = (y * x)' )ため、 実行スタックから文脈を取り出す前ではなく、実行スタックに文脈を送るときに、2つの演算対象の順序を逆にする必要があります。

末尾再帰ではないが最終再帰
再帰呼び出しが、実行されたコードの末尾に配置されると、非末尾再帰手続きが最終になります(実行可能な命令行が、複数の再帰呼び出しの前後にありません)。

例1.組み合わせ係数(二項係数) nCp の計算と、パスカルの三角形の表示:

Function recursiveCombination (ByVal n As UInteger, ByVal p As UInteger) As LongInt
    If p = 0 Or p = n Then
        Return 1
    Else
        Return recursiveCombination(n - 1, p) + recursiveCombination(n - 1, p - 1)
    End If
End Function

Dim As UInteger n = 10
For I As UInteger = 0 To n
    For J As UInteger = 0 To I
        Locate , 6 * J + 3 * (n - I) + 3
        Print recursiveCombination(I, J);
    Next J
    Print
Next I

Sleep


円 例2.円の再帰的描画:

Sub recursiveCircle (ByVal x As Integer, ByVal y As Integer, ByVal r As Integer)
    Circle (x, y), r
    If r > 16 Then
        recursiveCircle(x + r / 2, y, r / 2)
        recursiveCircle(x - r / 2, y, r / 2)
        recursiveCircle(x, y + r / 2, r / 2)
        recursiveCircle(x, y - r / 2, r / 2)
    End If
End Sub

Screen 16

Locate 2, 2
recursiveCircle(160, 160, 150)

Sleep

例3.クイックソート・解法:

Dim Shared As UByte t(99)

Sub recursiveQuicksort (ByVal L As Integer, ByVal R As Integer)
    Dim As Integer pivot = L, I = L, J = R
    Do
        If t(I) >= t(J) Then
            Swap t(I), t(J)
            pivot = L + R - pivot
        End If
        If pivot = L Then
            J = J - 1
        Else
            I = I + 1
        End If
    Loop Until I = J
    If L < I - 1 Then
        recursiveQuicksort(L, I - 1)
    End If
    If R > J + 1 Then
        recursiveQuicksort(J + 1, R)
    End If
End Sub

Randomize
For I As Integer = LBound(t) To UBound(t)
    t(i) = Int(Rnd * 256)
Next I

Print "元の数列:"
For K As Integer = LBound(t) To UBound(t)
    Print Using "####"; t(K);
Next K
Print

recursiveQuicksort(LBound(t), UBound(t))

Print "昇順数列:"
For K As Integer = LBound(t) To UBound(t)
    Print Using "####"; t(K);
Next K
Print

Sleep

末尾でもなく最終でもない再帰
再帰呼び出しが、実行されたコードの最後(少なくとも実行可能な命令行が、いくつかの再帰呼び出しの後またはその間にある)に配置されていない場合、末尾再帰手続きでなく、また最終再帰でもありません。

例、ハノイの塔解法:

'' この例では、2 つの再帰呼び出しは、実行コードブロックの最後にあります。
''   しかし、命令行で区切られており、順序制約があります。

Sub recursiveHanoi (ByVal n As Integer, ByVal departure As String, ByVal middle As String, ByVal arrival As String)
    If n > 0 Then
        recursiveHanoi(n - 1, departure, arrival, middle)
        Print "  move one disk from " & departure & " to " & arrival
        recursiveHanoi(n -1 , middle, departure, arrival)
    End If
End Sub

recursiveHanoi(3, "A", "B", "C")

Sleep

相互再帰
最初の関数が2番目の関数を呼び出し、次に2番目の関数が最初の関数を呼び出す場合、2つの関数は相互に再帰的である、と言われます。

相互再帰を使った例:再帰関数 'even()' 、'odd()' :

Declare Function recursiveIsEven(ByVal n As Integer) As Boolean
Declare Function recursiveIsOdd(ByVal n As Integer) As Boolean

Function recursiveIsEven(ByVal n As Integer) As Boolean
    If n = 0 Then
        Return True
    Else
        Return recursiveIsOdd(n - 1)
    End If
End Function

Function recursiveIsOdd(ByVal n As Integer) As Boolean
    If n = 0 Then
        Return False
    Else
        Return recursiveIsEven(n - 1)
    End If
End Function

Print recursiveIsEven(16), recursiveIsOdd(16)
Print recursiveIsEven(17), recursiveIsOdd(17)

Sleep

入れ子になった再帰
再帰関数が、関数に渡された引数が関数自体を参照している場合、入れ子になっている、と言います。

入れ子になった再帰関数を使う例、'Ackermann()' 再帰関数:

Function recursiveAckermann (ByVal m As Integer, ByVal n As Integer) As Integer
    If m = 0 Then
        Return n + 1
    Else
        If n = 0 Then
            Return recursiveAckermann(m - 1, 1)
        Else
            Return recursiveAckermann(m - 1, recursiveAckermann(m, n - 1))
        End If
    End If
End Function

Print recursiveAckermann(3, 0), recursiveAckermann(3, 1), recursiveAckermann(3, 2), recursiveAckermann(3, 3), recursiveAckermann(3, 4)

Sleep

参照:
プログラマーのための案内に戻る
←リンク元に戻る プログラム開発関連に戻る

ページ歴史:2020-08-07 12:57:07
日本語翻訳:WATANABE Makoto、原文著作者:fxm

ホームページのトップに戻る

表示-非営利-継承