递归算法的理解与应用
递归算法(recursion algorithm)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。
通俗的说就是能把大问题等价于一个小问题的循环重复,从而通过解决一个小问题来达到解决大问题的目的。
这里的循环重复,和普通的loop 语句不太一样,在代码中体现为方法的自调用。
众所周知,循环的过程必须有一个控制条件来断开循环,否则就会无限循环下去。
所以,能够使用且应该使用递归算法的应用场景,个人归纳为三点:
1. 大问题能拆分等价于小问题的循环重复(必须)
2. 有控制条件(称为出口)来断开自我调用,或者继续自我调用,控制条件并不一定是简单的判断语句,可以有多种情况或者多个条件(必须)
3. 一次自调用的结果,应该是下一次调用的初始值
让我们来看几个例子,并且对比使用递归算法以及不使用递归的差异,了解一下使用递归的心路历程。
1. 阶乘
//n*(n-1)*(n-1-1)*(n-1-1-1)... //解析n *(n-1) *(n-1-1) //阶乘的过程归纳为一个乘法运算的重复 *(n-1) public static double FactorialWithRecursion(int n) { var result = 1; int temp; if (n > 1) { result = n * (n - 1); { temp = n - 1; if (temp > 1)//可以看到往下都是一个重复的过程,可以看作是一个递归方法调用的展开 { result = result * (temp - 1); temp = temp - 1; if (temp > 1) { result = result * (temp - 1); temp = temp - 1; if (temp > 1) { result = result * (temp - 1);//可以把重复的代码抽离出来写出一个方法 ... } } } } return result; } else { return result; } }
经过抽离重复的代码,可以改进为如下。
public static double FactorialWithRecursion(int n) { if (n > 1)//明确终止条件 { Console.Write(n+"*"); return n * FactorialWithRecursion(n - 1); } else //当不满足终止条件时的处理,即断开自我循环调用的点 { Console.WriteLine(1); return 1; } }
2. 最经典的递归问题,斐波那契数列。
阐述: 斐波那契数列 1、1、2、3、5、8、13、21、34
斐波纳契数列以如下被以递归的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
简单的说,当n>=2时,当前数是前两个数值相加的结果。
/// <summary> /// /// </summary> /// <param name="targetNumberIndex">指定斐波那契数列第几位</param> /// <param name="previous">前一个数的值</param> /// <param name="currentResult">当前累加结果</param> /// <param name="currentNumberIndex">当前累加到第几位</param> /// <returns></returns> public static double FibonacciWithRecursion(int targetNumberIndex, int previous = 1, int currentResult = 1, int currentNumberIndex = 2) {
if (targetNumberIndex > 2 && currentNumberIndex <= targetNumberIndex) { currentNumberIndex += 1; Console.WriteLine(currentResult); var temp = currentResult; currentResult = currentResult + previous; return FibonacciWithRecursion(targetNumberIndex, temp, currentResult, currentNumberIndex); } else { return 1; } }
这边采用的是一个正向思维,即F(n) = F(n-1)+F(n-2). 上图中并不是一个优秀的实现,但是便于理解。
重复调用的过程归纳为,result = previous + result;
在这个例子当中,就体现了我在上文提到的第三点,一次函数调用的结果,是下一次函数调用的初始值。
3. 应用题
题目:一个人赶着鸭子去每个村庄卖,每经过一个村子卖去所赶鸭子的一半又一只。这样他经过了七个村子后还剩两只鸭子,问他出发时共赶多少只鸭子?
1. 首先我们发现题目中有句话“每经过一个村子卖去所赶鸭子的一半又一只”,可以看出这句话表明了这是一个重复的过程。
2. 在分析上面那句话,“卖去所赶鸭子的一半又一只”,这表明了重复的计算方式,但是这里有两种思维方式:
1) 计算卖出去的鸭子数量,y=x/2+1;
2)计算剩下的鸭子数量, y=x/2-1;
3. 再看“经过了七个村子后还剩两只鸭子,问他出发时共赶多少只鸭子“,可以得出这是一个根据结果推出初始条件的问题。
假设初始有X只鸭子,等式为 [(x/2-1)/2 -1]/2 -1...= 2; 重复过程为result =(result/2 -1), 出口即为重复天数,直到天数为零
如果从结果推出初始条件,等式可以写为,[(2+1)*2+1]*2 +1... = x; 重复过程为result = (result*2 + 1),出口为重复天数,直到天数等于给予的天数7
读者可以自己尝试着去实现这两种不同的方式。
最后附上一张自制体现递归过程的图
各种算法的思想,永远是程序猿的必备利器。