写递归函数的正确思惟方法

什么是递归

 

  简单的定义: “当函数直接或者间接调用本身时,则发生了递归.” 提及来简单, 可是理解起来复杂, 由于递归并不直观, 也不符合咱们的思惟习惯, 相对于递归, 咱们更加容易理解迭代. 由于咱们平常生活中的思惟方式就是一步接一步的, 而且可以理解一件事情作了N遍这个概念. 而咱们平常生活中几乎不会有递归思惟的出现.
举个简单的例子, 即在C/C++中计算一个字符串的长度. 下面是传统的方式, 咱们通常都这样经过迭代来计算长度, 也很好理解.算法

size_t length(const char *str) { size_t length = 0; while (*str != 0) { ++length; ++str; } return length; } 

  而事实上, 咱们也能够经过递归来完成这样的任务.函数

size_t length(const char *str) { if (*str == 0) { return 0; } return length(++str) + 1; } 

  只不过, 咱们都不这么作罢了, 虽然这样的实现有的时候可能代码更短, 可是很明显, 从思惟上来讲更加难以理解一些. 固然, 我是说假如你不是习惯于函数式语言的话. 这个例子相对简单, 稍微看一下仍是能明白吧.
迭代的算法能够这样描述: 从第一个字符开始判断字符串的每个字符, 当该字符不为0的时候, 该字符串的长度加一.
递归的算法能够这样描述: 当前字符串的长度等于当前字符串除了首字符后, 剩下的字符串长度+1.
做为这么简单的例子, 两种算法其实大同小异, 虽然咱们习惯迭代, 可是, 也能看到, 递归的算法不管是从描述上仍是实际实现上, 并不比迭代要麻烦.测试

 

理解递归

  在初学递归的时候, 看到一个递归实现, 咱们老是不免陷入不停的回溯验证之中, 由于回溯就像反过来思考迭代, 这是咱们习惯的思惟方式, 可是实际上递归不须要这样来验证. 好比, 另一个常见的例子是阶乘的计算. 阶乘的定义: “一个正整数的阶乘(英语:factorial)是全部小于或等于该数的正整数的积,而且0的阶乘为1。” 如下是Ruby的实现:优化

def factorial(n) if n <= 1 then return 1 else return n * factorial(n - 1) end end 

  咱们怎么判断这个阶乘的递归计算是不是正确的呢? 先别说测试, 我说咱们读代码的时候怎么判断呢?
回溯的思考方式是这么验证的, 好比当n = 4时, 那么factoria(4)等于4 * factoria(3), 而factoria(3)等于3 * factoria(2)factoria(2)等于2 * factoria(1), 等于2 * 1, 因此factoria(4)等于4 * 3 * 2 * 1. 这个结果正好等于阶乘4的迭代定义.
用回溯的方式思考虽然能够验证当n = 某个较小数值是否正确, 可是其实无益于理解.
Paul Graham提到一种方法, 给我很大启发, 该方法以下:spa

  1. 当n=0, 1的时候, 结果正确.
  2. 假设函数对于n是正确的, 函数对n+1结果也正确.
    若是这两点是成立的,咱们知道这个函数对于全部可能的n都是正确的。

  这种方法很像数学概括法, 也是递归正确的思考方式, 事实上, 阶乘的递归表达方式就是1!=1,n!=(n-1)!×n(见wiki). 当程序实现符合算法描述的时候, 程序天然对了, 假如还不对, 那是算法自己错了…… 相对来讲, n,n+1的状况为通用状况, 虽然比较复杂, 可是还能理解, 最重要的, 也是最容易被新手忽略的问题在于第1点, 也就是基本用例(base case)要对. 好比, 上例中, 咱们去掉if n <= 1的判断后, 代码会进入死循环, 永远不会结束.code

使用递归

  既然递归比迭代要难以理解, 为啥咱们还须要递归呢? 从上面的例子来看, 天然意义不大, 可是不少东西的确用递归思惟会更加简单……
经典的例子就是斐波那契数列, 在数学上, 斐波那契数列就是用递归来定义的:递归

·F0 = 0
·F1 = 1 
·Fn = Fn – 1 + Fn – 2游戏

  有了递归的算法, 用程序实现实在再简单不过了:ip

def fibonacci(n) if n == 0 then return 0 elsif n == 1 then return 1 else return fibonacci(n - 1) + fibonacci(n - 2) end end 

  改成用迭代实现呢? 你能够试试.
  上面讲了怎么理解递归是正确的, 同时能够看到在有递归算法描述后, 其实程序很容易写, 那么最关键的问题就是, 咱们怎么找到一个问题的递归算法呢?
Paul Graham提到, 你只须要作两件事情:内存

  1. 你必需要示范如何解决问题的通常状况, 经过将问题切分红有限小并更小的子问题.
  2. 你必需要示范如何经过有限的步骤, 来解决最小的问题(基本用例).

若是这两件事完成了, 那问题就解决了. 由于递归每次都将问题变得更小, 而一个有限的问题终究会被解决的, 而最小的问题仅需几个有限的步骤就能解决.

  这个过程仍是数学概括法的方法, 只不过和上面提到的一个是验证, 一个是证实.
如今咱们用这个方法来寻找汉诺塔这个游戏的解决方法.(这实际上是数学家发明的游戏)

有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将全部圆盘移至C杆:
1.每次只能移动一个圆盘.
2.大盘不能叠在小盘上面.

汉诺塔

这个游戏在只有3个盘的时候玩起来较为简单, 盘越多, 就越难, 玩进去后, 你就会进入一种不停的经过回溯来推导下一步该干什么的状态, 这是比较难的. 我记得第一次碰到这个游戏好像是在大航海时代某一代游戏里面, 当时就以为挺有意思的. 推荐你们都实际的玩一下这个游戏, 试试你脑壳能想清楚几个盘的状况.
如今咱们来应用Paul Graham的方法思考这个游戏.

通常状况:
当有N个圆盘在A上, 咱们已经找到办法将其移到C杠上了, 咱们怎么移动N+1个圆盘到C杠上呢? 很简单, 咱们首先用将N个圆盘移动到C上的方法将N个圆盘都移动到B上, 而后再把第N+1个圆盘(最后一个)移动到C上, 再用一样的方法将在B杠上的N个圆盘移动到C上. 问题解决.

基本用例:
当有1个圆盘在A上, 咱们直接把圆盘移动到C上便可.

算法描述大概就是上面这样了, 其实也能够看做思惟的过程, 相对来讲仍是比较天然的. 下面是Ruby解:

def hanoi(n, from, to, other) if n == 1 then puts from + ' -> ' + to else hanoi(n-1, from, other, to) hanoi(1, from, to, other) hanoi(n-1, other, to, from) end end 

当n=3时的输出:

A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

上述代码中, from, to, other的做用其实也就是提供一个杆子的替代符, 在n=1时, 其实也就至关于直接移动. 看起来这么复杂的问题, 其实用递归这么容易, 没有想到吧. 要是想用迭代来解决这个问题呢? 仍是你本身试试吧, 你试的越多, 就能越体会到递归的好处.

递归的问题

固然, 这个世界上没有啥时万能的, 递归也不例外, 首先递归并不必定适用全部状况, 不少状况用迭代远远比用递归好了解, 其次, 相对来讲, 递归的效率每每要低于迭代的实现, 同时, 内存好用也会更大, 虽然这个时候能够用 尾递归来优化, 可是尾递归并非必定能简单作到.
相关文章
相关标签/搜索