了解递归以生成排列

时间:2011-09-24 08:00:41

标签: c++ recursion

我发现递归,除了非常直接的因素,如阶乘,很难理解。以下代码段打印字符串的所有排列。任何人都可以帮助我理解它。什么是正确理解递归的方法。

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;
}

6 个答案:

答案 0 :(得分:53)

PaulR有正确的建议。你必须通过“手”(使用你想要的任何工具 - 调试器,纸张,记录函数调用和某些点的变量)来运行代码,直到你理解它为止。有关代码的解释,我将向您推荐quasiverse的优秀答案。

也许这个字符串略小的调用图的可视化使其更加明显: Call graph

该图表是使用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)

enter image description here

此代码和参考可能有助于您理解它。

// 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

答案 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”时。

  1. 第一次从Main调用的排列方法。所以用索引0调用,这是第一次调用。
  2. 在for循环的else部分中,我们重复从0到2,每次进行1次调用。
  3. 在每个循环下,我们以LpCnt + 1递归调用。 4.1当index为1时,则进行2次递归调用。 4.2当index为2然后是1个递归调用。
  4. 因此,从第2点到第4.2点,每个循环的总呼叫数为5,总共15个呼叫+主要呼入呼叫= 16。 每次loopCnt为3,然后条件执行。

    从图中我们可以看到循环计数总共变为3次,即3次因子值,即输入“ABC”长度。

    如果语句的for循环重复'n'次以显示来自示例“ABC”的字符,即3。 我们输入的总共6次(因子时间)是否显示排列。 所以总运行时间= n X n!。

    我已经给出了一些静态的CallCnt变量和表来详细了解每一行的执行情况。

    专家,如果我的任何细节不明确或不正确,请随时编辑我的回答或评论,我很乐意纠正他们。

    enter image description here enter image description here

    Download the sample code and other samples from here

答案 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次。