Javaでの再帰

Javaの再帰

1. 前書き

この記事では、あらゆるプログラミング言語のコアコンセプトである再帰に焦点を当てます。

recursive functionの特性を説明し、再帰を使用してJavaのさまざまな問題を解決する方法を示します。

2. 再帰を理解する

2.1. 定義

Javaでは、関数呼び出しメカニズムはthe possibility of having a method call itselfをサポートします。 この機能はrecursion.として知られています

たとえば、0からいくつかの値nまでの整数を合計したいとします。

public int sum(int n) {
    if (n >= 1) {
        return sum(n - 1) + n;
    }
    return n;
}

再帰関数には2つの主な要件があります。

  • A Stop Condition –関数は、特定の条件が満たされたときに、それ以上再帰的に呼び出すことなく値を返します。

  • The Recursive Call –関数は停止条件に一歩近いinputで自分自身を呼び出します

再帰呼び出しごとに、JVMのスタックメモリに新しいフレームが追加されます。 したがって、if we don’t pay attention to how deep our recursive call can dive, an out of memory exception may occur.

この潜在的な問題は、末尾再帰最適化を活用することで回避できます。

2.2. 末尾再帰と頭再帰

再帰関数をtail-recursionwhen the recursive call is the last thing that function executes.と呼びます。それ以外の場合は、head-recursionと呼ばれます。

上記のsum()関数の実装は、先頭再帰の例であり、末尾再帰に変更できます。

public int tailSum(int currentSum, int n) {
    if (n <= 1) {
        return currentSum + n;
    }
    return tailSum(currentSum + n, n - 1);
}

末尾再帰の場合、the recursive call is the last thing the method does, so there is nothing left to execute within the current function.

したがって、論理的には、現在の関数のスタックフレームを格納する必要はありません。

コンパイラcanはこのポイントを利用してメモリを最適化しますが、Java compiler doesn’t optimize for tail-recursion for nowは注意する必要があります。

2.3. 再帰と反復

再帰は、コードをより明確で読みやすくすることにより、いくつかの複雑な問題の実装を簡素化するのに役立ちます。

ただし、すでに見てきたように、再帰的なアプローチでは、再帰的な呼び出しごとに必要なスタックメモリが増えるため、多くの場合、より多くのメモリが必要になります。

別の方法として、再帰の問題を解決できる場合は、反復によって解決することもできます。

たとえば、sumメソッドは反復を使用して実装できます。

public int iterativeSum(int n) {
    int sum = 0;
    if(n < 0) {
        return -1;
    }
    for(int i=0; i<=n; i++) {
        sum += i;
    }
    return sum;
}

再帰と比較して、反復アプローチの方がパフォーマンスが向上する可能性があります。 そうは言っても、反復は再帰に比べて複雑で理解しにくいものになります。たとえば、二分木を横断することです。

頭の再帰、尾の再帰、反復アプローチの正しい選択はすべて、特定の問題と状況に依存します。

3. 例

それでは、いくつかの問題を再帰的に解決してみましょう。

3.1. 10のn乗を見つける

nの10の累乗を計算する必要があるとします。 ここで、入力はn.です。再帰的に考えると、最初に(n-1)の10の累乗を計算し、その結果に10を掛けることができます。

次に、10の(n-1)乗を計算するには、10の(n-2)乗になり、その結果に10を掛けます。 10の(n-n)乗、つまり1を計算する必要があるポイントに到達するまで、このように続けます。

これをJavaで実装したい場合は、次のように記述します。

public int powerOf10(int n) {
    if (n == 0) {
        return 1;
    }
    return powerOf10(n-1) * 10;
}

3.2. フィボナッチ数列のn番目の要素を見つける

0および1で始まり、the Fibonacci Sequence is a sequence of numbers where each number is defined as the sum of the two numbers proceeding it0 1 1 2 3 5 8 13 21 34 55

したがって、nの数が与えられた場合、問題はFibonacci Sequencen番目の要素を見つけることです。 To implement a recursive solution, we need to figure out the Stop Condition and the*Recursive Call*.

幸いなことに、それは本当に簡単です。

シーケンスのn番目の値をf(n)と呼びましょう。 次に、f(n) = f(n-1) + f(n-2)Recursive Call.があります

一方、f(0) = 0およびf(1) = 1Stop Condition)

次に、問題を解決するための再帰的な方法を定義することは非常に明白です。

public int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n-1) + fibonacci(n-2);
}

3.3. 10進数から2進数への変換

それでは、10進数を2進数に変換する問題について考えてみましょう。 要件は、正の整数値nを受け取り、バイナリString表現を返すメソッドを実装することです。

10進数を2進数に変換する1つの方法は、値を2で除算し、余りを記録して、商を2で除算し続けることです。

0の商が得られるまで、このように分割し続けます。 次に、残りをすべて予約順に書き出すことにより、バイナリ文字列を取得します。

したがって、私たちの問題は、これらの剰余を予約順に返すメソッドを書くことです。

public String toBinary(int n) {
    if (n <= 1 ) {
        return String.valueOf(n);
    }
    return toBinary(n / 2) + String.valueOf(n % 2);
}

3.4. 二分木の高さ

バイナリツリーの高さは、ルートから最も深いリーフまでのエッジの数として定義されます。 ここでの問題は、特定のバイナリツリーに対してこの値を計算することです。

1つの簡単なアプローチは、最も深い葉を見つけてから、ルートとその葉の間のエッジを数えることです。

しかし、再帰的な解決策を考えようとすると、二分木の高さの定義を、ルートの左ブランチとルートの右ブランチの最大高さに1を加えたものとして言い換えることができます。

ルートに左ブランチと右ブランチがない場合、その高さはzeroです。

実装は次のとおりです。

public int calculateTreeHeight(BinaryNode root){
    if (root!= null) {
        if (root.getLeft() != null || root.getRight() != null) {
            return 1 +
              max(calculateTreeHeight(root.left),
                calculateTreeHeight(root.right));
        }
    }
    return 0;
}

したがって、some problems can be solved with recursion in a really simple way.

4. 結論

このチュートリアルでは、Javaで再帰の概念を紹介し、いくつかの簡単な例でそれを実証しました。

この記事の実装はover on Githubにあります。