掌握递归编程

时间:2014-03-28 13:09:19

标签: algorithm recursion data-structures recursive-datastructures

我在思考/解决递归问题时遇到了麻烦。我真的很欣赏这个概念,我可以理解它们就像创建基本案例,退出案例和&递归调用等我可以解决简单的问题,如在数组中写入阶乘或整数求和。这就是我的想法停止的地方。当问题变得复杂时,我无法真正应用概念或提出解决方案。例如,河内的塔,虽然我能理解问题和解决方案,但我,我自己无法解决问题。它也适用于其他算法,如快速排序/二叉树遍历。所以我的问题是

  1. 掌握它的最佳方法是什么?
  2. 任何人都可以建议一系列问题或问题,我可以将其作为练习来练习吗?
  3. 学习功能语言会帮助我理解吗?
  4. 请建议。

6 个答案:

答案 0 :(得分:39)

递归只是一种思考方式,就像迭代一样。当我们在学校的孩子时,我们没有被教导递归思考,而且存在真正的问题。你需要将这种思维方式融入你的武器库,一旦你这样做,它将永远留在那里。

掌握最佳方法:

我发现总是首先弄清楚基本情况是有用的,也许起初它们不是最简单的情况,但是一旦你开始在那个基础情况之上构建递归,你就会意识到你可以简化它。识别基本情况的重要性在于,首先,您要关注最简单形式需要解决的问题(更简单的情况),并以某种方式绘制未来算法的路线图,其次,确保算法停止。也许并没有返回预期的结果,但至少会停止,这总是令人鼓舞。

此外,它总是有助于弄清楚问题的一个小实例如何帮助您找到更大问题实例的解决方案。例如,如何为已经输入n解决方案的输入n-1构建解决方案。

解决您可以想到的所有问题。是的,Hanoi Towers是一个非常好的例子,它的递归解决方案是非常聪明的解决方案。尝试更容易的问题,几乎是元素问题。

问题清单

  1. 数学运算:指数和您能想到的每一项数学运算。
  2. 字符串处理:回文是一项非常好的练习。在网格中查找单词也很有用。
  3. 了解树状数据结构:这尤其是IMO最好的培训。树是递归数据结构。了解他们的遍历(按顺序,后序,预订,计算其高度,直径等)。几乎所有关于树状数据结构的操作都是一个很好的练习。
  4. 组合问题:非常重要,组合,排列等。
  5. 寻路:李的算法,迷宫算法等。
  6. 但最重要的是,从简单问题开始。几乎每个问题都有递归解决方案。数学问题很难掌握它。每次看到for循环或while循环时,请将该算法转换为递归。

    编程语言

    函数式编程在很大程度上依赖于递归。我不认为这应该有用,因为它们本身就是递归的,对于那些不太了解递归的用户来说可能很麻烦。

    使用一种简单的编程语言,一种你最熟悉的编程语言,最好是一种不会因记忆烦恼和指针而烦恼的语言。在我看来,Python是一个非常好的开始。非常简单,打字或复杂的数据结构不会让您感到烦恼。只要该语言可以帮助您专注于递归,它就会更好。

    最后一条建议,如果您无法找到问题的解决方案,请在互联网上搜索或致电寻求帮助,完全了解它的作用并转移到另一个。不要让他们绕过你,因为你想要做的是将这种思维方式融入你的脑袋

    主递归,首先需要主递归:)

    希望这有帮助!

答案 1 :(得分:9)

我的建议:相信递归函数"完成工作" ,即满足其规范。并且知道这一点,您可以构建一个功能,在满足规范的同时解决更大的问题。

您如何解决河内塔问题?假设有一个功能Hanoi(N)能够在不违反规则的情况下移动一堆N盘。使用此功能,您可以轻松实现Hanoi'(N + 1):执行河内(N),移动下一个磁盘并再次执行河内(N)。

如果河内(N)有效,那么河内(N + 1)也能正常工作,而不会违反规则。要完成参数,必须确保递归调用已终止。在这种情况下,如果你可以非递归地解决河内(1)(这是微不足道的),你就完成了。

使用这种方法,您不必担心事情会如何实际发生,您可以保证它有效。 (前提是您转移到越来越小的问题实例。)

另一个例子:二叉树的递归遍历。假设函数Visit(root)完成了这项工作。然后,if left -> Visit(left); if right -> Visit(right); print root将完成这项工作!因为第一个调用将打印左子树(不要担心如何),第二个调用将打印正确的子树(不要担心如何),并且根也将打印出来。

在后一种情况下,您可以通过处理越来越小的子树来实现终止。

其他例子:Quicksort。假设您有一个对数组进行就地排序的函数,让Quicksort。您将按如下方式使用它:在大型元素之前移动小元素,就地,通过将​​它们与精心选择的" pivot"进行比较。 value(实际上,数组中的任何值都可以)。然后通过Quicksort函数对大元素进行排序,并以相同的方式对大元素进行排序,您就完成了!无需怀疑将要发生的分区的确切顺序。如果避免使用无效子阵列,则可以确保终止。

最后一个例子,Pascal的三角形。你知道元素是它上面两个元素的总和,边上有1个元素。所以闭着眼睛C(K, N)= 1 if K=0 or K=N, else C(K, N) = C(K-1, N-1) + C(K, N-1)那就是它!

答案 2 :(得分:3)

学习函数式语言肯定会帮助你思考递归。我会推荐Haskell或Lisp(或Clojure)。好的是,在进行递归之前,您不需要访问这两种语言中的“硬位”。要了解递归,你不必必须学习这些语言中的任何一种来进行“真正的”编程。

Haskell的模式匹配语法意味着基本案例很容易看到。在Haskell中,Factorial看起来像这样:

factorial 0 = 1
factorial n = n * factorial (n - 1)

......这完全等同于程序语言:

int factorial(n) {
    if(n==0) {
         return 1;
    } else {
         return n * factorial(n-1)
    }
}

...但是用较少的语法来掩盖这个概念。

为了完整性,这里的Lisp中的算法是相同的:

(defun factorial (n)
   (if (== n 0)
       1
       (* n (factorial (- n 1)))))

你应该能够看到的是等价的,虽然起初所有的括号都会模糊人们对正在发生的事情的看法。仍然,Lisp书将涵盖许多递归技术。

此外,任何关于函数式语言的书都会给你很多递归的例子。您将从使用列表的算法开始:

 addone [] = []
 addone (head:tail) = head + 1 : addone tail

..它使用一个非常常见的模式,每个函数有一个递归调用。 (实际上这种模式非常普遍,几乎所有语言都将其抽象为名为map)的库函数

然后,您将通过对节点中的每个分支进行一次递归调用来继续遍历树的函数。

更一般地说,想一想这样的问题:

  

“我可以解决这个问题的一小部分,并留下同样的问题,只是更小吗?”。

......或......

  

“这个问题是否容易解决,只要其余部分已经解决了?”。

因此,例如,factorial(n)如果您知道factorial(n-1)就很容易解决,这表明了一种递归解决方案。

事实证明,可以通过这种方式考虑很多问题:

  

“对1000个项目的列表进行排序似乎很难,但是如果我选择一个随机数,对所有小于这个数字的数字进行排序,那么对所有大于这个数字的数字进行排序,我已经完成了。” (最终归结为长度为1的排序列表)

...

  

“计算到节点的最短路径很难,但是如果我能够知道每个相邻节点到那里的距离,那就很容易了。”

...

  

“访问此目录树中的每个文件都很难,但我可以用同样的方式查看基本目录和威胁子目录中的文件。”

同样是河内之塔。如果您这样陈述,解决方案很简单:

 To move a stack from a to c:
  If the stack is of size 1
      just move it.
  otherwise
      Move the stack minus its largest ring, to b (n-1 problem)
      Move the largest ring to c (easy)
      Move the stack on b to c (n-1 problem)

我们通过草绘两个显然困难的步骤使问题看起来很容易。但这些步骤又是同样的问题,但“一个更小”。


您可能会发现使用纸张代表调用堆栈手动逐步执行递归算法很有用,如本答案中所述:Understanding stack unwinding in recursion (tree traversal)


在你对递归更加满意之后,回过头来思考它是否是针对特定案例的正确解决方案。虽然factorial()是演示递归概念的好方法,但在大多数语言中,迭代解决方案更有效。了解尾部递归优化,哪些语言具有特色,以及原因。

答案 3 :(得分:3)

递归是实现Divide& amp;的一种便捷方式。征服范式:当你需要解决一个特定的问题时,一个强有力的方法是将其分解为性质相同的问题,而将分解为更小的。通过重复这个过程,您将最终处理的问题非常小,以至于可以通过其他方法轻松解决。

你必须要问自己的问题是“我可以通过解决部分问题来解决这个问题吗?”。当答案是肯定的时,你应用这个众所周知的方案:

  • 递归地将问题分成子问题,直到尺寸很小,

  • 通过直接方法解决子问题,

  • 以相反的顺序合并解决方案。

请注意,分割可以分为两部分或更多部分,这些部分可以平衡或不平衡。

例如:我可以通过执行部分排序来对数字数组进行排序吗?

答案1:是的,如果我将最后一个元素排除并对其余元素进行排序,我可以通过在最右边的位置插入最后一个元素来对整个数组进行排序。这是插入排序。

答案2:是的,如果我找到最大的元素并将其移到最后,我可以通过对剩余元素进行排序来对整个数组进行排序。这是选择排序。

答案3:是的,如果我对数组的两半进行排序,我可以通过合并两个序列对整个数组进行排序,使用辅助数组进行移动。这是合并排序。

答案4:是的,如果我使用数据分区对数组进行分区,我可以通过对这两部分进行排序来对整个数组进行排序。这是快速排序。

在所有这些情况下,您可以通过解决相同性质的子问题并添加一些胶水来解决问题。

答案 4 :(得分:2)

递归很难,因为它是一种不同的思维方式,我们在年轻时从未被介绍过。

从你所说的你已经拥有了你真正需要的概念就是更多地练习它。功能语言肯定会有所帮助;你将被迫以递归的方式思考你的问题,在你知道递归看起来很自然之前

你可以做很多与递归相关的练习,请记住,循环完成的任何事情都可以递归完成。

请参阅此answer以获取有关参考和运动问题的详细信息

答案 5 :(得分:1)

对于复杂问题,我建议针对小问题大小解决问题并查看您找到的模式类型。例如,在河内的塔中,从问题大小开始,然后是两个,然后是三个等等。在某些时候,你可能会开始看到一个模式,你会发现你所拥有的一些要做的就像你在小尺寸问题上所要做的那样,或者它足够相似,你可以使用与以前相同的技术,但有一些变化。

我刚刚经历了河内塔问题并研究了自己的想法。我从第一个问题开始:

   We have one disk on peg A.
   *** Move it to peg C.
   Done!

现在两个人。

   We have two disks on peg A.
   I need to use peg B to get the first disk out of the way.
   *** Move from peg A to peg B
   Now I can do the rest
   *** Move from peg A to peg C
   *** Move from peg B to peg C
   Done!

现在有三个。

事情开始变得有趣了。解决方案并不那么明显。但是,我已经弄清楚如何将两个磁盘从一个挂钩移动到另一个挂钩,所以如果我可以将两个磁盘从挂钉A移动到挂钉B,则将一个磁盘从挂钉A移动到挂钉C,然后将两个磁盘从挂钉B移动要钉住C,我会完成的!    我对两个磁盘的逻辑是有效的,除了钉子不同。如果我们将逻辑放入函数中,并为pegs创建参数,那么我们就可以重用逻辑。

def move2(from_peg,to_peg,other_peg):
   # We have two disks on from_peg
   # We need to use other_peg to get the first disk out of the way
   print 'Move from peg '+from_peg+' to peg '+other_peg
   # Now I can do the rest
   print 'Move from peg '+from_peg+' to peg '+to_peg
   print 'Move from peg '+other_peg+' to peg '+to_peg

逻辑是:

       move2('A','B','C')
       print 'Move from peg A to peg C'
       move2('B','C','A')

我也可以通过使用move1函数来简化:

def move1(from_peg,to_peg):
    print 'Move from '+from_peg+' to '+to_peg

现在我的move2功能可以

def move2(from_peg,to_peg,other_peg):
   # We have two disks on from_peg
   # We need to use other_peg to get the first disk out of the way
   move1(from_peg,other_peg,to_peg)
   # Now I can do the rest
   move1(from_peg,to_peg)
   move1(other_peg,to_peg)

好的,那四个呢?

似乎我可以应用相同的逻辑。我需要从钉子A到钉子B得到三个盘子,然后从A到C得到三个盘子,然后从B到C得到三个盘子。我已经解决了已经移动了三个,但是错误的钉子,所以我会概括它:

def move3(from_peg,to_peg,other_peg):
   move2(from_peg,other_peg,to_peg)
   move1(from_peg,to_peg)
   move2(other_peg,to_peg,from_peg)

酷!等等,move3和move2现在非常相似,这是有道理的。对于任何大小的问题,我们可以将除了一个磁盘之外的所有磁盘移动到挂钩B,然后将一个磁盘从A移动到C,然后将挂钉B上的所有磁盘移动到挂钩C.所以我们的移动功能可以将磁盘数量作为参数:

def move(n,from_peg,to_peg,other_peg):
    move(n-1,from_peg,other_peg,to_peg)
    move1(from_peg,to_peg)
    move(n-1,other_peg,to_peg,from_peg)

这看起来非常接近,但是在n == 1的情况下它不起作用,因为我们最终调用move(0,...)。所以我们需要处理:

def move(n,from_peg,to_peg,other_peg):
    if n==1:
        move1(from_peg,to_peg)
    else:
        move(n-1,from_peg,other_peg,to_peg)
        move1(from_peg,to_peg)
        move(n-1,other_peg,to_peg,from_peg)

出色!五个问题的大小怎么样?我们只需要调用move(5,'A','C','B')。看起来任何问题的大小都是一样的,所以我们的主要功能就是:

def towers(n):
    move(n,'A','C','B')

我们已经完成了!