早速ですが、次のような問題を考えてみましょう。「ソウサ 4」の結果はいくつになるでしょうか。

ソウサ x:

  • x=1 のとき、ソウサ x の結果は 1 である。
  • x ≥ 2 のとき、ソウサ x の結果は「ソウサ x-1 の結果に x を足した値」である。

答えは下図の通り 1+2+3+4=10 になります。

さて、この例では答えを求めるために「ソウサの結果」を呼び出しており、このように自分自身を引用することを「再帰的定義」と言います。また、プログラミングにおいて自分自身を呼び出す関数を「再帰関数」と言います。再帰関数は、複雑なアルゴリズムを簡潔に実装するときに便利なツールである一方、初学者にとっては最初の壁になりがちです。そこで今回は、3 つの例を用いてわかりやすく解説していきたいと思います。

再帰関数の例 (1):足し算

冒頭の例を Python で実装すると次のようになります。元々は「ソウサ x」だったのが sousa(x) になったと考えるとイメージしやすいと思います。

# sousa(x) は自分自身を呼び出す「再帰関数」
def sousa(x):
    if x == 1:
        return 1
    return sousa(x-1) + x

下図は、sousa(4) を呼び出した場合の挙動のイメージを表しています。返り値は前述の「ソウサ 4」と同じ 10 です。

再帰関数の例 (2):フィボナッチ数列

次に紹介する例は「フィボナッチ数列」です。フィボナッチ数列は「第 1 項・第 2 項が 1 で、それ以降は前の 2 つを足した値」となるような数列のことを指し、第 10 項までは以下のようになります。

 n   1   2   3   4   5   6   7   8   9   10 
第 n 項  1   1   2   3   5   8   13   21   34   55 

さて、フィボナッチ数列の第 n 項を求めるにはどうすれば良いのでしょうか。本連載の第 7 回第 8 回 で紹介した動的計画法を用いて計算することも可能ですが、今回は再帰関数を利用してみましょう。

def fib(n):
    if n <= 2:
        return 1
    return fib(n-1) + fib(n-2)

下図は、fib(4) を呼び出した場合の挙動のイメージを表しています。少し複雑ですが、最終的な返り値は 3 です。なお、このプログラムは n に対して計算回数が指数関数的に増加するため、n ≥ 50 だと現実的な時間内に処理が終わらないことに注意してください。

第7回「動的計画法(その 1)」はこちらから
第8回「動的計画法(その 2 ):ナップザック問題」はこちらから

再帰関数の例 (3):文字列を全部出力

再帰関数のイメージは掴めたでしょうか。最後にやや難しい例として、「a と b から成る N 文字の文字列をすべて出力する」というケースを考えてみましょう。単純に for 文を用いて実装することも可能ですが、以下のようなプログラムになり、とても面倒です。もっとスマートなやり方はないのでしょうか。

# N = 1 のとき
if N == 1:
    for s1 in ['a', 'b']:
        print(s1)

# N = 2 のとき
if N == 2:
    for s1 in ['a', 'b']:
        for s2 in ['a', 'b']:
            print(s1 + s2)

# N = 3 のとき
if N == 3:
    for s1 in ['a', 'b']:
        for s2 in ['a', 'b']:
            for s3 in ['a', 'b']:
                print(s1 + s2 + s3)

# N = 4 のとき(以下略)
if N == 4:
    for s1 in ['a', 'b']:
        for s2 in ['a', 'b']:
            for s3 in ['a', 'b']:
                for s4 in ['a', 'b']:
                    print(s1 + s2 + s3 + s4)

ここで再帰関数を使うと、次に示すような短いプログラムで正しく出力することができます。ここで、引数 N は出力したい文字列の長さ、引数 cur_string は現在の文字列を意味し、cur_string に 1 文字ずつ追加することによって再帰呼び出しを行います。

def func(N, cur_string):
    if len(cur_string) == N: # 文字数が N に達した場合
        print(cur_string)
    else:
        func(N, cur_string + "a") # s の後ろに "a" を付けた文字列にする
        func(N, cur_string + "b") # s の後ろに "b" を付けた文字列にする

# func(N, "") を呼び出すと、N 文字の a, b から成る文字列がすべて出力される
func(2, "")

下図は、関数 func(2, "") を呼び出した場合の挙動のイメージを表しています。文字列はアルファベット順に出力され、N=2 のときは aaabbabb の順になります。

* * *

このように、自分自身を呼び出す関数を「再帰関数」と言います。再帰関数は慣れるまでは難しく感じますが、3 つ目の例のような複雑な全探索から、本連載の後半で紹介する予定の「深さ優先探索(DFS)」まで、多くのケースで利用されます。皆さんも実際にプログラムを書いて練習してみましょう。