<《アルゴリズムが美しい>>-(2)再帰的思考の詳細な説明

コンテンツ

序文

再帰の基本概念

再帰の3つの要素

ケーキカット思考:再帰的な簡単な演習

最大公約数を解くための再帰式の使用

これ以上のことはありません:挿入ソートは再帰的にソートされます

二分探索の再帰的ソリューション

マルチブランチ再帰:フィボナッチ数列 

再帰のいくつかの最適化のアイデアについて話す

トピック戦闘

最終要約


序文

私が最初に再帰に触れたとき、私の友人のほとんどは私と同じように混乱しているに違いないと思います。彼らはそれが素晴らしいとため息をついたので、このような短いコード行で多くのことが行われました。

実際、恐れることはありません。簡単に学ぶことができます。再帰境界再帰の概念の使い方を考えるだけです。元の問題をいくつかのサブ問題に分解します。再帰を記述するだけで済みます。と境界、そして残りはコンピュータに任せます。、私たちは上司です、それは私たちのためです。私たちは理解できないコードを理解するのを助けるために再帰的な図を描き、それらのスタックスタッキングプロセスについて考えないようにします私たちの頭の中で私たちの脳はいくつかのスタックを押すことができます!代わりに、彼は自分自身を混乱させます。

最近、再帰について多くの知識を読みました。少しお役に立てれば幸いです。私の意見を皆さんと共有したいと思います。

再帰の基本概念

一見冗談っぽい再帰の定義があります:「再帰を理解するには、まず再帰を理解するまで再帰を理解する必要があります」が、この再帰の説明は非常に直感的です。再帰は独自の関数を繰り返し呼び出すことですが、毎回問題があります。範囲が絞り込まれて境界データを直接取得できるようになるまで絞り込み、帰りに対応する解を求めますこの観点から、再帰は分割統治法のアイデアを実装するのに非常に適しています。

再帰の3つの要素

3つの要素をよりよく引き出すために、簡単な例を見てみましょう。再帰を使用してnの階乗を解きます。

1.重複(サブ問題)を見つける-再帰的

まず、n!の計算式を与えます。n!= 1 * 2 * 3 * ... * n;この再帰形式n!= n *(n-1)!なので、スケールはnです。問題は次のように変換されます。サイズn-1の問題。n!がf(n)で表される場合、f(n)= f(n-1)* n(再帰的)と書くことができるので、スケールを小さくする-サブ問題。(注:再帰を見つけることは、再帰の中で最も難しいステップです。経験を要約するために、さらに質問に連絡する必要があります。次に、いくつかの質問を練習します。さらに練習して、発生したループを再帰に変更する必要があります)

int f(n)
{
   return f(n-1)*n
}

2.繰り返しの変化量を見つける-パラメータ

変化量をパラメータとして使用する必要があります。この問題には変数nが1つしかないため、明らかにパラメータとして使用されます。この問題では、この点の役割はそれほど重要ではありません。以下に、配列の合計の問題について説明します。その価値。

3.パラメータ変更の傾向を見つける-デザインのエクスポート

上記のコードは自分自身を呼び出す状況があるため、再帰と呼ばれますが、上記のコードは十分に標準化されておらず、問題が発生しても、自分自身を呼び出し続け、底なしの穴に落ち、最終的にスタックオーバーフローが発生しますエラーが発生します。

次に、その「出口」を見つける必要があります。つまり、パラメーターが何であるかを見つけ、再帰が終了し、結果を直接返す必要があります。

明らかに、上記の例では、エクスポート用にn == 1、f(1)= 1の場合、以下のコードを改善しましょう。

int f(int n)
{
	if (n == 1)
	{
		return 1;
	}
	return f(n - 1) * n;
} 

この時点で、f(n)関数内のコードは完成しています。

上で述べたように、コードを理解していない場合は、理解しやすいように再帰的な図を描く必要があります。これを例として、図を描いてみましょう。

再帰の3つの要素をよりよく理解するために、いくつかの演習を行います。

ケーキカット思考:再帰的な簡単な演習

配列の合計:

#include<iostream>
int main()
{
   int a[]={1,2,3,4,5,6,7};
   //递归函数sum
   return 0;
}

1.繰り返しを見つける-再帰的

再帰式を見つけるために、元の問題をいくつかのサブ問題に分けます。ケーキを切り、最初に切り取って食べ、残りを次のステップで食べるように、この問題を使用します。最初の要素のみを引き出し、残りは再帰処理に渡されます

int sum(int a[])
{
   return a[0]+sum(a);
}

 終わったね

2.繰り返しの変化量を見つける-パラメータ

この繰り返しのプロセスでは、配列の範囲が絶えず縮小し、配列の開始点が変化し、何かが常に右に向かっていることがわかります。ただし、上記の再帰式には変化する範囲がありません。 、最初から常に再帰でした。これを分析した後、パラメータを追加するために繰り返しの変化量を見つける必要があることがわかります。ここに別のパラメータnを追加して、配列の長さを記録します。

int sum(int a[],int n,int begin)
{
   return a[begin]+sum(a,n,begin+1);
}

3.パラメータ変更の傾向を見つける-デザインのエクスポート

beginが配列の最後の要素のインデックスと等しい場合、直接戻ります。これが出口です。


int sum(int a[],int n,int i)
{
    if (i == n-1)
	{
		return a[i];
	}
	return a[i] + sum(a, n,i+1);
}

これは非常に単純ではありませんか?このトピックは単純であると不満を言う大ボスがいるはずです。最初は、単純から始めて、3つの要素を上手に使いましょう。問題が発生したときも同じルーチンです。言う、別の

文字列を反転します

int main()
{
	string sa = "wewe89r";
	cout << f(sa);
	return 0;
}

1.繰り返しを見つける-再帰的

この質問でも、ケーキカット思考を使って再帰式を見つけることができます。わかりやすくするために、絵を描きます。

int f(string str)
{
 
  return f(str.substr(1))+str.substr(0,1);   //substr(0,1)表示从下标0开始取一个字符形成的串                                    
                                             
                                             //substr(1)表示从下标1开始到结尾形成的串
}

2.繰り返しの変化量を見つける-パラメータ

この質問の文字列の長さは絶えず変化しているので、パラメータとして文字列の長さを使用します。ここでは、substr関数を使用します。新しいパラメータを追加する必要はありませんが、本質は、文字列。

3.パラメータ変更の傾向を見つける-デザインのエクスポート

文字列の長さが1以下の場合、プログラムはもうすぐ終了します。この出口を見つけてください

string  f(string str) {
	int len = str.length();
	if (len <= 1)
		return str;
	return f(str.substr(1)) + str.substr(0, 1); //substr(0,1)表示从下标0开始取一个字符形成的串
}                                               //substr(1)表示从下标1开始到结尾形成的串

さて、この質問はここにあります。ここの誰もが3つの要素をさらに理解していると思います。練習の質問の後、このモデルに従って考えることができます。考えを深めるためにブログに頼るのは現実的ではありません。はい、だからあなたは自分で練習しなければなりません。

これはここにあります、そして私はあなたを以下でさらに研究するために連れて行きます。

最大公約数を解くための再帰式の使用

正の整数aとbの最大公約数は、aとbのすべての公約数の最大公約数を指します。たとえば、4と6の最大公約数は2であり、3と9の最大公約数は3です。 。一般に、gcd(a、b)は、aとbの最大公約数を表すために使用され、ユークリッドアルゴリズムは、最大公約数を解くために一般的に使用されます。

ユークリッドのアルゴリズムは、次の定理に基づいています。

aとbが正の整数であるとすると、gcd(a、b)= gcd(b、a%b);(証明はスキップされます)

上記の定理から、a <bの場合、aとbが交換されるという定理の結果がわかります。a> bの場合、データサイズは常にこの定理によって削減でき、削減は次のようになります。とても早い。これはすぐに結果を得るようであり、知識にはもう1つ必要なことがあります。再帰境界、つまり、結果を計算できるようにデータサイズをどれだけ減らすことができるかです。これは非常に単純です。 0の最大公約数と任意の整数aはaです。この結論は再帰的な境界と見なすことができます。これから、再帰の2つのキーが取得されているため、再帰的な形式で記述することを簡単に考えることができます。

1.再帰式:gcd(a、b)= gcd(b、a%b);

2.再帰的境界:gcd(a、0)=a。

したがって、次のコードを取得します。

int gcd(a,b)
{
if(a==0) return a;
else return gcd(b,a%b);
}

これ以上のことはありません:挿入ソートは再帰的にソートされます

 いくつかのアルゴリズムについては、すでに詳細に紹介しました。忘れた方は、私のブログ「7つの一般的な並べ替えアルゴリズム」をご覧ください。

コードに直接移動します:(または上記の3つの要素に従います)

#include<iostream>
using namespace std;
void InsertSort(int* a, int n)
{
	if (n == 0)
	{
		return;
	}
	InsertSort(a, n - 1);
		int x = a[n];
		int index = n - 1;
		while (index>-1&&x < a[index])
		{
			a[index + 1] = a[index];
			index--;
		}
		a[index+1] = x;
	}
int main()
{
	int a[] = { 1,3,4,2,6,5 };
	InsertSort(a, sizeof(a)/sizeof(int)-1);
	for (auto x : a)
	{
		cout << x << " ";
	}
	return 0;
}

二分探索の再帰的ソリューション

 フルレンジの二分探索は
 、3つのサブ問題に相当します
*左探索(再帰)
*中間比率
*右探索(再帰)

#include<iostream>
using namespace std;
int binarySearch(int* a, int left, int right, int key)
{
	while (left < right)
	{
		int mid = left + ((right - left) >> 1);
		int number = a[mid];
		if (number < key)
		{
			return binarySearch(a, mid + 1, right, key);
		}
		else if (number > key)
		{
			return binarySearch(a, left, mid - 1, key);
		}
		else
			return mid;
	}
}
int main()
{
	int a[] = { 1,2,3,4,5,6,7,8,9,10 };
	int find=binarySearch(a, 0, sizeof(a)/sizeof(int)-1, 5);
	cout << find << endl;
	return 0;
}

マルチブランチ再帰:フィボナッチ数列 

フィボナッチ数列は、f(0)= 1、f(1)= 1、f(n)= f(n-1)+ f(n-2)(n> = 2)の順序を満たします。シーケンスいくつかの項目は、1、1、2、3、5、8 、、、です。再帰的境界はすでに定義からf(0)= 1およびf(1)= 1に精通しているため、再帰的式はf(n)= f(n-1)+ f(n-2)(n > = 2)、したがって、nの階乗を解く方法に従って、n番目の項目のプログラムを記述できます。

int f(int n)
{ 
if(n==1||n==2)
 return 1;
else
 return f(n-1)+f(n-2);
}

また、この方法でコードを書くことは簡潔で理解しやすいものですが、それは非常に非効率的であることも知っています。非効率性はどこにありますか?n = 15と仮定して、再帰ツリーを描画します。 

この再帰ツリーを理解する方法、f(15)を計算する場合は、 f (14)とf (13)を計算する必要があり、 f(14)を計算するには、  f(13)f(12 )を計算する必要があります。 )など。最後に、 f(1)に遭遇すると 、結果がわかり 、結果を直接返すことができ、再帰ツリーが下向きに成長することはなくなります。  f(2)

再帰計算を行う場合、f(13)とf(12)の方程式が繰り返し計算されることは明らかです。計算の数が多いほど、計算の繰り返しが多くなります。これはひどいことなので、最適化する方法を見つける必要があります。それ。

再帰のいくつかの最適化のアイデアについて話す

問題が明らかになると、実際には、問題の半分が解決されています。時間のかかる理由は繰り返し計算するので、「メモ」を作成できます。サブ質問の答えを計算した後、急いで戻ってこないで、まず「メモ」に書き留めてから戻ってください。 ;サブ質問が発生するたびに、「メモ」に移動して確認してください。この問題が以前に解決されていることがわかった場合は、回答を直接使用して、計算に時間を無駄にしないでください。

通常、この「メモ」として配列が使用されますが、もちろんハッシュテーブル(辞書)を使用することもできますが、考え方は同じです。


 int f(int n)
 { 
    if(n ==0||n==1)
    { 
      return 1; 
    } 
     //先判断有没计算过 
     if(arr[n] != -1)
    { 
       // 已经计算过,不用再计算了
       return arr[n];
    }
     else
     {
      // 没有计算过,递归计算,并且把结果保存到 arr数组里 
        arr[n] = f(n-1) + f(n-1); 
        reutrn arr[n]; 
      } 
 }

 これまでのところ、メモ化を使用した再帰的ソリューションは、反復動的計画法ソリューションと同じくらい効率的です。実際、このソリューションは、このソリューションが「トップダウン」「再帰」ソリューションであることを除いて、一般的な動的プログラミングソリューションとほぼ同じです。より一般的な動的プログラミングコードは、「ボトムアップ」「再帰」ソリューションです。プッシュ」して解決します。

トップダウン」とは何ですか?先ほど描いた再帰ツリー(またはグラフ)は、たとえば、より大規模な元の問題から上から下に伸び、 これら2つの基本ケースに 一致 f(20)するまでスケールを徐々に分解し、  次にレイヤーごとに答えを返すことに注意してください。 「トップダウン」と呼ばれます。f(1)f(2)

ボトムアップ」とは何ですか?次に、最も単純で最小の問題サイズ、既知の結果の合計 (基本ケース)f(1) を 下から直接開始し、必要なf(2)答えに到達するまで押し上げ f(20)ます。これは「再帰」の考え方であり、動的計画法が一般的に再帰を取り除く理由でもありますが、ループの反復によって計算を完了します。

自底向上:

public int f(int n)
 {
    if(n == 1||n==2)
     return 1; 
    int f1 = 1;
    int f2 = 2; 
    int sum = 0; 
    for (int i = 3; i <= n; i++)
     { 
     sum = f1 + f2;
      f1 = f2;
       f2 = sum; 
     } 
     return sum; 
}

もちろん、再帰的なます。再帰を最適化するためのプルーニング再帰、およびバックトラック操作です。

トピック戦闘

次の2つの古典的な再帰演習では、手を練習できます

ジャンプステップ

ハノイの塔

最終要約

以上が再帰についての私の理解です。他の人を引き付ける役割を果たすことができるといいのですが。わからないことがあれば、コメント欄にメッセージを残して一緒に話し合ってください。一言で言えば、一言で言えば、私が上で述べた再帰の3つの要素に精通している必要があります。要約と習熟を実践します。

おすすめ

転載: blog.csdn.net/m0_58367586/article/details/123752617