【问题标题】:Examples of Recursive functions [closed]递归函数的例子
【发布时间】:2010-09-12 17:01:03
【问题描述】:

任何人都可以建议说明递归函数的编程示例吗?有常见的老马,例如 斐波那契数列河内塔,但除此之外的任何东西都会很有趣.

【问题讨论】:

标签: recursion


【解决方案1】:

This illustration 是英文,而不是实际的编程语言,但有助于以非技术方式解释该过程:

一个孩子睡不着,所以她妈妈讲了一个小青蛙的故事, 谁睡不着,所以青蛙的妈妈讲了一个小熊的故事, 谁睡不着,所以熊妈妈讲了一个小黄鼠狼的故事 ……谁睡着了。 ……小熊睡着了; ……小青蛙睡着了; ……孩子睡着了。

【讨论】:

  • 是的,但最终条件/基本情况在哪里? ;-)
  • @Joachim Weasel 始终是基本情况,伙计。 你知道的!
  • 还有,这很漂亮。
  • 您不会经常看到缩进的英文。 :-)
  • 这个答案让我想起了电影《盗梦空间》。
【解决方案2】:

要想understandrecursion,首先要了解recursion

【讨论】:

    【解决方案3】:

    递归的经验法则是,“使用递归,当且仅当您的任务在每次迭代中分成两个或更多类似的任务”。

    所以斐波那契不是递归应用的好例子,而河内是一个很好的例子。

    所以大多数递归的好例子都是在不同的disquises中遍历树。

    例如: 图遍历 - 访问过的节点永远不会被再次访问的要求有效地使图成为树(树是没有简单循环的连通图)

    分治算法(快速排序、归并排序)——“分”之后的部分构成子节点,“征服”构成从父节点到子节点的边。

    【讨论】:

    • 第一句+1
    • 第一句+1
    • 我没听懂第一句话,其余的 +1。
    • 详细说明为什么第一句话如此关键。想象一下相反的情况。如果任务没有分成多个路径,您可以简单地使用迭代。谷歌斐波那契迭代与递归。迭代版本更快,内存效率更高,并且不会溢出堆栈。所以你只使用递归......当你需要/受益足以超越使用递归的额外缺点时。
    【解决方案4】:

    测试一个字符串是否为回文怎么样?

    bool isPalindrome(char* s, int len)
    {
      if(len < 2)
        return TRUE;
      else
        return s[0] == s[len-1] && isPalindrome(&s[1], len-2);
    }
    

    当然,您可以使用循环更有效地做到这一点。

    【讨论】:

    • 为什么要提供一个糟糕的递归示例并就同一件事批评人们?
    • @Kaerber 我没有批评任何人。我在 Yuval 的回答中(以及我自己)说过,它们是最好用循环完成的例子。
    • @Kip @Kaerber,如果我们使用编译时评估,不一定是坏事。 . . . constexpr bool compileTimeIsPalindrome(const char* s, int len) { return len &lt; 2 ? true : s[0] == s[len-1] &amp;&amp; compileTimeIsPalindrome(&amp;s[1], len-2); } ;-)
    【解决方案5】:

    【讨论】:

      【解决方案6】:

      来自数学世界,有the Ackermann function

      Ackermann(m, n)
      {
        if(m==0)
          return n+1;
        else if(m>0 && n==0)
          return Ackermann(m-1, 1);
        else if(m>0 && n>0)
          return Ackermann(m-1, Ackermann(m, n-1));
        else
          throw exception; //not defined for negative m or n
      }
      

      它总是终止,但即使对于非常小的输入,它也会产生非常大的结果。例如,Ackermann(4, 2) 返回 265536 - 3。

      【讨论】:

      • if (m &gt; 0 &amp;&amp; n == 0) 它应该返回Ackermann(m - 1, 1) 而不是Ackermann(m - 1, n)(即Ackermann(m - 1, 0))。至少根据你引用的维基百科。
      • @monkey_05_06 没错。我 5 年前的错字已更正。
      • 嘿,他们比我们强。 ;) 只是为了后代而澄清。
      【解决方案7】:

      另外两个“通常的嫌疑人”是Quicksort 和 MergeSort

      【讨论】:

        【解决方案8】:

        interpreter design pattern 是一个很好的例子,因为很多人没有发现递归。维基百科文章中列出的示例代码很好地说明了如何应用它。但是,仍然实现解释器模式的更基本的方法是用于嵌套列表的 ToString 函数:

        class List {
            public List(params object[] items) {
                foreach (object o in items)
                    this.Add(o);
            }
        
            // Most of the implementation omitted …
            public override string ToString() {
                var ret = new StringBuilder();
                ret.Append("( ");
                foreach (object o in this) {
                    ret.Append(o);
                    ret.Append(" ");
                }
                ret.Append(")");
                return ret.ToString();
            }
        }
        
        var lst = new List(1, 2, new List(3, 4), new List(new List(5), 6), 7);
        Console.WriteLine(lst);
        // yields:
        // ( 1 2 ( 3 4 ) ( ( 5 ) 6 ) 7 )
        

        (是的,我知道如果您希望有一个名为Eval 的函数,那么在上面的代码中发现解释器模式并不容易……但实际上,解释器模式并没有告诉我们这个函数被调用了什么,甚至它是什么确实如此,并且 GoF 书中明确列出了上述模式的示例。)

        【讨论】:

        • 有趣的是,any 迭代可以表示为 foldfold 可以反过来解释(双关语)作为编程语言的参数化解释器,只需两个命令element(e)NIL,其中列表是程序。酷吧?在这里找到解释:Functional Programming for Beginners - Rúnar Óli Bjarnason - Boston Area Scala Enthusiasts.
        • 嵌套列表实际上是一种快速、肮脏和低效的树,难怪递归很适合它们。
        • @Kaerber 我不明白该评论的结论。我也同意它的前提:嵌套列表是树,真的。它们既不快,也不脏,也不低效。它们只是实现通用树的一种方式,这种方式有时(但诚然不是很常见)合适。
        • 抱歉,我还没想好。嵌套列表实际上是树的有效(并且是规范的)实现之一。结论基于两个隐藏的前提:1)递归的唯一良好用途是遍历树,2)树的字符串表示需要遍历它,并且声明一个,3)嵌套列表是树。所以这个论点得出的结论是,是的,事实上,递归对于嵌套列表上的 toString 函数是合适/有效的。
        【解决方案9】:

        在我看来,了解递归是件好事,但大多数可以使用递归的解决方案也可以使用迭代来完成,而且迭代的效率要高得多。

        这里说的是在嵌套树(如 ASP.NET 或 Winforms)中查找控件的递归方式:

        public Control FindControl(Control startControl, string id)
        {
              if (startControl.Id == id)
                   return startControl
        
              if (startControl.Children.Count > 0)
              {
                   foreach (Control c in startControl.Children)
                   {
                        return FindControl(c, id);
                   }
              }
              return null;
        }
        

        【讨论】:

        • “而且迭代效率要高得多”——这种笼统的陈述几乎总是错误的(又一个;-))。使用尾递归,性能与迭代相当。
        • 函数式编程的支持者似乎更喜欢递归,并以尾递归 == 迭代这一事实为借口。在现实世界中,递归是有代价的。
        • 神圣!你是如何从我的开发箱中获取代码的?这几乎完全是逐个字符,甚至是行距,这是我在自己的库中拥有的一个函数。
        • 斯蒂芬,我想我们都不止一次地写过这个函数 :) Konrad,即使是尾递归也会增加一次将函数参数推送到堆栈的成本。
        • Jon,即使是迭代,当封装在一个函数中时,“将函数参数推送到堆栈一次的成本增加了。”所以在那里。此外,还有函数调用内联,它也适用于尾递归函数。
        【解决方案10】:

        这是一个来自文件系统世界的实用示例。此实用程序递归地计算指定目录下的文件。 (我不记得为什么了,但实际上我很久以前就需要这样的东西......)

        public static int countFiles(File f) {
            if (f.isFile()){
                return 1;
            }
        
            // Count children & recurse into subdirs:
            int count = 0;
            File[] files = f.listFiles();
            for (File fileOrDir : files) {
                count += countFiles(fileOrDir);
            }
            return count;
        }
        

        (请注意,在 Java 中,File 实例可以表示普通文件或目录。此实用程序将目录排除在计数之外。)

        一个常见的现实世界示例是FileUtils.deleteDirectory() 来自 Commons IO 库;见API doc & source

        【讨论】:

          【解决方案11】:

          一个真实的例子是“物料清单成本”问题。

          假设我们有一家制造最终产品的制造公司。每个产品都可以通过其部件列表和组装这些部件所需的时间来描述。例如,我们用外壳、电机、卡盘、开关和电线制造手持电钻,需要 5 分钟。

          给定每分钟的标准人工成本,制造我们每件产品的成本是多少?

          哦,顺便说一句,有些零件(例如电源线)是购买的,所以我们直接知道它们的成本。

          但实际上我们自己制造了一些零件。我们用一个外壳、一个定子、一个转子、一个轴和轴承制作了一个电机,这需要 15 分钟。

          我们用冲压件和电线制造定子和转子,...

          因此,确定成品的成本实际上相当于遍历表示我们流程中所有整体到零件列表关系的树。用递归算法很好地表达了这一点。当然也可以迭代完成,但核心思想与自己动手记账混在一起,所以不清楚发生了什么。

          【讨论】:

            【解决方案12】:

            我知道的最毛茸茸的例子是 Knuth 的 Man or Boy Test。 除了递归之外,它还使用嵌套函数定义(闭包)、函数引用和常量/函数二元论(按名称调用)的 Algol 特性。

            【讨论】:

              【解决方案13】:

              正如其他人已经说过的,很多规范递归示例都是学术性的。

              我过去遇到的一些实际用途是:

              1 - 导航树结构,例如文件系统或注册表

              2 - 操作可能包含其他容器控件(如 Panels 或 GroupBoxes)的容器控件

              【讨论】:

              • 2其实是树形结构,所以是1 :)
              【解决方案14】:

              我个人最喜欢的是Binary Search

              编辑:另外,树遍历。例如,遍历文件夹文件结构。

              【讨论】:

              • 二分查找不是递归的好例子,它作为循环的实现要高效得多。
              • 这是递归的一个很好的例子因为它很容易与循环相比。任何学习递归编程的人都可以看到这两种算法之间的关系。如果使用尾递归正确实现二进制搜索,编译器可以使它们像编写循环一样高效。
              【解决方案15】:

              Guido van Rossum 的Implementing Graphs 在 Python 中有一些递归函数来查找图中两个节点之间的路径。

              【讨论】:

                【解决方案16】:

                我最喜欢的类型,Merge Sort

                (最喜欢的,因为我记得算法并且在性能方面还不算太差)

                【讨论】:

                • 合并排序很棒 :) ...也是我真正记得的少数算法之一!
                【解决方案17】:
                • 阶乘
                • 深入遍历树(在文件系统、游戏空间或任何其他情况下)

                【讨论】:

                • 实际上,阶乘通常使用循环更好。这是一个经常使用的愚蠢示例
                • 它可能在循环中表现更好,但这是一个很好理解的想法,很容易递归表达,尤其是在具有部分函数的语言中。
                • 这么多常见的例子实际上都是类似循环的,仅仅是因为使用函数式语言的人似乎更喜欢递归。在“普通”编程语言中,为了性能和代码可读性,大多数事情都最好作为循环来完成。
                • 我认为,以这种方式教授递归更容易,因为递归的最佳用例 - 分而治之的算法 - 比递归本身复杂得多。
                【解决方案18】:

                反转一个字符串怎么样?

                void rev(string s) {
                  if (!s.empty()) {
                    rev(s[1..s.length]);
                  }
                  print(s[0]);
                }
                

                理解这一点有助于理解递归。

                【讨论】:

                • 像阶乘一样,使用循环更容易反转字符串。
                • 当然可以,但是这个问题需要例子来说明递归,这个例子增加了其他例子中没有的东西。
                • 好吧,正如他们所说,如果你不能成为一个好榜样,那就是一个可怕的警告。
                【解决方案19】:

                processing lists 怎么样,比如:

                • map(和andmap、ormap)
                • 折叠(折叠,折叠)
                • 过滤器
                • 等等……

                【讨论】:

                • 不是递归的好例子。
                【解决方案20】:

                曾几何时,不久前,小学生使用 Logo 和 Turtle Graphics 学习递归。 http://en.wikipedia.org/wiki/Turtle_graphics

                递归也非常适合通过详尽的试验来解决难题。有一种叫做“填充”(Google it)的谜题,你会得到一个像填字游戏一样的网格,还有单词,但没有线索,没有编号的方块。我曾经为一个谜题发布者编写了一个使用递归的程序来解决谜题,以确保已知的解决方案是唯一的。

                【讨论】:

                  【解决方案21】:

                  递归函数非常适合使用recursively defined datatypes

                  • 一个自然数为零或另一个自然数的后继
                  • 列表是空列表或前面有元素的另一个列表
                  • 树是具有一些数据和零个或多个其他子树的节点

                  等等

                  【讨论】:

                  • 唯一值得递归的是一棵树。其他数据类型也可以归纳定义而不是递归定义(即空列表是列表,具有一个元素的列表是列表,如果具有 n-1 个元素的列表是列表,则具有 n 个元素的列表是列表),其中表明循环会更有效率。
                  【解决方案22】:

                  将电子表格列索引转换为列名。

                  这比听起来要复杂,因为电子表格列不能正确处理“0”数字。例如,如果在从 Z 递增到 AA 时将 A-Z 作为数字,这就像从 9 到 11 或从 9 到 00 而不是 10(取决于 A 是 1 还是 0)。即使是Microsoft Support example 也会因为任何高于 AAA 的东西而出错!

                  递归解决方案有效,因为您可以在那些新数字边界上进行递归。此实现在 VB.Net 中,并将第一列 ('A') 视为索引 1。

                  Function ColumnName(ByVal index As Integer) As String
                      Static chars() As Char = {"A"c, "B"c, "C"c, "D"c, "E"c, "F"c, "G"c, "H"c, "I"c, "J"c, "K"c, "L"c, "M"c, "N"c, "O"c, "P"c, "Q"c, "R"c, "S"c, "T"c, "U"c, "V"c, "W"c, "X"c, "Y"c, "Z"c}
                  
                      index -= 1 'adjust index so it matches 0-indexed array rather than 1-indexed column'
                  
                      Dim quotient As Integer = index \ 26 'normal / operator rounds. \ does integer division'
                      If quotient > 0 Then
                          Return ColumnName(quotient) & chars(index Mod 26)
                      Else
                          Return chars(index Mod 26)
                      End If
                  End Function
                  

                  【讨论】:

                  • 它可以像循环一样高效(或更高)。
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-03-15
                  • 2018-12-29
                  • 2021-12-12
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多