递归:计算思维的核心
引言
人类对这个世界的认识是从特例到普遍,从具体到抽象,从简单到复 杂的,是一个递推
(Iterative
)的过程。这种人类固有的认知与思维方式令我们可以轻易的理解具体的事物,但同时却限制了我们的抽象能力
和大局观
。
而计算机执行任务的方式(可以称之为“计算思维”
)与人类正好相反,是自顶向下,由全局到局部的,往往是一个递归
(Recursive
)的过程,是人为的进行了抽象设计的。递归是计算思维中的核心概念,通过递归,将复杂的计算问题分解为更小的子问题,然后计算机从全局进入到局部,一步步迭代执行简化的子问题,最终求得整个问题的解。
可见,若将人类普遍的思维方式当做正向的思维,那么计算机的“处事方式”则往往采取了“逆向思维”!
递归思想
递归是一种强大且优雅的解决问题的方法。它以简洁的方式处理复杂问题,尤其适用于具有重复性质的问题。
试想一下求解 的方式,如果按照正向思维,我们可以通过 来求解。其 Python 代码如下:
def factorial(n):
result = 1
for i in range(1, n+1):
result *= i
return result
而如果使用递归思想,我们可以将 分解为 ,然后再将 分解为 ,以此类推,直到 为止。Python 代码如下:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
通过上述递归求解 的方式,我们可以了解递归求解问题的两个关键要素:
基本情况
(Base Case
),不再递归的条件,即最初始的、简单的、可以直接得出结果的情况。比如阶乘里的 。递归情况
(Recursive Case
),即当问题每一步变更符合固定规律时,可以根据前后两步的固定的关系基于前一步的解求得当前迭代轮次的解,递归就是基于这个关系从后往前计算的过程。比如阶乘里的 = 。
递归的本质就是调用自身。在阶乘的例子中,它在逻辑上更接近自然的数学定义,更符合问题的自然分治结构。递归调用将问题拆解成更小的子问题,并在达到递归基条件时终止递归。
递归历险
通过求解 这样简单的问题我们可能还无法感受到递归的妙处,下面我们以复杂一些的示例来展示递归思想的优势。
抢 20 游戏
两个人做游戏,轮流从 1 和 2 中挑选一个数字,并计算所有被选到数字的总和,当数字的总和正好为 20 时,当前出手的人获胜。问题是如何设定策略可以保证一定能赢?
这个问题正向思维的解法是通过穷举所有可能的情况,然后找到必胜策略,但要穷举所有情况是相当困难的。但是如果采用递归思维,将这个问题分解一下,问题可能会变得相当简单。
我们试想一下,如果想要保证最后一轮轮到自己时,一定能使数字的总和为 20,那么在上一轮轮到自己时数字的总和一定要是 17,因为只有在 17 的基础上,我们可以保障无论对手选择 1 还是 2,我们都可以使数字的总和为 20。
同理,要保障自己可以得到总和为 17 的结果,那么在上上轮轮到自己时数字的总和一定要是 14;以此类推,我们可以得到一个规律,即在轮到自己时,数字的总和一定要是 20 的倍数减去 3 的倍数。最终我们可以得出,只要我们能先抢到 的总和数,我们就一定能赢,比如我们首轮首次出手就抢到 2。
这就是递归思维的妙处,通过分析问题的结构,我们可以找到问题的关键规律,比如只要在上一轮我们抢到了比最终总和 20 差 3 的总和数,就能赢得游戏。在此基础上可以将整个问题拆解成简单的子问题,如保障每一轮都能得到比上一轮总和差 3 的总和数,就一定能赢得游戏。如此,我们只需要关注并依次解决当前简单的子问题,直到迭代达到递归基条件时终止,如只要保障最初可以抢到 2 就一定能赢。问题就这样被神奇的解决了。
递归思维的解法是通过分析问题的结构,找到问题每一步变动时的规律,如果这个变动是一个固定关系,那么可以假设我们已经拿到前一步的解,基于这个固定关系和前一步的解,我们就能够计算出下一步的解,如此从后往前递归迭代,直到遇到初始的状态。
上台阶问题
总共有 20 个台阶,每次可以上 1 或者 2 个台阶,那么总共有多少种上到 20 阶的方式?
与“抢 20 游戏”类似,我们求解这个问题的正向思路就是将所有的可 能性统统列举出来,比如(1、4、7、10、12、15、18、20),(1、2、5、8、11、14、17、20)等等,然后试图从中找到一般性规律,从而找到第 n 阶的做法数与 n 的关系()。但这无疑是非常困难的,我们看看 F(n) 的公式就知道这有多难:
但是如果我们换个思路,采用逆向思维,问题就会变得很简单。我们只要知道,当前所处的台阶位置只能是从低它 1 阶或 2 阶这两种情况而来,比如第 20 阶只能是从第 19 阶或第 18 阶而来,那么自然而然的就可以得出 F(20) 其实就是 F(19) 与 F(18) 的和,更一般的:
我们不难知道 F(1) = 1,F(2) = 2,因此采用递归方法很容易实现求解:
def climb_stairs(n):
if n == 1:
return 1
if n == 2:
return 2
return climb_stairs(n-1) + climb_stairs(n-2)