上一课中提到了程序运行的“现场”,为了更加深入的了解递归调用(递归一般是调用),也要简单讨论一下程序运行的“现场”。程序运行的“现场”与拍电影的现场差不多,只是一虚一实而已。
一般来说,一个应用是一个进程,一个进程往往有多个线程。现场是线程创建的,线程可以创建多个现场,但线程只能保持一个现场;反过来,某个现场也只能由创建线程持有。
Python中所谓的“名字空间”就是一块程序运行“现场”的模板。当某个名字空间被调用进入运行之前,系统根据“名字空间”这个“现场”的模板在内存某个区域创建一个“现场”,使“现场”里的状态(Python里称作名字)都处于就绪状态(名字已分配内存储存空间),程序就在这个“现场”中运行。当运行结束后,返回调用的名字空间的现场,被调用的名字空间的这个“现场”就被销毁回收了。
从Python的角度看,递归就是某个线程在某个名字空间的现场中调用这个名字空间。系统把线程当前的现场压入运行堆栈(内存里),然后根据这个名字空间创建一个新的现场供给这个线程使用,线程进入执行状态。如此下去,直到不符合递归调用条件为止。所以,递归是内存吃货,不冤枉。
循环则是同一个线程在同一个现场重复执行某一块代码,不涉及调用,所以不会有现场压入运行堆栈的情况,内存消耗当然少了。
如果忽略程序执行的细节,递归也是一种循环,因此,一般情况递归和循环是可以相互转化的。
例题1:用递归模拟下面升序循环。
递归模拟代码:
运行结果:
例题2:用递归模拟下面降序循环。
和上一例题一样,把递归语句放在执行语句之后,不过需要修改递归条件。这是递归调用的常用模式,因为这种模式与人类的思维模式比较接近。下图所示代码:
当然也可以不修改递归条件,把递归语句放在执行语句之前。下图所示代码:
运行结果都一样:
例题2的前递归模拟和例题1的代码除了递归调用语句的位置不一样外,其他代码都相同。为什么只改变递归调用语句的位置,结果就倒过来了呢?为此,下面把例题2的前递归模拟执行过程描述如下:
1号现场:创建,状态i=1,遇到递归语句,入栈,递归调用;
2号现场:创建,状态i=2,遇到递归语句,入栈,递归调用;
3号现场:创建,状态i=3,遇到递归语句,入栈,递归调用;
4号现场:创建,状态i=4,遇到递归语句,入栈,递归调用;
5号现场:创建,状态i=5,遇到递归语句,入栈,递归调用;
6号现场:创建,状态i=6,遇到递归语句,入栈,递归调用;
7号现场:创建,状态i=7,遇到递归语句,入栈,递归调用;
8号现场:创建,状态i=8,遇到递归语句,入栈,递归调用;
9号现场:创建,状态i=9,遇到递归语句,入栈,递归调用;
10号现场:创建,状态i=10,不满足i<10的递归条件,销毁,返回;
9号现场:恢复,状态i=9,执行语句print(i, end="\t"),销毁,返回;
8号现场:恢复,状态i=8,执行语句print(i, end="\t"),销毁,返回;
7号现场:恢复,状态i=7,执行语句print(i, end="\t"),销毁,返回;
6号现场:恢复,状态i=6,执行语句print(i, end="\t"),销毁,返回;
5号现场:恢复,状态i=5,执行语句print(i, end="\t"),销毁,返回;
4号现场:恢复,状态i=4,执行语句print(i, end="\t"),销毁,返回;
3号现场:恢复,状态i=3,执行语句print(i, end="\t"),销毁,返回;
2号现场:恢复,状态i=2,执行语句print(i, end="\t"),销毁,返回;
1号现场:恢复,状态i=1,执行语句print(i, end="\t"),销毁,返回。
例题3:用循环实现上一课的计算前n个奇数和。
。
循环代码如下:
例题4:用循环实现上一课的半数和。
循环代码如下:
从例题4中可以看到,循环代码要比递归代码复杂。但如果maxNum很大(比如100亿),递归代码就无能为力了,循环代码只要计算机速度够快,照样能算出来。
练习题1:用递归和循环两种方法编程计算
2/3+2/9+2/27+2/81+......
的前n(1≤n≤20)项和(分数形式)。与例题4的结果比较,总结出数学规律,然后设计一个程序验证这个数学规律。
我们知道,循环是可以嵌套的,那么是否能用嵌套的递归调用实现呢?当然可以的,只要把递归语体中的执行部分换成递归调用就就行。
例题5:嵌套递归和循环方式打印九九乘法表。
程序运行结果:
练习题2:在数学中用符号“^”表示幂运算,例如2^3=8,用递归和循环两种方法编程打印九九幂表。
【舍罕王赏麦】
传说印度的舍罕王打算重赏国际象棋的发明人——当时的宰相西萨·班·达依尔。
这位聪明的宰相胃口似乎并不大,他对国王说:“陛下,请您在这张棋盘的第1小格内,赏给我1粒麦子,在第2个小格内给两粒,第3格内给4粒,照这样每一小格内比前一个小格加一倍。把这棋盘的64个小格放满就行啦!”
国王一听,心中暗喜,这个赏赐并不多,便答道:“你当然会如愿以偿的!”
国王立即令人把一袋麦子拿来,叫仆人照办。谁知还没到第20格,袋子已经空了。一袋又一袋的麦子扛到国王面前,但麦粒数一格接一格迅速增长,国王很快就看出,即便把全印度的麦子都给他,也实现不了他的诺言!
现在就用递归算法来算一下,64个格子到底需要多少粒麦粒才能放满。先看下面的算法分析。
第1格:rs1=1 总和:sum1=1
第2格:rs2=rs1X2 总和:sum2=sum1+rs2
第3格:rs3=rs2X2 总和:sum3=sum2+rs3
第4格:rs4=rs3×2 总和:sum4=sum3+rs4
…(直到第64个格子)
每轮用前一个格子的麦子数乘以2,再加上以前所有格子中麦子的数量,如此递归64。
例题6:计算64个格子2倍递增求和。
运行结果:
练习题3:把上面的代码转为循环方法。
【舍罕王赏麦(续)】(内容来自网络)
如果把这些麦粒合成重量,那就要给宰相4000亿蒲式耳才行。这样一算,这位看来“胃口”不大的宰相所要求的麦子,竟是全世界在2000年内所生产的全部小麦。计算结果说明,舍罕王是不可能实现自己的诺言的了。这自然使舍罕王十分尴尬。
正当舍罕王一筹莫展之际,王太子的数学教师知道了这件事,他笑着对国王说:“陛下,这个问题很简单啊,就像1+1=2一样容易,您怎么会被它难倒?”
国王大怒:“难道你要我把全世界两千年产的小麦都给他?”
这位教师说:“没有必要啊,陛下,其实,您只要让宰相大人到粮仓去,自己数出那些麦子就可以了。假如宰相大人一秒钟数一粒,数完18,446,744,073,709,551,615粒麦子所需要的时间,大约是5800亿年。就算宰相大人日夜不停地数,数到他自己魂归极乐,也只是数出了那些麦粒中极小的一部分。这样的话,就不是陛下不支付赏赐,而是宰相大人自己没有能力取走赏赐”。
国王恍然大悟,当即下令召来宰相,按王太子老师的方法赏赐他。
西萨·班·达依尔沉思片刻后笑道:“陛下啊,您的智慧超过了我,那些赏赐……我也只好不要了!”
当然,最后宰相西萨·班·达依尔还是获得了很多赏赐(不过没有麦子了^O^)。
练习题4:如果西萨·班·达依尔还年轻,还有40年的寿命,一年按365天计算,每秒数一个麦子放入棋盘,他能填到第几格?用递归和循环两种方法编程计算。
参考资料:
1、《Python算法图解》何韬编著 清华大学出版社
何韬现任TCL教育科技有限责任公司资深工程师。曾就职于互爱(北京)、用友软件、大唐电信、IBM等IT企业,主持及参与过多个大型IT产品和项目的开发,有丰富的IT产品设计和开发经验。目前致力于IT教育领域,努力把IT实战开发经验与教学有机结合起来,服务于IT产业人才培养。