再帰
関数を呼び出すプロセスでは、関数自体が直接または間接的に呼び出されます。これは、関数の再帰呼び出しと呼ばれます。
ドロステ効果は、再帰の視覚的な形式です。
再帰は通常、大きくて複雑な問題を、解決する元の問題と同様の小さな問題に変換します。問題がある程度小さい場合、答えは明白です。
再帰には、境界条件、再帰的な順方向セクション、および再帰的な戻りセクションが必要です。
「Recursion」と「return」の2つのプロセスへの再帰関数の実行。これらの2つのプロセスは、再帰的な終了条件が満たされるまで、終了条件、つまり、レイヤーごとの再帰によって制御されます。終了再帰は、レイヤーごとに、その後
、通常の再帰呼び出し関数は同じで、呼び出しが発生するたびに、新しいスタックフレーム(新しいパラメータデータ、フィールド保護、ローカル変数)を割り当てる必要があります。再帰プロセスはレイヤーバイレイヤーであるため、通常の関数とは異なります。レイヤー呼び出しプロセスがあります。レイヤーごとにスタックフレームを連続して割り当てるプロセスは、再帰的な終了条件が発生するまで戻り始めません。その後、スタックフレームスペースがレイヤーごとに解放され、前のレイヤーに戻り、最後に戻ります。メインの呼び出し関数に。
例:5人の生徒が一緒に座っていて、5人目の生徒は何歳ですか?彼は4人目の生徒より2歳年上だと言い、4人目の生徒の年齢を尋ねた。3人目の生徒より2歳年上だと言った。3人目の生徒に聞いた。彼は2歳以上だったと言った。 2番目の生徒。2番目の生徒に聞いてください。生徒は最初の生徒より2歳年上だと言い、最後に最初の生徒に尋ねました。彼は10歳だと言いました。5番目の生徒は何歳ですか。
非再帰的な問題解決方法は次のとおりです。
//非递归求年龄
int Age(int n)
{
int tmp = 10; //第一个人年龄
for(int i=1;i<n;i++)
{
tmp += 2;//后面的人比前一个多 2 岁
}
return tmp;
}
では、再帰をどのように処理するのでしょうか?
Age関数を使用して年齢を見つける場合、Age
(1)は最初の人の年齢を表し、
Age(2)は2番目の人の年齢を表します。
…
Age(n-1)は最初の人の年齢。n-1人の年齢
。Age(n)はn番目の人の年齢を表します。
//递归求年龄
int Age(int n)
{
int tmp;//保存年龄
if(n == 1)
tmp = 10;
else
tmp = Age(n-1) + 2;//当前第n个比第n-1个年龄多 2
return tmp;
}
上図の赤は、関数の呼び出しプロセスを示しています。このプロセスでは、各関数がまだ実行されていないため、各関数が占有していたメモリスペースを解放できず、関数の呼び出しは特定のスタックを占有する必要があります。スペース(スタックフレーム)であり、スタックスペースが非常に小さい(動的メモリの章で、スタック1Mについて説明しました)。再帰の数が非常に多い場合は、スタックスペースが不足している可能性があります。
//递归求年龄
int Age(int n)
{
int tmp;//保存年龄
if(n == 1)
tmp = 10;
else
tmp = Age(n-1) + 2;//当前第 n 个比第 n-1 个多 2
return tmp;
}
//递归调用次数太多,程序崩溃
int main()
{
printf("%d\n",Age(5000));//windows 系统,程序崩溃
return 0;
}
例:再帰を使用して階乗nを見つけます!
//递归求 n 的阶乘
//Fac(0)表示 0 的阶乘
//Fac(1)表示 1 的阶乘
//Fac(2)表示 2 的阶乘
//......
//Fac(n-1)表示 n-1 的阶乘
//Fac(n)表示 n 的阶乘
#include<stdio.h>
int Fac(int n)
{
if(n==0 || n==1)
return 1;
else
return Fac(n-1)*n;
}
int main()
{
for(int i=0;i<10;i++)
{
printf("%d!=%d\n",i,Fac(i));
}
return 0;
}
再帰を使用して1+ 2 + 3 + ... + nを見つけます。
//递归求和
//Sum(1)表示从 1 加到 1
//Sum(2)表示从 1 加到 2
//Sum(3)表示从 1 加到 3
//......
//Sum(n-1)表示从 1 加到 n-1
//Sum(n)表示从 1 加到 n
int Sum(int n)
{
if(n < 0) return -1;
if(n==0 || n==1)
return n;
else
return Sum(n-1) + n;
}
例:再帰を使用してフィボナッチ数列を見つけることは、
実際には再帰に最も不適切な例です。
//非递归求斐波那契数列
#include<stdio.h>
int Fibon_for(int n)
{
int f1 = 1;
int f2 = 1;
int f3 = 1;
for(int i=2;i<n;i++)
{
f3 = f1 + f2;
f1 = f2;
f2 = f3;
}
return f3;
}
//递归求斐波那契数列
int Fibon(int n)
{
if(n==1 || n==2)
return 1;
else
return Fibon(n-1) + Fibon(n-2);
}
int main()
{
printf("非递归结果:");
for(int i=1;i<10;i++)
{
printf("%d ",Fibon_for(i));
}
printf("\n");
printf("递归结果: ");
for(int i=1;i<10;i++)
{
printf("%d ",Fibon(i));
}
return 0;
}
値が大きい場合、2つの実行の効率(時間)は大きく異なり
ます。再帰処理を使用します。時間計算量はO(2 ^ n)、つまり2のn乗です。
非再帰的処理を使用します。時間計算量はO(N)であり、
カラムは、後述することが最も再帰リースに適さないフィボナッチ数は、再帰の好適例で説明:ハノイの塔
ハノイの塔。古代にはバチカンの塔がありました。塔にはA、B、Cの3つの座席がありました。最初は、Aの座席に
いくつかのプレートがありました。プレートはサイズが異なり、大きい方が下部と上部の小さい方。老僧はこれらのプレートをシートAからシートCに移動したいと考えていましたが、一度に移動できるプレートは1つだけであり、移動中は常に大きなプレートが下にあり、小さなプレートは頂上で。ブロックBは移動中に使用できます。プレートを動かすステップを出力するようにプログラムするように要求します
上記は2枚のプレートの場合で、非常に簡単です。プレートが3つある場合はどうなりますか?
この状況に達した場合にのみ、図2と図5を注意深く分析し、下部プレートをAからCに移動してから、他の小さなプレートをCに移動できます。
#include<stdio.h>
//模拟从 x 搬运到 y 的过程
void Move(char x,char y)
{
printf("%c -> %c\n",x,y);
}
//将 n 个盘子的汉诺塔从 a 通过 b 搬到 c
void Hanoi(int n,char a,char b,char c)
{
if(n == 1)//只有一个盘子,直接搬
{
Move(a,c);
}
else //先将上面的n-1个搬到b上,然后搬最下边的一个到c,再把n-1个从b搬到c
{
Hanoi(n-1,a,c,b);//上面 n-1 个从 a 通过 c 搬到 b
Move(a,c);//只剩最后一个,直接搬
Hanoi(n-1,b,a,c);//把上面搬到 b 上的 n-1 个盘子有从 b 通过 a 搬到 c
}
}
int main()
{
Hanoi(2,'A','B','C');
return 0;
}
操作の結果は、
再帰プロセスを説明するために次のとおりです。
要約:関数が呼び出されると、それ自体または他の関数によって呼び出されるかどうかに関係なく、スタックフレームが呼び出された関数に割り当てられます。
無限の再帰はありません。
つまり、再帰関数には、再帰の終わりである出口が必要です(再帰で終了する条件ステートメントが存在する必要があります)
。問題の規模が大きすぎてはならず、再帰が深すぎて、スタックオーバーフロー
次の質問を見てください:
整数(符号なし整数)を入力し、再帰的アルゴリズムを使用して整数を逆の順序で出力します。
上記の2つの方法の出力結果は何ですか?
左:4 3 2 1
printf( "%d"、n%10);その後、毎回再帰的に実行されます
#include<stdio.h>
void backward(int n)
{
if(n>0)
{
printf("%d ",n%10);
backward(n/10);
}
}
int main()
{
int n=0;
scanf("%d",&n);
printf("原整数:%d\n",n);
printf("反向数:");
backward(n);
printf("\n");
return 0;
}
右:1 2 3 4
は、終了条件が発生するまで再帰的に保持し、その後、1つずつ印刷に戻ります。
#include<stdio.h>
void backward(int n)
{
if(n>0)
{
backward(n/10);
printf("%d ",n%10);
}
}
int main()
{
int n=0;
scanf("%d",&n);
printf("原整数:%d\n",n);
printf("反向数:");
backward(n);
printf("\n");
return 0;
}
再帰の終了条件は非常に重要です。そうしないと、無限に再帰して無限ループに陥り、最終的にスタックスペースが使い果たされ、StackOverflowエラーが報告されます。
注:
1。制限:再帰プロセスを設計するときは、再帰を終了できる条件が少なくとも1つ必要です。また、妥当な数の再帰呼び出し内でそのような条件が満たされない状況にも対処する必要があります。通常の状況で満たすことができる条件がない場合、プロセスは無限ループを実行するリスクが高くなります
。2。メモリ使用量:アプリケーションのローカル変数によって使用されるスペースは限られています。プロシージャがそれ自体を呼び出すたびに、ローカル変数の追加のコピーを保存するためにより多くのメモリスペースが必要になります。このプロセスが無期限に続くと、最終的にStackOverflowExceptionエラー
3が発生します。効率:ほとんどすべての状況で、再帰の代わりにループすることが可能です。ループは、変数の受け渡し、追加のストレージスペースの初期化、および戻り値に必要なオーバーヘッドを生成しないため、ループを使用することは、再帰呼び出しを使用してパフォーマンスを大幅に向上させることと同じです
。4。相互再帰:2つのプロシージャが相互に呼び出すと、パフォーマンスが低下する可能性があります。無限再帰。この種の設計によって引き起こされる問題は、単一の再帰的プロセスによって引き起こされる問題と同じですが、検出とデバッグがより困難です
。5。呼び出すときに括弧を使用する:Functionプロシージャがそれ自体を再帰的に呼び出す場合、括弧の後に括弧を追加する必要があります。プロシージャ名(パラメータリストがない場合でも)。それ以外の場合、関数名は関数の戻り値と見なされます
。6。テスト:再帰的プロセスを作成するときは、常に特定の制約を満たし、再帰的プロセスを終了できることを確認するために、慎重かつ自信を持ってテストする必要があります。過度の再帰呼び出しによるメモリ不足からの保護