递归相信大家都很熟悉,简单来说,就是把一个很大的东西按照相同的模式回归到一个确定的点上。
比如说爬楼梯,如果一个人一步能跨2个台阶,现在问:他第100步能跨到第几个台阶上?
有人说200,很明显不对,因为老朽没说他最开始在哪个台阶上,如果最开始就在第200个台阶上,那答案就是200 + 200 = 400了。
我们可以简单的列个等式:H(n) = H(n - 1) + 2, H(0) = C(n >= 1,C为常量),这样才能求解H(100) = 400。
所以说,没指定初始值的递归都是耍流氓!
由此我们不禁要问:正确的递归应该满足哪些原则呢?
其实书本已经写得很清楚了:
其中第1条和第2条就是指我上面说的,很好理解,需要注意的是”朝向基准“这个关键词,朝向基准更多地是强调收敛,而不是单调,如F(n) = (-1)^n * F(n - 1),F(0) = 1, (n为不小于1的正整数)不单调,但同样可以求。
第3条就是说你每个递归都是可执行的,再举上面爬台阶的例子,你必须能保证一步跨出后就是”增加“2层,这样我们才能”准确“地计算出下一层是多少。不能说搞不好哪步跨不上去,这样预估的结果肯定不具有唯一性。
最难理解的可能是第4条:合成效益法则,因为这条与程序的正确性无关,但同时也是程序员最容易损失性能的地方!
就拿斐波那契数列F(n) = F(n - 2) + F(n - 1)来说,我们很容易写出如下的递归算法:
/**
* Retrieve Fibonacci result by recursion.
* F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
*/
private fun fibonacci(n: Int): Int {
// Ignore parameter checking!!!
// Decide the basic point.
return when(n) {
0 -> 0
1 -> 1
else -> fibonacci(n - 1) + fibonacci(n - 2)
}
}
代码确实很简洁,但是有没有发现n-1以下均被执行了double。
也就是说在不同的递归里做了重复的动作,如上图求F(n)计算了F(n - 2),但是求F(n - 1)同样计算了F(n - 2),这就违背了第4条的合成效益法则。
那么正确地做法是什么样的呢?就不用递归呗,直接上代码吧。
/**
* Retrieve Fibonacci result by eliminating repeated works in recursion.
* F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
*/
private fun fibonacciPlus(n: Int): Int {
// Ignore parameter checking to ensure n >= 2.
// Declare an array to store Fibonacci numbers.
val storeArray = Array<Int>(n + 2) { it }
storeArray[0] = 0
storeArray[1] = 1
for(i in 2 .. n) {
//Add the previous 2 numbers in the series and store it.
storeArray[i] = storeArray[i - 1] + storeArray[i - 2]
}
return storeArray[n]
}
最后,大家跟老夫一起默念一下递归的四个原则。
git: https://github.com/codersth/dsa-study.git
文件:Recursion4Principles.kt