我在递归方面遇到了问题,无法思考或解决问题。我非常赞赏这一概念,能够理解创建基本情况、退出情况和递归调用等内容。我可以解决简单的问题,比如编写阶乘或整数数组求和。这就是我的思维停止的地方。当问题变得复杂时,我无法真正应用这些概念或想出解决方案。例如,汉诺塔问题,虽然我可以理解问题和解决方案,但我自己无法想出解决方案。其他算法,如快速排序/二叉树遍历也都是如此。所以我的问题是:
- 掌握它的最佳方法是什么?
- 有人能推荐一系列问题或题目,供我练习吗?
- 学习函数式语言会帮助我理解吗?
请给予建议。
我在递归方面遇到了问题,无法思考或解决问题。我非常赞赏这一概念,能够理解创建基本情况、退出情况和递归调用等内容。我可以解决简单的问题,比如编写阶乘或整数数组求和。这就是我的思维停止的地方。当问题变得复杂时,我无法真正应用这些概念或想出解决方案。例如,汉诺塔问题,虽然我可以理解问题和解决方案,但我自己无法想出解决方案。其他算法,如快速排序/二叉树遍历也都是如此。所以我的问题是:
请给予建议。
递归只是一种思维方式,就像迭代一样。当我们还在学校时,我们没有被教授如何递归地思考,这就是真正的问题所在。你需要将这种思维方式纳入你的工具库中,一旦你做到了,它就会永远留在那里。
我发现首先找出基本情况非常有用,也许起初它们并不是最简单的,但一旦你开始在这个基本情况上构建递归,你会意识到你可以简化它。确定基本情况的重要性在于,首先,你专注于以最简单的形式解决问题(较简单的情况),这在某种程度上为未来的算法提供了路线图;其次,你确保算法停止。也许不返回期望的结果,但至少停止,这总是令人鼓舞的。
此外,弄清楚一个问题的小实例如何帮助你找到更大实例的解决方案,总是很有帮助。例如,你如何在已经有输入 n-1
的情况下构建输入 n
的解决方案。
尝试用递归解决你能想到的每个问题。是的,汉诺塔不是一个很好的例子,它的递归解决方案是一种非常聪明的解决方案。试试更简单的问题,几乎是最基本的问题。
但最重要的是,从简单问题开始。几乎每个问题都有一个递归解决方案。数学问题非常适合掌握此技巧。每次看到 for
循环或 while
循环时,请将该算法转换为递归。
函数式编程非常依赖于递归。我认为这可能不会对不太了解递归的用户有太大帮助,因为它们本质上是递归的,并且可能很繁琐。
使用一种简单的编程语言,最好是你最熟悉的一种,最好是那种不会让你在内存麻烦和指针方面感到困扰的语言。在我看来,Python 是一个非常好的开始。它非常简单,不会让你感到烦恼,也不会让你感到类型或复杂数据结构的麻烦。只要这种语言帮助您专注于递归,它就会更好。
最后一个建议是,如果您找不到问题的解决方案,请在互联网上搜索或寻求帮助,完全理解它的作用,然后转移到其他问题。不要让它们绕过你,因为你正在尝试将那种思考方式加入你的脑中。
要 掌握递归,首先需要 掌握递归 :)
希望这有所帮助!
call/cc
和闭包)应用到我的Algol方言中。我计划教授Scheme作为我女儿的第一门语言,以检查它的效果如何。 - Sylwester我的建议是:相信递归函数“能够完成任务”,即满足其规范。有了这个认识,您可以构建一个函数来解决更大的问题,同时仍然满足规范。
如何解决汉诺塔问题?假设有一个名为Hanoi(N)的函数,能够在不违反规则的情况下移动N个盘子的堆。使用此函数,您可以轻松实现Hanoi'(N+1):执行Hanoi(N),移动下一个盘子,再次执行Hanoi(N)。
如果Hanoi(N)有效,则Hanoi'(N+1)也有效,而不会违反规则。为了完成论证,您必须确保递归调用终止。在这种情况下,如果您可以非递归地解决Hanoi(1)(这是微不足道的),那么您就完成了。
使用这种方法,您不必担心事情实际上会发生什么,您保证它有效。(前提是您转移到问题的较小和较小的实例。)
另一个例子:二叉树的递归遍历。假设函数Visit(root)
完成了任务。那么,if left -> Visit(left); if right -> Visit(right); print root
就可以完成任务!因为第一次调用将打印左子树(不必担心如何),第二次调用将打印右子树(不必担心如何),并且根也将被打印。C(K, N)= 1 if K=0 or K=N, else C(K, N) = C(K-1, N-1) + C(K, N-1)
。就是这样!
递归很难,因为它是一种不同的思考方式,当我们年幼时从未接触过。
根据你的说法,你已经掌握了概念,只需要多练习。使用函数式语言一定会有帮助;你会被迫递归地思考问题,在你意识之前,递归就会变得非常自然。
有很多与递归相关的练习可以做,记住任何使用循环完成的事情也可以用递归来完成。
请参见此答案,其中有关于引用和练习问题的很好的细节说明。
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)))))
addone [] = []
addone (head:tail) = head + 1 : addone tail
这里讲的是一种常见的模式,每个函数只有一个递归调用。(实际上这个模式非常常见,几乎所有语言都把它封装成叫做 map
的库函数)
然后你将会学习遍历树形结构的函数,通过为每个节点的每个分支进行一次递归调用。
更一般地说,可以像这样思考问题:
“我能解决这个问题的一小部分,让自己只剩下相同的问题吗?”
或者...
“如果问题的其余部分已经被解决,那么这个问题是否容易解决?”
例如,如果知道factorial(n-1)
,就可以简单地解决factorial(n)
,这表明递归是一个可行的解决方法。
事实证明,很多问题都可以用这种方式思考:
“对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)
factorial()
是演示递归概念的好方法,但在大多数语言中,迭代解决方案更有效。了解尾递归优化,哪些语言具有这种功能以及原因。recur
,它不会导致堆栈溢出。Common Lisp没有尾递归优化,因此依赖于loop
,而Scheme即使对于相互(尾)递归调用也保证进行尾递归优化。 - Sylwesterdef 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 '从柱子 A 移到柱子 C' move2('B','C','A')
我可以通过另外一个函数 move1 来使这段代码更简便:
def move1(from_peg,to_peg):
print 'Move from '+from_peg+' to '+to_peg
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)
太好了!如果问题的规模为5怎么办?我们只需要调用move(5,'A','C','B')。看起来任何问题的规模都是一样的,因此我们的主函数只需:
def towers(n):
move(n,'A','C','B')
我们完成了!
10!= 1 * 10!= 10 * 9!
,因此问题和解决方案都采用a * b!
的形式。大多数人发现抽象很难。但实际上,这就是尝试在不同情况下找到相似之处。您甚至可以在日常生活中练习这一点(尝试看到两个不同概念的相似之处)。 - Vincent van der Weele