C语言递归分析
思路
下图描述的是从问题引出到问题变异的思维过程:

概述
本文以数制转换为引,对递归进行分析。主要是从多角度分析递归过程及讨论递归特点和用法。
引子
一次在完成某个程序时,突然想要实现任意进制数相互转换,于是就琢磨,至少涉及以下参数:
- 源进制数:scr
- 目标进制:dest_d
实现的大致思路:
scr --> 数字分解 --> 按权求和 --> dest
很明显这个过程是先正序分解,然后逆序求和,所以我就联想到了递归。
递归
1. 递归的含义
递归就是递归函数。递归函数是直接或间接调用自身的函数。
举个例子:
程序1: btoa.c
/*
** 接受一个整型值(无符号),把它转换为字符并打印它,前导零被删除。
*/
#include <stdio.h>
void binary_to_ascii( unsigned int value ) {
unsigned int quotient;
quotient = value / ;
if( quotient != )
binary_tc_ascii( quotient );
putchar( value % + '' );
}
另外递归还有所谓“三个条件”,“两个阶段”。我就不说了。实际应用时一般都很自然的满足条件。
2. 递归过程分析
中断角度
看例:
有5人从左至右坐,右边人的年龄比相邻左边人大2岁,最左边的那个人10岁。问最右边人年龄。
程序2: age.c#include <stdio.h>
age(int n) {
int c;
if( n == )
c = ;
else
c = age( n- ) + ;
return(c);
} int main() {
printf("%d\n\n",age( ) );
return ;
}
表达式:
递推和回推过程:
这跟中断有什么联系呢?现在看来确实不很明显,不过最初我就是由它想到《微机原理》中的中断的:从age(5)开始执行,然后调用age(4),即来一个中断,此时先保护现场,然后一直递归直到n=1时,中断结束,然后层层返回,也就是不断恢复现场的过程。
嵌套调用角度:
嵌套调用关系图:
看懂了这个图,把上面的fun_a()和fun_b()全换成一样的fun(),就相当于是递归时的函数对自身的调用过程。
另外好像这幅图更容易看出“中断过程”吧。堆栈角度
如果中断和嵌套这两个角度都看明白的话,这个堆栈角度就是升华一下。
还用程序1为例进行分析:
程序1的函数有两个变量:参数value和局部变量quotient。下面的一些图显示了堆栈的状态,当前可以访问的变量位于栈顶。所有其他调用的变量饰以灰色阴影,表示它们不能被当前正在执行的函数访问。
假定我们以4267这个值调用递归函数。当函数开始执行时,堆栈的内容如下图所示。
执行除法运算之后,堆栈的内容如下:
接着,if语句判断出 quotient 的值非零,所以对该函数执行递归调用。当这个函数第二次被调用之初,堆栈的内容如下:
堆栈上创建了一批新的变量,隐藏了前面的那批变量,除非当前这次递归调用返回,否则它们是不能被访问的。再次执行除法运算之后,堆栈的内容如下:
quotient的值现在为42,仍然非零,所以需要继续执行递归调用,并再创建一批变量。在执行完这次调用的除法运算之后,堆栈的内容如下:
此时,quotient的值还是非零,仍然需要执行递归调用。在执行除法运算之后,堆栈的内容如下:
不算递归调用语句本身,到目前为止所执行的语句只是除法运算以及对quotient的值进行测试。由于递归调用使这些语句重复执行,所以它的效果类似循环:当quotient的值非零时,把它的值作为初始值重新开始循环。但是,递归调用将会保存一些信息(这点与循环不同),也就是保存在堆栈中的变量值。这些信息很快就会变得非常重要。
现在quotient的值变成了零,递归函数便不再调用自身,而是开始打印输出。然后函数返回,并开始销毁堆栈上的变量值。
每次调用putchar得到变量value的最后一个数字,方法是对value进行模10余运算,其结果是一个0~9之间的整数。把它与字符常量'0'相加,其结果便是对应于这个数字的ASCII字符,然后把这个字符打印出来。
接着函数返回,它的变量从堆栈中销毁。接着,递归函数的前一次调用重新继续执行,它所使用的是自己的变量,它们现在位于堆栈的顶部。因为它的value值是42,所以调用putchar后打印出来的数字是2 。
接着递归函数的这次调用也返回,它的变量也被销毁,此时位于堆栈顶部的是递归函数再前一次调用的变量。递归调用从这个位置继续执行,这次打印的数字是6 。在这次调用返回之前,堆栈的内容如下:
现在我们已经展开了整个递归过程,并回到该函数最初的调用。这次调用打印出数字7,也就是它的value参数除10的余数。
然后,这个递归函数就彻底返回到其他函数调用它的地点。
如果你把打印的字符一个接一个排在一起,出现在打印机或屏幕上,你将看到正确的值4267 。3. 递归的应用
上面从不同角度对递归过程进行了分析。而际应用时并不要求你搞清楚每个递归的内部过程,重要的是用对。
下面主要是不恰当应用递归的一些例子:
许多教材中都把计算阶乘和菲波那契数列用来说明递归,然而前者中递归并没有提供任何优越之处,后者中递归的效率非常之低。
看一下极端的菲波那契数求解:
表达式:

这种递归形式的定义容易诱导人们使用递归形式来解决问题:程序3:fib_rec.c
/*
** 用递归方法计算第n个菲波那契数列的值。
*/ int fibonacci( int n ) {
if( n <= )
return ;
return fibonacci( n - ) + fibonacci( n - );
}这里有一个陷阱:它使用递归步骤计算fibonacci( n -1)和 fibonacci( n -2)。但是,在计算 fibonacci( n -1)时也将计算 fibonacci( n -2)。这个额外的代价有多大呢?答案是:它的代价远远不止一个冗余计算:每个递归调用都会触发另外两个递归调用,面这两个调用的任何一个还并将触发两个递归调用,再接下去的调用也是如此。这样,冗余计算的数量增长得非常快。例如,在递归计算fibonacci(10)时,fibonacci(3)的值被计算了21次。但是在递归计算fibonacci(30)时,fibonacci(3)的值被计算了317811次,当然,这317811次产生的结果是完全一样的,除了其中之一外,其余的纯属浪费。
想得更极端一些,假如你在程序中递归时不是两次而是3次,4次,更多次的调用自身,那我想可能会让程序崩溃吧。现在让我们尝试用循环代替递归:程序4:fib_iter.c
int fibonacci( int n ) {
int result;
int previous_result;
int next_older_result;
result = previous_result = ;
while(n > ) {
n -= ;
next_older_result = previous_result;
previous_result = result;
result = previous_result + next_older_result;
}
return result;
}
OK,说到这了,本文引子是数制转换,总得说点数制转换点题是吧。
嗯,把题目都忘记了,回引子看一下吧。
程序5:convert.c
#ifndef _CONERT_H
#define _CONERT_H
#include <stdio.h>
#include <math.h>
#endif /*
**main()
*/ int conert2any( int scr, int dest_d, int pow_base ) {
/*
** 调用该函数时参数pow_base必须为0
*/
int quotient, result;
int dest_d_base = ;
quotient = scr / dest_d;
if( quotient != )
result = ( scr % dest_d ) * pow( dest_d_base, pow_base) + conert2any( quotient, dest_d, ++pow_base );
else
result = ( scr % dest_d ) * pow( dest_d_base, pow_base);
return ( result );
}
OK,这个数制转换程序用递归实现,没什么问题,但受上例启发它也可以改为循环:
程序6:convert_loop.c
do {
result += (scr % dest_d ) * pow( dest_d_base, pow_base++ );
} while( scr /= dest_d != )
相比于递归,它更短小精悍,效率也高些。
经过两个递归改为循环的例子,你应该发现这两个例子有一个共同点:递归调用时最后执行的语句是return 。
对于这种调用时最后执行的是return的递归,有一种专门的称呼:尾部递归。
可以发现一般情况下尾部递归都可以改为相应的循环形式,而且更简洁高效。
那什么时候才必须用递归呢?据我目前的经验和思考,只有程序1--逆序打印是必须的,其它好像没有必须用递归的。
好了,到这递归也告一段落了,来个小插曲,谈一下我写程序5时的一些感受:
实现这个进制转换函数时,对递归的理解还不深,犯了现在看来可笑的错误:其中要用递归实现加权求和,我还曾苦思如何实现累加呢,每一次调用完后变量都销毁了,如何累加呢?苦思的结果是:利用静态变量保存累加的值。如果到此为止的话我也不会进一步学习递归。因为我想,虽然这样能实现,可是不完美,即便碧波函数调用完了,静态变量依然在占着空间,而且再次调用前还得先清零。C语言的递归不该是如此麻烦的,一定是我哪里想差了,于是我就反复看书上的例子,终于醒悟:直接用return返回不就可以实现累加了嘛。唉,当时脑子真是灌了浆糊了。
言归正传,全文结束,对递归总结一下:
- 递归即是函数对自身的嵌套调用。
- 一般情况下尾部递归是不必要的,用循环会更好。
- 用递归分析重复过程层次分明,所以最好用先用递归分析,然后转用循环去实现。
说明:
- 程序1,3,4 引自《C和指针》7.5
- 程序2 引自 本校教材《C语言程序设计》7.4
- “堆栈角度” 引自 《C和指针》7.5
date: 2014-12-10
C语言递归分析的更多相关文章
- C语言中递归什么时候能够省略return引发的思考:通过内联汇编解读C语言函数return的本质
事情的经过是这种,博主在用C写一个简单的业务时使用递归,因为粗心而忘了写return.结果发现返回的结果依旧是正确的.经过半小时的反汇编调试.证明了我的猜想,如今在博客里分享.也是对C语言编译原理的一 ...
- C语言 · 高精度加法
问题描述 输入两个整数a和b,输出这两个整数的和.a和b都不超过100位. 算法描述 由于a和b都比较大,所以不能直接使用语言中的标准数据类型来存储.对于这种问题,一般使用数组来处理. 定义一个数组A ...
- Windows server 2012 添加中文语言包(英文转为中文)(离线)
Windows server 2012 添加中文语言包(英文转为中文)(离线) 相关资料: 公司环境:亚马孙aws虚拟机 英文版Windows2012 中文SQL Server2012安装包,需要安装 ...
- iOS开发系列--Swift语言
概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...
- C语言 · Anagrams问题
问题描述 Anagrams指的是具有如下特性的两个单词:在这两个单词当中,每一个英文字母(不区分大小写)所出现的次数都是相同的.例如,"Unclear"和"Nuclear ...
- C语言 · 字符转对比
问题描述 给定两个仅由大写字母或小写字母组成的字符串(长度介于1到10之间),它们之间的关系是以下4中情况之一: 1:两个字符串长度不等.比如 Beijing 和 Hebei 2:两个字符串不仅长度相 ...
- JAVA语言中的修饰符
JAVA语言中的修饰符 -----------------------------------------------01--------------------------------------- ...
- Atitit 项目语言的选择 java c#.net php??
Atitit 项目语言的选择 java c#.net php?? 1.1. 编程语言与技术,应该使用开放式的目前流行的语言趋势1 1.2. 从个人职业生涯考虑,java优先1 1.3. 从项目实际来 ...
- 【开源】简单4步搞定QQ登录,无需什么代码功底【无语言界限】
说17号发超简单的教程就17号,qq核审通过后就封装了这个,现在放出来~~ 这个是我封装的一个开源项目:https://github.com/dunitian/LoTQQLogin ————————— ...
随机推荐
- C语言 之 printf () 函数你真的会用吗?
main(){ int i=8; printf("%d %d %d %d %d %d ",++i,--i,i++,i--,-i++,-i--); } 运行结果 8 7 7 8 -7 ...
- ucos_ii 上锁函数OSSchedLock()函数透析
因为任务调度时一般都是通过OSTIMEDLY()来实现.在这个函数中会对当前的任务执行挂起.同时查看任务调度表中是否有优先级合适的就绪任务.如果当前任务运行时调用OSSchedLock()给调度器上锁 ...
- Objective-C Http常用API 同步请求与异步请求
开发iOS应用要调用Http接口.获取Http资源,有一套比较成熟的框架ASIHTTPRequest.而我还是比较喜欢使用原始一点的 API,而它跟其他的面向对象语言有许多共通之处.本文分同步请求和异 ...
- 基于JDK6的JAX-WX为客户端提供XML与JSON格式数据服务,以及客户端采用AXIS调用案例
1:WebService服务端工程目录如下: 需要第三方jar包:gson-2.2.4.jar\javax.xml.bind.jar\commons-lang-2.5.jar 源码如下: packag ...
- 【Xamarin-IOS 开发环境搭建】
MAC 比较贵,虚拟机的干活..... 配置完虚拟机.进去后 ,安装XCode的 时候 ,失败了.错误信息: But when i have tried to install the Xcode it ...
- netcat
一.概述 netcat是网络工具中的瑞士军刀,它能通过TCP和UDP在网络中读写数据.通过与其他工具结合和重定向,你可以在脚本中以多种方式使用它.使用netcat命令所能完成的事情令人惊讶.netca ...
- android使用BlueStacks作为模拟器
android原生的模拟器启动比较慢,偶尔还会出现一些莫名的问题,我们除了可以使用我们的android手机来进行调试外,还可以使用第三方的android模拟器,例如BlueStacks模拟器: 相对原 ...
- Could not open ServletContext resource [/WEB-INF/applicationContext.xml]解决方法
抛错: org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document ...
- QTableWidget查找指定项(由github处学习到)
from PyQt4 import QtGui, QtCore class Window(QtGui.QWidget): def __init__(self, rows, columns): QtGu ...
- pyqt下拉菜单和打开指定的内容(或者exe,doc,ppt,url等内容)
#下拉菜单及显示 def _action(self): menu=QMenu(self) menu1=menu.addMenu(u'文件') menu11=menu1.addAction(u'新建任务 ...