递归(一)——用“单步调试法”来理解递归调用过程

        在算法的学习过程中,“递归”算法似乎显得很神秘,时常让学习者一头雾水,感觉莫名其妙,可是掌握递归又是一个绕不过去的坎,因为很多更高级的数据结构和算法思想就是以递归为基础的,比如数据结构中的树和图,比如算法中的动态规划。

        递归函数之所以让我们觉得很玄幻,首先是无法理解函数自己调用自己, 其次是我们不清楚函数执行过程中的每一步都发生了什么,此时此刻,多么想单步调试代码啊,可是如果这么做了,就会发现仿佛进入了一个N维空间,一层又一层啊 ... ...  简直是有去无回,进去就出不来的感觉,放弃吧!

        但今天我们就着一个简单的题目,执行一次单步调试,看看递归到底怎么执行的每一步。

        做题目之前,我们只需要知道:递归就是把大事分解成若干件小事,把每一个小事的结果求出,然后通过一个决策过程就能得到大事的答案

  • 题目

求数组arr[L...R] 中的最大值,用递归方法实现。

  • 思路:利用二分法,将组数分为左、右两个部分,找到左侧数组的最大值,再找到右侧数组的最大值,两个最大值进行比较,较大的那个就是arr的最大值。

1)将L~ R 范围分成左右两个部分,左边:L~Mid, 右边:Mid~R;

2)求左边部分的最大值Max_L, 右边部分的最大值 Max_R;

3)   L~R 范围上的最大值,就是 max{Max_L, Max_R}

递归过程发生在2),不断的利用二分法,求左右两个部分的最大值,直到左部分(或右部分)只有一个数,则结束二分,结束递归。

  • 代码实现
#include <stdio.h>

int findMax(int* arr, int L, int R){
    if(L == R ) {
        return arr[L];
    }
    int mid = L + ((R-L)>>1);
    int max_l = findMax(arr,L,mid);
    int max_r = findMax(arr,mid+1,R);
    return max_l>max_r?max_l:max_r;
}

int main(){
    int arr[4] = {3,7,4,6};
    printf("max = %d", findMax(arr,0,3));
    return 0;
}

小技巧

        在求两个数L 和 R的中值的时候,代码中没有使用 mid = (L+R)/ 2, 而是利用了位运算,并且没有直接求两个数的和 而是去求了两个数的差,这是因为,如果L 和 R的值很大的话,它们的和有可能是个超大数,超出数据类型的表示范围,造成溢出报错。

        所以,这行代码利用位运算 代替除法运算,提高了程序的运行效率,利用求差运算取代求和运算,避免了数据溢出。

        int mid = L + ((R-L)>>1);  // 两个数的中值 = 较小的那个数 + 两个数差的一半

  • 运行结果

  • 代码分析

        接下来,我们来分析每一步的递归调用都发生了什么。

数组值3746
下标0123

        递归函数也是函数,它之所以可以顺利的完成了调用过程,就是因为利用了系统栈。这就是递归能够实现的真相。下图表格就代表系统栈。

        当程序执行进入  findMax (0,3) 后,不满足 if(L== R)条件,程序继续执行,执行到第8 行时,出现了子函数的调用语句,此时就需要保护现场,将当前函数findMax(0,3),以及局部变量mid ( =1), max_L = ? 和当前中断的行号 (=8) 都存入系统栈,接着销毁findMax(0,3)的执行过程,进入子函数 find(0,1); 

f(0,3), mid = 1, max_l = ? 行号:8

进入函数findMax(0,1) 后,不满足 if(L == R) ,程序继续执行到第8行,又遇到子程序调用,再次保存现场,将当前函数findMax(0,1),以及其对应的局部变量mid = 0, max_L=? , 行号=8 存入系统栈后,然后销毁findMax(0,1)的执行过程,进入子函数findMax(0,0);

f(0,1),mid=0, max_l= ? 行号:8
f(0,3), mid = 1, max_l = ? 行号:8

        进入函数findMax(0,0) 后,满足L== R,函数返回。 返回值给系统栈的栈顶。此时的栈顶是findMax(0,1), 那么就重建函数findMax(0,1)的执行过程,等待接收返回值的是 findMax(0,1) 的max_l, 即max_l = 4, 此时的行号是8 ,就会继续往下执行第9行语句。

f(0,3), mid = 1, max_l = ? 行号:8

   

执行到第9行语句,又遇到子函数的调用,保存现场 findMax(0,1), mid=0,mar_l = 4, max_r = ? ,行号=9;销毁findMax(0,1)的执行过程,进入子函数findMax(1,1);

f(0,1),mid = 0,max_l = 4, max_r = ? ,行号: 9
f(0,3), mid = 1, max_l = ? 行号:8

进入到子函数findMax(1,1), 满足 L==R, 函数返回, 返回值给系统栈的栈顶。此时的栈顶是findMax(0,1), 那么就重建函数findMax(0,1)执行过程,等待接收返回值是 findMax(0,1) 的max_r, 即max_r = 7, 此时的行号是9 ,继续往下执行第10行语句:return max_l>max_r?max_l:max_r, 返回值为7 ,给到系统栈栈顶,此时的栈顶是findMax(0,3) ;

f(0,3), mid = 1, max_l = ? 行号:8

重新构建findMax(0,3)的执行过程,max_l = 7, 程序继续往下执行到第9行,遇到子函数调用,保存现场,当前函数是findMax(0,3), mid = 1, max_l = 7, max_r=?,行号=9, 销毁findMax(0,3)的执行过程,进入子函数findMax(2,3);

f(0,3), mid = 1, max_l = 7,  max_r = ? 行号:9

进入函数findMax(2,3) , 不满足 if(L== R), 程序继续执行到第8行,遇到子函数调用,保存现场:findMax(2,3), mid = 2,max_l = ? 行号=8 ,销毁函数findMax(2,3) 的执行过程,进入子函数findMax(2,2);

f(2,3), mid = 2,max_l = ?,行号:8
f(0,3), mid = 1, max_l = 7,  max_r = ? 行号:9

进入函数findMax(2,2), 满足条件 L==R, 函数返回。返回值给到系统栈栈顶,此时系统栈栈顶是findMax(2,3), 重新构建函数findMax(2,3) 的执行过程。 即max_l = 返回值4, 程序继续往下执行到第9行,遇到子函数调用,保存现场: f(2,3), mid = 2, max_l = 4, max_r = ? 行号9,然后销毁当前函数 findMax(2,3)的执行过程, 进入子函数findMax(3,3);

f(2,3), mid = 2,max_l = 4, max_r = ? 行号:9
f(0,3), mid = 1, max_l = 7,  max_r = ? 行号:9

进入函数findMax(3,3), 满足条件L== R, 函数返回。返回值给到系统栈栈顶,此时系统栈栈顶是findMax(2,3), 重新构建函数findMax(2,3) 的执行过程。 max_r = 返回值6, 程序继续往下执行到第10行,return max_l >max_r? max_l:max_r, 所以返回 6. 

f(0,3), mid = 1, max_l = 7,  max_r = ? 行号:9

返回值给到系统栈栈顶,此时栈顶是函数findMax(0,3) , 重新构建函数findMax(0,3) 的执行过程,max_r = 返回值6, 程序继续往下执行到第10行, return max_l >max_r? max_l:max_r, 返回7.

栈空,程序执行结束。

  • 分析总结

1. 整个分析过程虽然比较啰嗦,但我们通过“单步调试”这种方法,给递归调用做了揭秘,知道了递归函数在执行过程中到底是怎么操作的,系统栈就是它的大杀器。

2. 通过对调用过程的感性的直观认识,知道为什么“递归层数不能太深”了,因为如果递归层数太深,就会增大对系统栈的开销,有可能出现栈溢出。

3. 在用递归思想处理问题的时候,我们不可能每一次都做这样的“单步调试”的分析,也没有这样分析的必要,但是要做做到“心中有数”,大脑中是有一张脑图的👇

对程序的运行走向,也要略知一二👇:

        第一步: 求0位置~3位置上的最大值 fun(0,3);  计算出中点 mid = 1;

        第二步: 求0位置~1位置上的最大值 fun(0,1), 计算出中点 mid = 0;

        第三步: 因为L== R== 0, 所以fun(0) = 3;

        第四步: 因为L == R == 1, 所以 fun(1) = 7;

        第五步: 0位置~1位置的最大值,是7

        第六步:求2位置~3位置的最大值fun(2,3), mid = 2;

        第七步:因为 L== R == 2, 所以 fun(2) = 4;

        第八步:因为 L == R == 3,所以 fun(3) = 6;

        第九步:2位置~3位置的最大值是6;

        第十步:0位置~ 3位置的最大值是 7;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xinran0703

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值