【C言語】関数再帰

皆さんこんにちは、深魚です〜 

目次

1. 関数再帰の知識の説明

1. 再帰とは何ですか?

2. 再帰に必要な 2 つの条件

2.1 演習 1:

2.2 演習 2:

 2. 再帰と反復

2.1 演習 3

2.2 演習 4


1. 関数再帰の知識の説明

1. 再帰とは何ですか?

プログラムが自分自身を呼び出すプログラミング手法を再帰といいます。

プロシージャまたは関数には、その定義または説明内で直接的または間接的にそれ自体を呼び出すメソッドがあり、これにより通常、大規模で複雑な問題が層ごとに、元の問題と同様の小規模な問題に変換され、解決されます。

再帰的戦略:問題解決プロセスに必要な複数の繰り返し計算を記述できるプログラムは少数であり、プログラム内のコード量が大幅に削減されます。

再帰についての主な考え方は次のとおりです: 大きなものを小さくする

最も単純な再帰: main 関数が自分自身を呼び出す

#include<stdio.h>
int main()
{
	printf("hehe\n");
	main();
	return 0;
}

コードの結果:終了する前にプログラムがクラッシュするまでノンストップで hehe を出力します。

 Q:では、なぜプログラムがクラッシュするのでしょうか?

原因:スタック オーバーフロー スタックがオーバーフローしました。プログラムごとに、スタックが使用できるメモリには制限があり、プログラムが使用するスタック領域が最大値を超えると、スタック オーバーフローが発生し、プログラムがクラッシュします。

これは間違ったプログラムですが、このプログラムの main 関数はそれ自体を継続的に呼び出し、スタックがオーバーフローするまで継続的に再帰します。

2. 再帰に必要な 2 つの条件

制約条件があり、制約条件が満たされると再帰は続行されません

再帰呼び出しのたびに、この制限に近づき続けます

2.1 演習 1:

整数値 (符号なし) を受け取り、その各ビットを順番に出力します。

例: 入力: 1234、出力 1 2 3 4

最も一般的な方法:入力数値を引き続き %10、/10 にし、それを配列に保存し、配列内の各要素を出力します。

再帰メソッドの書き方を見てみましょう。

まず、関数がそれ自体を呼び出すため、カスタム関数を作成する必要があります。次に、画面上に入力値の各ビットを出力する Print 関数を作成し、次に、大きなものを小さくするというアイデアを使用します。

Print(1234)//常に単純化し、残り 1 桁になるまで毎回 1 桁を削除し、その後直接印刷します

プリント(123)+4

印刷(12)+3 +4

印刷(1)+2 +3 +4

1+2+3+4

 コード:

#include<stdio.h>
void Print(int n)
{
	if (n > 9)
	{
		Print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	int num = 0;
	scanf("%d", &num);
	Print(num);
	return 0;
}

絵を描いて理解を深めます。

再帰: 再帰 + 回帰

(1) 入力値が 1234 の場合、n=1234 の場合、9 より大きい場合は直接 if 文に入り、n/10 で次の Print 関数に入ります。なお、このとき printf("% d ", n % 10); ステートメントは実行されません。

(2) 次の Print 関数に入るとき、n=123、もう一度 if ステートメントを入力し、n/10 次の Print 関数に入る、この時点では printf("%d ", n % 10); このステートメントはまだ実行されていません。 、

(3) 次の Print 関数に入るとき、n=12、次に if ステートメント、n/10 に入る、次の Print 関数に入る、この時点では printf("%d ", n % 10); このステートメントはまだ実行されていません。 、

(4) 次の Print 関数に入るとき、n=1、if ステートメントを直接スキップして、1 を出力します。 

しかし、なぜこの時点では 1 だけが出力されるのでしょうか?

(5) これは、Print 関数が 3 回前に呼び出されたときにコードの最後の行が終了していなかったためです。つまり、再帰は再帰レベルを完了しただけであり、回帰はまだ実行されていません。次に、戻って printf( "%d ", n % 10 ); このステートメントは回帰のプロセスです。このように言うと少しわかりにくいかもしれません。もう一度図を見てみましょう (赤は再帰のプロセスを表し、青は再帰のプロセス表します)回帰過程)

 さて、前に紹介した再帰を見てみましょう, それは理解しやすいでしょう. 大きなものを小さなものに減らすというアイデアを通じてプログラムの繰り返し部分をマージするだけです, それは大幅に量を減らしますコード。

n=123 の場合、関数スタック フレームが作成されると、Print 関数用のスペースが 3 回開かれ、各呼び出しには Print 関数呼び出し用のスペースがあることがわかります。

別の質問 について考えてみましょう。if ステートメントが削除されても問題ありませんか?

回答:間違いなく不可能です。削除された場合は、Print 関数を実行してください。再帰を続けた場合、結果はデッド再帰になります。これは再帰に必要な条件の 1 つです。制限事項

もう一つの質問: Print 関数の Print(n/10) を Print(n) に変更しても問題ありませんか?

回答:これも不可能です。n が変化し続ける場合、常に調整することになります。n が常に減少し再帰的であるからこそ、Print 関数は戻り続けることができます。これも再帰に必要な条件です。: に近づき続ける各再帰呼び出し後の制限条件

2.2 演習 2:

関数を作成するときに一時変数を作成することはできません。文字列の長さを調べてください。

strlen 関数を見て文字列の長さを調べてみましょう。strlen 関数の戻り値の型は size_t です。

size_t は型、符号なし整数です

size_t は sizeof 用に設計されています

size_t 型のデータを印刷する場合は %zd を使用します

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abc";
	size_t len = strlen(arr);
	printf("%zd", len);
	return 0;
}

実際、このトピックは strlen 関数の実現をシミュレートすることです。

(1)非再帰的メソッドは strlen 関数を実装します。長さを求めるときに変数 count が作成されることがわかりますが、これは質問の意味を満たしていません。

#include<stdio.h>
size_t my_strlen(char* arr)
{
	size_t count = 0;
	while (*arr != '\0')
	{
		arr++;
		count++;
	}
	return count;
}
int main()
{
	char arr[] = "abc";
	size_t len = my_strlen(arr);//这里的arr传递的是数组首元素的地址
	printf("%zd", len);
	return 0;
}

(2)再帰的メソッドは strlen 関数を実装します。

大きなものを小さくするというアイデアを使用します。

my_strlen("abc")

1+my_strlen("bc")

1+1+my_strlen("c")

1+1+1+my_strlen("")

1+1+1+0

 コード:

#include<stdio.h>
size_t my_strlen(char* str)
{
	if (*str != '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}
int main()
{
	char arr[] = "abc";
	size_t len = my_strlen(arr);//这里的arr传递的是数组首元素的地址
	printf("%zd", len);
	return 0;
}

[リマインダー]:コード関数の再帰部分が return 1 + my_strlen を記述する場合、 str++ と ++str には大きな違いがあるため、関数 recursion に ++ を記述するのではなく、 str+1 を直接記述することが最善です (str++)、結果は得られません(後者の++はstrの値を代入してから++を行うため、この処理はstrが変化していないことと同じであり、結果はありません)。ここは++strと書くべきですが、この書き方はお勧めできません。間違いやすいですし、strの戻りがあるとどちらも間違ってしまいます。

図面分析:

再帰:

str が a を指し、*str が a の場合、1 + my_strlen(str+1) を返し、次の my_strlen 関数を入力します。

次回関数 str が b を指すときは、引き続き 1 + my_strlen(str+1) が返され、引き続き次の my_strlen 関数の入力が行われます。

次に、c を指す次の関数 str を入力するか、1 + my_strlen(str+1) を返し、次の my_strlen 関数を入力します。

今回は、\0 を指す関数 str を入力し、0 を返し、戻り値を開始します。

戻る:

my_strlen(str+1)=0 に最後から 2 番目のカスタム関数の 1 を加えたものを返し、1 を返します。

return 1 は前のレイヤー my_strlen(str+1) に最後から 2 番目のカスタム関数の 1 を加えたものを返します。 return 2;

return 2 は前のレイヤー my_strlen(str+1) に最後から 2 番目のカスタム関数の 1 を加えたものを返します。 return 3;

最後に、カスタム関数の値をメイン関数に渡し、結果を出力します。

 2. 再帰と反復

反復はループであるため、理解するのは簡単です。

2.1 演習 3

n の階乗を求めます (オーバーフローを考慮せずに)

 数学では、n<=1、階乗は 1、n>1、階乗は n*Fac(n-1) です。

5!=5*4! (これによりコードが単純化され、1 から n まで乗算されなくなります)

コード:

#include<stdio.h>
int Fac(int n)
{
	if (n <= 1)
		return 1;
	else
		return n*Fac(n - 1);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int r=Fac(n);
	printf("%d", r);
	return 0;
}

しかし、このコードにはまだ問題があります: 入力値が 100 の場合、出力結果は 0 になります。入力値が 10000 の場合、スタックは直接オーバーフローします (理由は、この再帰関数が再帰的であるにもかかわらず、戻り値が返されていないためです) 、スタックスペースを大量に消費します

上記のコードを反復 (ループ) 方法に変更して改善できます。

#include<stdio.h>
int Fac(int n)
{
	int i = 0;
	int r = 1;
	for (i = 1; i <= n; i++)
	{
		r = r * i;
	}
	return r;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int r = Fac(n);
	printf("%d\n", r);
	return 0;
}

このコードで得られる結果にもエラーは発生しますが、スタック オーバーフローは発生しません。

これら 2 つのコードから、再帰は実際にはループに置き換えることができますが、どちらにも利点と欠点があることがわかります。

2.2 演習 4

n 番目のフィボナッチ数を検索します (オーバーフローに関係なく)

アイデア:数式

 

 コード:

#include<stdio.h>
int Fbi(int n)
{
	if (n <= 2)
		return 1;
	else
		return Fbi(n - 1) + Fbi(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fbi(n);
	printf("%d", ret);
	return 0;
}

しかし、このコードにはまだ問題があります。

入力する数値が大きいと計算に時間がかかり、しばらく出なくなります 例えば50と入力すると計算に時間がかかります

このコードがどのように計算するかを見てみましょう。

50 を入力すると、49 と 48 を計算する必要があり、49 と 47 を計算する必要があり、48 と 47 と 46 を計算する必要があります。同様に、50 番目のフィボナッチ数を計算するには多くの計算が必要になるため、プログラムは長い間、数値が何度も繰り返し計算され、多くの時間を無駄にしていることがわかります。

ただし、このコードにはスタック オーバーフローがありません

非常に多くの分岐がありますが、50 番目のフィボナッチ数を計算するときに、最初に 49 番目のフィボナッチ数を計算するため、実際には再帰の深さは 50 だけです。一番左の行 50 対 1 が計算されます。再帰の深さは 50 回のみなので、スタック オーバーフローは発生しません。

 上記のコードを反復 (ループ) 方法に変更して改善できます。

実際、通常の計算は、前から後ろに 1、1、2、3、5、8 と計算され、最初の 2 つの数値の加算が 3 番目の数値に等しくなります。その後、再度循環し、次のサイクルで次の数値を加算する必要があります。 2 番目の数 ボナッチ数が最初の数とみなされ、3 番目のフィボナッチ数が 2 番目の数とみなされ、必要な数がカウントされてループが停止するまでこのように加算が実行されます。

コード:

[注意]: c は 1 に初期化する必要があります。そうしないと、入力 n が 2 の場合、c の初期値がループに入らずに直接出力されます。

#include<stdio.h>
int Fbi(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
		c = a + b;
		a = b; 
		b = c;
		n--;
	}
	return c;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fbi(n);
	printf("%d", ret);
	return 0;
}

 このコードの利点: 計算が速く、計算を繰り返す必要がない

                    デメリット: int型は大きすぎる数値を格納できない、大きすぎると数値が収まらず結果が不正になる

【要約】:

再帰の使用が容易で、記述されたコードに明らかな欠陥がない場合は、再帰的手法を使用できますが、再帰的手法にスタック オーバーフロー、低効率などの問題がある場合は、依然として再帰的手法を使用できます。問題を解決するには反復 (ループ) メソッドの使用を検討してください。また、スタック領域のスペース不足による圧力を軽減するために静的静的変数を使用することもできます。

関数再帰の内容はこれで終わりです. 初心者にとってはまだ内容がわかりにくく, トピックも比較的難しいです. いくつかの関連コンテンツは将来公開される予定です: ハノイの塔の問題, 問題段差で飛び跳ねるカエルの, これらはすべて関数です 再帰は典型的なトピックです。ご質問がある場合は、コメント エリアまたはプライベート メッセージを歓迎して通信してください。著者の文章は大丈夫だと思います。または、少し得したと思います。助けてください、ワンクリックトリプルリンクをお願いします、ありがとうございます!

おすすめ

転載: blog.csdn.net/qq_73017178/article/details/131997526
おすすめ