web-dev-qa-db-ja.com

なぜ再帰はスタックの最後ではなく最初の呼び出しを返すのですか?

これがなぜ5ではなく0を返すのか、私の人生ではわかりません。iは、最後のreturnステートメントに到達する前にインクリメントされ続けますが、最初の呼び出しから常に0を返しますスタック。スタックの最新の呼び出しはブロックi == 5returnに最初に到達するため、5が返されて出力されると思います。

0を返します

public static void main(String[] args) {
    System.out.println(incrementI(0));
}       


public static int incrementI(int i) {
    if (i == 5){
        return i;           
    } else {
         incrementI(i + 1);
    }
    return i;  
}

5を返します

public static int incrementI(int i) {
    if (i == 5){
        return i;           
    } else {
        return incrementI(i + 1);
    }               
}

私はコード修正を探しているのではなく、再帰がこのように機能する理由を説明しています。

6
NightSkyCode

ここでのキーはスタックフレームです。最初の例を見てみましょう:

public static int incrementI(int i) {
    if (i == 5){
        return i;           
    } else {
         incrementI(i + 1);
    }
    return i;  
}

first呼び出しでは、iという名前の変数があり、値は0です。 iは5ではないので、1を使用してincrementIを再度呼び出します。

今回は新しいスタックフレームと、i()という名前のdifferent変数があります。 incrementIを呼び出すたびに、新しい値で新しいiが作成されます。

incrementIを6回呼び出した場合、各関数呼び出しにはiの独自のコピーがあります。

 incrementI: 5
 incrementI: 4
 incrementI: 3
 incrementI: 2
 incrementI: 1
 incrementI: 0

最初の関数は5(つまりi)を返し、次の関数は4(つまりitsを返します)コピーof i)、ボトム関数が0を返すまで。

2番目の例では、同じことが発生します各関数が上記の関数が返したものを返します。したがって、上の関数は5を返し、次の関数は5を返します(それが前の関数が返したものです)、など。

18
Nathan Merrill

コードの最初のスニペットを見て、ケース分析を行ってみましょう。

いつ i == 5

コードの最初のスニペットは次と同等です。

public static int incrementI(int i) {
    return i;           
}

さもないと

ただし、iが5と異なる場合、コードは次と同等です。

public static int incrementI(int i) {
    // if (i == 5){
    //    return i;           
    // } else {
         incrementI(i + 1);
    //}
    return i;  
}

関数には、それ自体を呼び出す以外に副作用はありません。上記の5から開始した場合は終了しない可能性がありますが、これは幸いにもあなたの例では当てはまりません。コードが他の場所から呼び出されていないと想定し、帰納的な証明があれば、コードは次のようになります。

public static int incrementI(int i) {
    return i;  
}

結論

どちらの場合でも、コードは単純に入力を返します。スタックオーバーフローが発生する可能性があることを除いて、これはアイデンティティ関数とほとんど同じです。

5
coredump

ネイサンメリルの答えは正しいです。これが状況を説明する標準的な方法です。

状況を説明するもう1つの方法は、関数が呼び出されるたびに、関数のcopyが作成されることを想像することですその時点で、すべての形式それらの値に置き換えられます。 (これは形式は変数であるであるという完全に関連する事実を無視します。それらがそうではないことを少しの間、装いましょう。)

さらに、ifが評価されるときに、条件がtrueの場合はifが結果に置き換えられ、falseの場合は代替に置き換えられます。

incrementI(0)を呼び出して、このコードを表示します。

if (0 == 5){
    return 0;           
} else {
     incrementI(0 + 1);
}
return 0;

何が起こるのですか? 0 == 5はfalseなので、これはコードと同等です。

incrementI(0 + 1);
return 0;

呼び出しが戻ると、0が返されます。呼び出しがクラッシュすると、プログラムがクラッシュします。呼び出しがハングすると、プログラムがハングします。呼び出しが正常に戻ると仮定しましょう。したがって、メソッドは0を返します。

ここで5を渡したとします。これにより、このコードは「魔法のように」表示されます

if (5 == 5){
    return 5;           
} else {
     incrementI(5 + 1);
}
return 5;

5 == 5はtrueなので、これは

return 5;           
return 5;

これで、引数が与えられた場合にメソッドが返す結果を返す理由が明らかになったはずです。

この種の「等式推論」は、HaskellやMLなどの関数型プログラミング言語でより一般的に使用されますが、フォーマルを変更しない限り、Javaのような言語でもかなりまともな推論手法です。

5
Eric Lippert

何が起こっているのかを理解するには、バグを理解する必要があります。ifの1つのブランチでは、返されるものを変更しません。

最初の例はこれで置き換えることができます:

public static void main(String[] args) {
    System.out.println(incrementI(0));
}       


public static int incrementI(int i) {

  if (i==0){
    incrementI(1);
    incrementI(2);
    incrementI(3);
    incrementI(4);
    incrementI(5);
  }

  return i;
}

またはこれで:

public static int incrementI(int i) {
    if (i==0){
     // incrementI(1); line commented out, it had no effect on result
     // incrementI(2); line commented out, it had no effect on result
     // incrementI(3); line commented out, it had no effect on result
     // incrementI(4); line commented out, it had no effect on result
     // incrementI(5); line commented out, it had no effect on result
    }
    return i;
}

再帰には2つの目的があります。入力のシーケンスに基づいて値を計算すること、またはシーケンスの要素に作用することです。アクションを実行せず、値を計算して返さない再帰関数の呼び出しは、目的を果たさないためスキップできます。

1
jmoreno