【问题标题】:Understanding Recursion to generate permutations了解递归以生成排列
【发布时间】:2011-09-24 08:00:40
【问题描述】:

我发现递归,除了像阶乘这样非常直接的递归,很难理解。以下 sn-p 打印字符串的所有排列。谁能帮我理解一下。正确理解递归的方法是什么。

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

【问题讨论】:

  • 尝试在纸上画出草图,或者您也可以尝试在调试器中单步执行代码。
  • 第一件事是递归有时只会产生优雅、直观的解决方案。有时解决方案很优雅,但一点也不直观,我相信它就在这里。有时它既不优雅,也不直观。会不会有一些不雅但直观的东西?我不知道。在这种情况下,从概念上讲,您首先需要了解的是如何通过交换数组中的各种元素对来创建所有排列。然后你需要了解如何应用递归算法来实现这个概念。它可以帮助在每一步在纸上画出递归树。
  • 这是 详尽的递归,正如 Sazzad Hissain Khan 在下面的回答中所说,它是 回溯 的核心,参见 see.stanford.edu/materials/icspacs106b/… 中的第 2 页回溯用于约束传播问题

标签: c++ recursion


【解决方案1】:

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)"];
}

【讨论】:

  • 将调用堆栈可视化为树会很有帮助。还值得注意的是,当您进行递归调用时,您将向下推进树的一个单独的分支,并且在“for”或“while”循环的每次迭代中都会添加一个额外的分支。关于这个问题的一个令人困惑的事情是递归调用 permute 之后的第二次交换。这可以解释为“取消交换”,并且是必需的,因为 char 数组是通过引用而不是按值传递的,并且每次交换数组中的元素时,更改都是在下游可见的。
【解决方案2】:

为了在设计中有效地使用递归,你通过假设你已经解决了问题来解决问题。 当前问题的心理跳板是“如果我可以计算 n-1 个字符的排列,那么我可以通过依次选择每个字符并附加剩余 n-1 个字符的排列来计算 n 个字符的排列,我我在假装我已经知道该怎么做”。

然后您需要一种方法来执行所谓的“自底向上”递归。由于每个新的子问题都比上一个小,也许你最终会遇到一个你真正知道如何解决的子问题。

在这种情况下,您已经知道 ONE 字符的所有排列 - 它只是字符。因此,您知道如何解决 n=1 以及每个比您可以解决的数字多一的数字,您就完成了。这与数学归纳法密切相关。

【讨论】:

    【解决方案3】:

    它从剩下的所有可能的字符中选择每个字符:

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

    【讨论】:

    • @quasiverse 你在代码中的 cmets 对我真的很有帮助,谢谢 :)
    【解决方案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;
    }
    

    参考:Geeksforgeeks.org

    【讨论】:

      【解决方案5】:

      虽然这是一个很小的老问题,并且已经回答了添加我的输入以帮助新访问者的想法。还计划在不关注递归协调的情况下解释运行时间。

      我用 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 调用 Permutations 方法。所以用索引 0 调用,这是第一次调用。
      2. 在 for 循环的 else 部分,我们从 0 到 2 重复,每次调用 1 次。
      3. 在每个循环下,我们使用 LpCnt + 1 递归调用。 4.1 当索引为 1 时,则 2 次递归调用。 4.2 当 index 为 2 时,递归调用 1 次。

      因此,从第 2 点到第 4.2 点,每个循环的总调用次数为 5,总计为 15 次调用 + 主入口调用 = 16。 每次 loopCnt 为 3 然后 if 条件被执行。

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

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

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

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

      Download the sample code and other samples from here

      【讨论】:

        【解决方案6】:

        将递归视为简单的多个级别。在每个级别,您都在运行一段代码,在这里您在每个级别运行 n-i 次 for 循环。这个窗口在每个级别都在减小。 n-i 次,n-(i+1) 次,n-(i+2) 次,..2,1,0 次。

        关于字符串操作和排列,将字符串视为简单的字符“集合”。 "abcd" 为 {'a', 'b', 'c', 'd'}。排列正在以所有可能的方式重新排列这 4 个项目。或者以不同的方式从这 4 个项目中选择 4 个项目。在排列中,顺序确实很重要。 abcd 与 abd 不同。我们必须同时生成。

        您提供的递归代码正是这样做的。在“abcd”上方的字符串中,您的递归代码运行 4 次迭代(级别)。在第一次迭代中,您有 4 个元素可供选择。第二次迭代,您有 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} 中选择下一个。这样我们就可以生成给定字符串的所有可能排列。

        我们正在执行此设置操作,以便我们不会两次选择相同的字符。即 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 次循环。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-11-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-05-05
          相关资源
          最近更新 更多