我发现递归,除了非常直接的因素,如阶乘,很难理解。以下代码段打印字符串的所有排列。任何人都可以帮助我理解它。什么是正确理解递归的方法。
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;
}
答案 0 :(得分:53)
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)"];
}
答案 1 :(得分:24)
它从剩下的所有可能字符中选择每个字符:
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]
}
}
}
答案 2 :(得分:17)
要在设计中有效地使用递归,通过假设已经解决了来解决问题。 当前问题的心理跳板是“如果我可以计算n-1个字符的排列,那么我可以通过依次选择每个字符并附加剩余n-1个字符的排列来计算n个字符的排列,我假装我已经知道该怎么做了。
然后,您需要一种方法来执行所谓的“触底反弹”递归。由于每个新的子问题都小于最后一个问题,或许你最终会遇到一个你真正知道如何解决的子问题。
在这种情况下,你已经知道一个角色的所有排列 - 它只是角色。因此,您知道如何解决n = 1的问题,并且对于每个数字而言,只需要一个数字就可以解决它,并且您已经完成了。这与数学归纳法非常密切相关。
答案 3 :(得分:4)
此代码和参考可能有助于您理解它。
// 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;
}
答案 4 :(得分:3)
虽然这是一个古老的问题,但已经回答了添加我的输入以帮助新访问者的想法。还计划解释运行时间,而不关注递归调节。
我已经用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次因子值,即输入“ABC”长度。
如果语句的for循环重复'n'次以显示来自示例“ABC”的字符,即3。 我们输入的总共6次(因子时间)是否显示排列。 所以总运行时间= n X n!。
我已经给出了一些静态的CallCnt变量和表来详细了解每一行的执行情况。
专家,如果我的任何细节不明确或不正确,请随时编辑我的回答或评论,我很乐意纠正他们。
答案 5 :(得分:2)
仅将递归视为多个级别。在每个级别上,您正在运行一段代码,在这里,您在每个级别上n-i次运行for循环。该窗口在每个级别上都在减小。 n-i次,n-(i + 1)次,n-(i + 2)次,.. 2,1,0次。
关于字符串操作和排列,请将字符串视为简单的字符“集合”。 “ abcd”为{'a','b','c','d'}。排列正在以所有可能的方式重新排列这4个项目。或者以不同的方式从这4个项目中选择4个项目。在排列中,顺序很重要。 abcd与acbd不同。我们必须同时生成两者。
您提供的递归代码完全可以做到这一点。在“ abcd”上方的字符串中,您的递归代码运行4次迭代(级别)。在第一个迭代中,您有4个元素可供选择。第二次迭代,您有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”,而在第二次迭代中选择了“ c”,则这里有{b,d}可以玩。
重复此过程,直到我们总共选择4个字符。一旦选择了4个可能的字符,我们将打印字符。然后回溯并从可能的集合中选择其他字符。即,当回溯到第三次迭代时,我们从可能的集合{b
,d}中选择next。这样,我们就可以生成给定字符串的所有可能排列。
我们正在执行此设置操作,因此我们不会两次选择相同的字符。即abcc,abbc,abbd,bbbb无效。
您代码中的swap语句将执行此设置构造。它将字符串分成两组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次。