再帰は、階乗のような非常に単純なものを除いて、理解するのが非常に難しいと思います。次のスニペットは、文字列のすべての順列を出力します。誰でも私がそれを理解するのを助けることができます。再帰を適切に理解する方法は何ですか。
void permute(char a[], int i, int n)
{
int j;
if (i == n)
cout << a << endl;
else
{
for (j = i; j <= n; j++)
{
swap(a[i], a[j]);
permute(a, i+1, n);
swap(a[i], a[j]);
}
}
}
int main()
{
char a[] = "ABCD";
permute(a, 0, 3);
getchar();
return 0;
}
PaulRには正しい提案があります。理解するまで、「手」でコードを実行する必要があります(必要なツール-デバッガー、ペーパー、特定の時点での関数呼び出しと変数の記録)。コードの説明については、quasiverseの優れた答えを紹介します。
おそらく、コールグラフをわずかに小さい文字列で視覚化することで、その仕組みがより明確になります。
グラフは graphviz で作成されました。
// x.dot
// dot x.dot -Tpng -o x.png
digraph x {
rankdir=LR
size="16,10"
node [label="permute(\"ABC\", 0, 2)"] n0;
node [label="permute(\"ABC\", 1, 2)"] n1;
node [label="permute(\"ABC\", 2, 2)"] n2;
node [label="permute(\"ACB\", 2, 2)"] n3;
node [label="permute(\"BAC\", 1, 2)"] n4;
node [label="permute(\"BAC\", 2, 2)"] n5;
node [label="permute(\"BCA\", 2, 2)"] n6;
node [label="permute(\"CBA\", 1, 2)"] n7;
node [label="permute(\"CBA\", 2, 2)"] n8;
node [label="permute(\"CAB\", 2, 2)"] n9;
n0 -> n1 [label="swap(0, 0)"];
n0 -> n4 [label="swap(0, 1)"];
n0 -> n7 [label="swap(0, 2)"];
n1 -> n2 [label="swap(1, 1)"];
n1 -> n3 [label="swap(1, 2)"];
n4 -> n5 [label="swap(1, 1)"];
n4 -> n6 [label="swap(1, 2)"];
n7 -> n8 [label="swap(1, 1)"];
n7 -> n9 [label="swap(1, 2)"];
}
残りのすべての可能な文字から各文字を選択します。
void permute(char a[], int i, int n)
{
int j;
if (i == n) // If we've chosen all the characters then:
cout << a << endl; // we're done, so output it
else
{
for (j = i; j <= n; j++) // Otherwise, we've chosen characters a[0] to a[j-1]
{ // so let's try all possible characters for a[j]
swap(a[i], a[j]); // Choose which one out of a[j] to a[n] you will choose
permute(a, i+1, n); // Choose the remaining letters
swap(a[i], a[j]); // Undo the previous swap so we can choose the next possibility for a[j]
}
}
}
デザインで再帰を効果的に使用するには、既に解決済みであると仮定して問題を解決します。現在の問題のメンタルスプリングボードは、「n-1文字の順列を計算できた場合、それぞれを順番に選択して残りのn-1文字の順列を追加することにより、n文字の順列を計算できる「私はすでに方法を知っているふりをしている」。
次に、再帰を「ボトムアウト」する方法を実行する方法が必要です。それぞれの新しいサブ問題は最後のサブ問題よりも小さいため、おそらく最終的には、解決方法を本当に知っているサブ問題に到達するでしょう。
この場合、1人のキャラクターのすべての順列をすでに知っています。それは単なるキャラクターです。したがって、n = 1の場合と、それを解決できる数よりも1つ多いすべての数について、それを解決する方法を知っているので、完了です。これは、数学的帰納と呼ばれるものと非常に密接に関連しています。
このコードとリファレンスは、あなたがそれを理解するのに役立つかもしれません。
// C program to print all permutations with duplicates allowed
#include <stdio.h>
#include <string.h>
/* Function to swap values at two pointers */
void swap(char *x, char *y)
{
char temp;
temp = *x;
*x = *y;
*y = temp;
}
/* Function to print permutations of string
This function takes three parameters:
1. String
2. Starting index of the string
3. Ending index of the string. */
void permute(char *a, int l, int r)
{
int i;
if (l == r)
printf("%s\n", a);
else
{
for (i = l; i <= r; i++)
{
swap((a+l), (a+i));
permute(a, l+1, r);
swap((a+l), (a+i)); //backtrack
}
}
}
/* Driver program to test above functions */
int main()
{
char str[] = "ABC";
int n = strlen(str);
permute(str, 0, n-1);
return 0;
}
リファレンス: Geeksforgeeks.org
それは少し古い質問ですが、新しい訪問者を助けるために私の入力を追加するという考えにすでに答えました。また、再帰的調整に焦点を当てずに実行時間を説明することを計画しています。
サンプルはC#で作成しましたが、ほとんどのプログラマーにとって理解しやすいものです。
static int noOfFunctionCalls = 0;
static int noOfCharDisplayCalls = 0;
static int noOfBaseCaseCalls = 0;
static int noOfRecursiveCaseCalls = 0;
static int noOfSwapCalls = 0;
static int noOfForLoopCalls = 0;
static string Permute(char[] elementsList, int currentIndex)
{
++noOfFunctionCalls;
if (currentIndex == elementsList.Length)
{
++noOfBaseCaseCalls;
foreach (char element in elementsList)
{
++noOfCharDisplayCalls;
strBldr.Append(" " + element);
}
strBldr.AppendLine("");
}
else
{
++noOfRecursiveCaseCalls;
for (int lpIndex = currentIndex; lpIndex < elementsList.Length; lpIndex++)
{
++noOfForLoopCalls;
if (lpIndex != currentIndex)
{
++noOfSwapCalls;
Swap(ref elementsList[currentIndex], ref elementsList[lpIndex]);
}
Permute(elementsList, (currentIndex + 1));
if (lpIndex != currentIndex)
{
Swap(ref elementsList[currentIndex], ref elementsList[lpIndex]);
}
}
}
return strBldr.ToString();
}
static void Swap(ref char Char1, ref char Char2)
{
char tempElement = Char1;
Char1 = Char2;
Char2 = tempElement;
}
public static void StringPermutationsTest()
{
strBldr = new StringBuilder();
Debug.Flush();
noOfFunctionCalls = 0;
noOfCharDisplayCalls = 0;
noOfBaseCaseCalls = 0;
noOfRecursiveCaseCalls = 0;
noOfSwapCalls = 0;
noOfForLoopCalls = 0;
//string resultString = Permute("A".ToCharArray(), 0);
//string resultString = Permute("AB".ToCharArray(), 0);
string resultString = Permute("ABC".ToCharArray(), 0);
//string resultString = Permute("ABCD".ToCharArray(), 0);
//string resultString = Permute("ABCDE".ToCharArray(), 0);
resultString += "\nNo of Function Calls : " + noOfFunctionCalls;
resultString += "\nNo of Base Case Calls : " + noOfBaseCaseCalls;
resultString += "\nNo of General Case Calls : " + noOfRecursiveCaseCalls;
resultString += "\nNo of For Loop Calls : " + noOfForLoopCalls;
resultString += "\nNo of Char Display Calls : " + noOfCharDisplayCalls;
resultString += "\nNo of Swap Calls : " + noOfSwapCalls;
Debug.WriteLine(resultString);
MessageBox.Show(resultString);
}
手順:入力を「ABC」として渡すとき。
したがって、ポイント2から4.2の合計呼び出しは各ループで5であり、合計は15呼び出し+メインエントリ呼び出し= 16です。loopCntが3になるたびに条件が実行されます。
この図から、ループカウントが合計3倍になる3になることがわかります。つまり、3の階乗値、つまり「ABC」の長さを入力します。
If文のforループが「n」回繰り返されて、例「ABC」の文字が表示されます。つまり、合計6回(Factorial回)、順列を表示するかどうかを入力します。したがって、合計実行時間= n X n!。
各行の実行を詳細に理解するために、静的なCallCnt変数とテーブルをいくつか示しました。
専門家は、私の詳細のいずれかが明確または間違っていない場合、私の回答またはコメントを編集してください、私はそれらを修正させていただきます。
再帰を単にいくつかのレベルと考えてください。各レベルでコードを実行しています。ここでは、各レベルでforループをn-i回実行しています。このウィンドウは各レベルで減少します。 n-i回、n-(i + 1)回、n-(i + 2)回、.. 2,1,0回。
文字列の操作と置換に関しては、文字列を単なる「セット」の文字と考えてください。 「abcd」として{'a'、 'b'、 'c'、 'd'}。順列は、これら4つの項目をすべての可能な方法で再配置しています。または、これら4つのアイテムから4つのアイテムをさまざまな方法で選択します。順列では順序が重要です。 abcdはacbdとは異なります。両方を生成する必要があります。
あなたが提供する再帰的なコードはまさにそれを行います。 「abcd」の上の私の文字列では、再帰的なコードは4回の反復(レベル)を実行します。最初の反復では、4つの要素から選択できます。 2番目の反復では、3つの要素から選択し、3番目の2つの要素などを選択します。コードは4で実行されます!計算。以下に説明します
First iteration
:{a、b、c、d}から文字を選択します
Second Iteration
:減算セット{{a、b、c、d}-{x}}から文字を選択します。xは最初の反復から選択された文字です。つまり、最初の反復で「a」が選択されている場合、この反復には{b、c、d}から選択できます。
Third Iteration
:減算セット{{a、b、c、d}-{x、y}}から文字を選択します。xおよびyは前の反復から選択された文字です。つまり、最初の反復で「a」が選択され、2番目から「c」が選択された場合、ここで操作する{b、d}があります。
これは、全体で4文字を選択するまで繰り返されます。 4つの可能な文字を選択したら、その文字を出力します。次に、バックトラックして、可能なセットから別の文字を選択します。つまり、3番目の反復に戻るときに、可能なセット{b
、d}からnextを選択します。このようにして、指定された文字列のすべての可能な順列を生成しています。
同じ文字を2回選択しないように、このセット操作を行っています。つまり、abcc、abbc、abbb、bbbbは無効です。
コード内のスワップステートメントは、このセットの構築を行います。文字列を2つのセットfree set
に分割して、既に使用されているused set
から選択します。 i+1
の左側のすべての文字はused set
で、右側はfree set
です。最初の反復では、{a、b、c、d}の中から選択し、{a}:{b、c、d}を次の反復に渡します。次の反復では{b、c、d}のいずれかを選択し、{a、b}:{c、d}を次の反復に渡します。コントロールがこの繰り返しに戻ったときに、c
を選択し、スワッピングを使用して{a、c}、{b、d}を構築します。
それがコンセプトです。それ以外の場合、ここでは再帰は深さnで実行され、各レベルはn、n-1、n-2、n-3 ... 2,1回ループを実行します。