Python数据结构与算法——第五课 递归与循环

  上一课中提到了程序运行的现场,为了更加深入的了解递归调用(递归一般是调用),也要简单讨论一下程序运行的现场。程序运行的现场与拍电影的现场差不多,只是一虚一实而已。

  一般来说,一个应用是一个进程,一个进程往往有多个线程。现场是线程创建的,线程可以创建多个现场,但线程只能保持一个现场;反过来,某个现场也只能由创建线程持有。

  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+......

的前n1≤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教育科技有限责任公司资深工程师。曾就职于互爱(北京)、用友软件、大唐电信、IBMIT企业,主持及参与过多个大型IT产品和项目的开发,有丰富的IT产品设计和开发经验。目前致力于IT教育领域,努力把IT实战开发经验与教学有机结合起来,服务于IT产业人才培养。