理解递归
递归的两个特点
- 调用自身
- 结束条件
举个从小就听过的例子:
1. 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事: 2. 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事: 3. 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事: 4. 从前有座山,山中有座庙,庙里有个老和尚,老和尚在给小和尚讲故事: 4.“太困了不讲了”,于是都回去睡觉了。 3. 于是都回去睡觉了。 2. 于是都回去睡觉了。 1. 于是都回去睡觉了。
我晕,怎么讲了那么多故事,其实不然,我用不同颜色标记处来了每一层的对应关系,
很明显地看出颜色从头开始往中间逐渐变化,然后到一定程度(我们称之为递归的边界或是结束条件)就从内二外的层层返回。
这就是递归
类似于剥洋葱,一层套着一层,直到掰到最里层。
接着看下面的例子:
这句吓得我抱起了抱着抱着抱着我的小鲤鱼的我的我的我如果从字面意义上看可能看不出是什么意思,那么我们可以通过代码来实现同样的效果:
void digui(int n){
cout << "抱着";
if(!n){
cout << "我的小鲤鱼";
}
else{
digui(n - 1);
}
cout << "的我";
return;
}
int main(){
cout << "吓得我抱起了";
digui(2);
return 0;
}
计算n的阶乘
迭代版
n = 7
result = 1
for i in range(1, 7):
res = result * i
print(res)
1的阶乘等于1
2的阶乘等 1的阶乘 乘以2 等于2
3的阶乘等于 2的阶乘 乘以3 等于6
依次类推,就可以求出4,5,n的阶乘
我们在设计迭代算法的时候,使用的正向思维的方式
递归的思维正好相反,属于逆向思维
我们想计算5的阶乘
这个时候多么希望已经计算好了4的阶乘
然后在4的阶乘的基础上 乘以5 就是5的阶乘
但是4的阶乘我们不知道,继续向前希望求出3的阶乘 乘以4 得到4的阶乘
依此类推,不断向前推出到0的阶乘,1的阶乘等于1
这样后面所有的阶乘结果,都可以算出来。
这就是递归的逆向思维方式
即从 最终想要的答案出发
逐步向前寻找 上一层的答案
并且 用他们构造 当前层的答案
直到找到最深的那一层,问题的答案足够简单
递归执行便开始返回,并将每层的答案依次填上
def factorial(n):
if n == 0:
return 1
tmp = factorial(n - 1)
return tmp * n
迭代和递归的区别
一般来说,递归可以分为4个步骤
- 在函数定义中,我们要明确这个函数要做一件什么事情及它的输入参数是什么,我们希望他能完成什么样的任务,他的返回值是什么
- 在基础情况处理的阶段,我们要考虑判断数据规模最够小且答案显而易见的时候,直接将答案写死并将其返回,基础情况是为了递归触底的时候反弹,以免递归无限继续下去,
- 在递归调用阶段,我们每次让数据规模减小一点,并且调用递归函数,这样就能获得较小规模的问题答案,
- 在递推阶段,我们通过上一层的答案进行简单递推,便可以得到当前层的答案。
如何治疗晕递归
以归并排序为例:
为什么我们经常在涉及递归操作的时候会头晕?
因为里面涉及了超级操作,所以我们怀疑这个超级操作是否能完成他的使命,在这里面的生成逻辑会不会出错?
抱着这种怀疑的态度,我们可能就会视图深入探索一下超级操作的内部逻辑,也就是说视图让我们的思维进入下一层递归中,继续划分子数组,继续给子数组排序,这样的话,随着我们的思考层数加深,会感到递归设计越来越困难,头脑也越来越困惑。
那么我们怎么跳出这个圈套呢?
我们要将超级操作看成一个整体
我们要忽略超级操作里的一切细节,只要知道他作为一个整体,能完成什么使命就可以了,并且坚定不移的相信他能够完成他的使命,通过将超级操作看成一个整体,我们不再探究他内部的深层操作,只是假定他一定能够完成任务,这就是治疗晕递归的方法。
正如宋丹丹说,要把大象装冰箱总共分几步?
这里,把大象装进冰箱属于超级操作,我们不需要探究把大象装进冰箱的内部细节,而是坚定不移的相信他能够实现。
递归的设计思路