当前位置:  编程技术>c/c++/嵌入式

对C语言中递归算法的深入解析

    来源: 互联网  发布时间:2014-10-17

    本文导语:  许多教科书都把计算机阶乘和菲波那契数列用来说明递归,非常不幸我们可爱的著名的老潭老师的《C语言程序设计》一书中就是从阶乘的计算开始的函数递归。导致读过这本经书的同学们,看到阶乘计算第一个想法就是递归。...

许多教科书都把计算机阶乘和菲波那契数列用来说明递归,非常不幸我们可爱的著名的老潭老师的《C语言程序设计》一书中就是从阶乘的计算开始的函数递归。导致读过这本经书的同学们,看到阶乘计算第一个想法就是递归。但是在阶乘的计算里,递归并没有提供任何优越之处。在菲波那契数列中,它的效率更是低的非常恐怖。

这里有一个简单的程序,可用于说明递归。程序的目的是把一个整数从二进制形式转换为可打印的字符形式。例如:给出一个值4267,我们需要依次产生字符‘4',‘2',‘6',和‘7'。就如在printf函数中使用了%d格式码,它就会执行类似处理。

我们采用的策略是把这个值反复除以10,并打印各个余数。例如,4267除10的余数是7,但是我们不能直接打印这个余数。我们需要打印的是机器字符集中表示数字‘7'的值。在ASCII码中,字符‘7'的值是55,所以我们需要在余数上加上48来获得正确的字符,但是,使用字符常量而不是整型常量可以提高程序的可移植性。‘0'的ASCII码是48,所以我们用余数加上‘0',所以有下面的关系:

          ‘0'+ 0 =‘0'
          ‘0'+ 1 =‘1'
          ‘0'+ 2 =‘2'
             ...
从这些关系中,我们很容易看出在余数上加上‘0'就可以产生对应字符的代码。接着就打印出余数。下一步再取商的值,4267/10等于426。然后用这个值重复上述步骤。

这种处理方法存在的唯一问题是它产生的数字次序正好相反,它们是逆向打印的。所以在我们的程序中使用递归来修正这个问题。

我们这个程序中的函数是递归性质的,因为它包含了一个对自身的调用。乍一看,函数似乎永远不会终止。当函数调用时,它将调用自身,第2次调用还将调用自身,以此类推,似乎永远调用下去。这也是我们在刚接触递归时最想不明白的事情。但是,事实上并不会出现这种情况。

这个程序的递归实现了某种类型的螺旋状while循环。while循环在循环体每次执行时必须取得某种进展,逐步迫近循环终止条件。递归函数也是如此,它在每次递归调用后必须越来越接近某种限制条件。当递归函数符合这个限制条件时,它便不在调用自身。

在程序中,递归函数的限制条件就是变量quotient为零。在每次递归调用之前,我们都把quotient除以10,所以每递归调用一次,它的值就越来越接近零。当它最终变成零时,递归便告终止。

/*接受一个整型值(无符号0,把它转换为字符并打印它,前导零被删除*/

代码如下:

#include

int binary_to_ascii( unsigned int value)
{
          unsigned int quotient;

     quotient = value / 10;
     if( quotient != 0)
           binary_to_ascii( quotient);
     putchar ( value % 10 + '0' );
}


递归是如何帮助我们以正确的顺序打印这些字符呢?下面是这个函数的工作流程。
1. 将参数值除以10
2. 如果quotient的值为非零,调用binary-to-ascii打印quotient当前值的各位数字
3. 接着,打印步骤1中除法运算的余数

注意在第2个步骤中,我们需要打印的是quotient当前值的各位数字。我们所面临的问题和最初的问题完全相同,只是变量quotient的值变小了。我们用刚刚编写的函数(把整数转换为各个数字字符并打印出来)来解决这个问题。由于quotient的值越来越小,所以递归最终会终止。

一旦你理解了递归,阅读递归函数最容易的方法不是纠缠于它的执行过程,而是相信递归函数会顺利完成它的任务。如果你的每个步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总是能正确的完成任务。

但是,为了理解递归的工作原理,你需要追踪递归调用的执行过程,所以让我们来进行这项工作。追踪一个递归函数的执行过程的关键是理解函数中所声明的变量是如何存储的。当函数被调用时,它的变量的空间是创建于运行时堆栈上的。以前调用的函数的变量扔保留在堆栈上,但他们被新函数的变量所掩盖,因此是不能被访问的。

当递归函数调用自身时,情况于是如此。每进行一次新的调用,都将创建一批变量,他们将掩盖递归函数前一次调用所创建的变量。当我追踪一个递归函数的执行过程时,必须把分数不同次调用的变量区分开来,以避免混淆。

程序中的函数有两个变量:参数value和局部变量quotient。下面的一些图显示了堆栈的状态,当前可以访问的变量位于栈顶。所有其他调用的变量饰以灰色的阴影,表示他们不能被当前正在执行的函数访问。

假定我们以4267这个值调用递归函数。当函数刚开始执行时,堆栈的内容如下图所示:


执行除法之后,堆栈的内容如下:


  
接着,if语句判断出quotient的值非零,所以对该函数执行递归调用。当这个函数第二次被调用之初,堆栈的内容如下:
 
堆栈上创建了一批新的变量,隐藏了前面的那批变量,除非当前这次递归调用返回,否则他们是不能被访问的。再次执行除法运算之后,堆栈的内容如下:

quotient的值现在为42,仍然非零,所以需要继续执行递归调用,并再创建一批变量。在执行完这次调用的出发运算之后,堆栈的内容如下:
 
此时,quotient的值还是非零,仍然需要执行递归调用。在执行除法运算之后,堆栈的内容如下:



不算递归调用语句本身,到目前为止所执行的语句只是除法运算以及对quotient的值进行测试。由于递归调用这些语句重复执行,所以它的效果类似循环:当quotient的值非零时,把它的值作为初始值重新开始循环。但是,递归调用将会保存一些信息(这点与循环不同),也就好是保存在堆栈中的变量值。这些信息很快就会变得非常重要。

现在quotient的值变成了零,递归函数便不再调用自身,而是开始打印输出。然后函数返回,并开始销毁堆栈上的变量值。

每次调用putchar得到变量value的最后一个数字,方法是对value进行模10取余运算,其结果是一个0到9之间的整数。把它与字符常量‘0'相加,其结果便是对应于这个数字的ASCII字符,然后把这个字符打印出来。

输出4:


接着函数返回,它的变量从堆栈中销毁。接着,递归函数的前一次调用重新继续执行,她所使用的是自己的变量,他们现在位于堆栈的顶部。因为它的value值是42,所以调用putchar后打印出来的数字是2。

输出42:

接着递归函数的这次调用也返回,它的变量也被销毁,此时位于堆栈顶部的是递归函数再前一次调用的变量。递归调用从这个位置继续执行,这次打印的数字是6。在这次调用返回之前,堆栈的内容如下:

输出426:


现在我们已经展开了整个递归过程,并回到该函数最初的调用。这次调用打印出数字7,也就是它的value参数除10的余数。

输出4267:
然后,这个递归函数就彻底返回到其他函数调用它的地点。
如果你把打印出来的字符一个接一个排在一起,出现在打印机或屏幕上,你将看到正确的值:4267
使用递归一定要有跳出的条件:
这是一个最简单的递归, 不过它会一直执行, 可用 Ctrl+C 终止.
代码如下:

#include
void prn(int num) {
    printf("%d/n", num);
    if (num > 0) prn(--num); 
}
int main(void)
{
    prn(9);
    getchar();
    return 0;
}

代码如下:

实例: 翻转字符串
#include
void revers(char *cs);
int main(void)
{
    revers("123456789");
    getchar();   
    return 0;
}
void revers(char *cs)
{
    if (*cs)
    {
        revers(cs + 1);
        putchar(*cs);
    }
}

代码如下:

实例: 阶乘
#include
int factorial(int num);
int main(void)
{
    int i;
    for (i = 1; i 1) IntToBinary(num / 2);
    putchar(i ? '1' : '0');
//    putchar('0' + i);  /* 可代替上面一句 */
}

代码如下:

剖析递归:
#include
void prn(unsigned n);
int main(void)
{
    prn(1);
    getchar();
    return 0;
}
void prn(unsigned n) {
    printf("%d: %p/n", n, &n);  /* A */
    if (n < 4)
        prn(n+1);               /* B */
    printf("%d: %p/n", n, &n);  /* C */
}

例输出效果图:

分析:
程序运行到 A, 输出了第一行.
此时 n=1, 满足 < 4 的条件, 继续执行 B 开始了自调用(接着会输出第二行); 注意 n=1 时语句 C 还有待执行.
...如此循环, 一直到 n=4, A 可以执行, 但因不满足条件 B 执行不了了; 终于在 n=4 时得以执行 C.
但此时内存中有四个函数都等待返回(分别是 n=1、2、3、4 时), 咱们分别叫它 f1、f2、f3、f4.
f4 执行 C 输出了第五行, 函数返回, 返回给 f3(此时 n=3), f3 得以继续执行 C, 输出了第六行.
f3 -> f2 -> 继续 C, 输出了第七行.
f2 -> f1 -> 继续 C, 输出了第八行, 执行完毕!

如此看来, 递归函数还是很费内存的(有时不如直接使用循环), 但的确很巧妙.


    
 
 

您可能感兴趣的文章:

  • 使用C语言递归与非递归实现字符串反转函数char *reverse(char *str)的方法
  • 纯C语言:递归组合数源码分享
  • 纯C语言:递归最大数源码分享
  • 纯C语言:递归二进制转十进制源码分享
  • C语言使用普通循环方法和递归求斐波那契序列示例代码
  • C语言的递归思想实例分析
  • C语言二叉树的非递归遍历实例分析
  • c语言版本二叉树基本操作示例(先序 递归 非递归)
  • C语言函数的递归和调用实例分析
  • c语言实现MD5算法完整代码示例
  • LM优化算法的C语言实现 levmar
  • MD5算法的C语言实现
  • c语言的金钱算法
  • c语言 汉诺塔算法代码
  • C语言快速幂取模算法小结
  • C语言实现的PNPoly算法代码例子
  • C语言 扩展欧几里得算法代码
  • c语言快速排序算法示例代码分享
  • C语言对堆排序一个算法思路和实现代码
  • c语言实现奇偶排序算法
  • c语言中使用BF-KMP算法实例
  • C语言的数字游戏算法效率问题探讨实例
  • 纯C语言:贪心Prim算法生成树问题源码分享
  • C语言实现二叉树遍历的迭代算法
  • C语言kmp算法简单示例和实现原理探究
  • c语言实现单链表算法示例分享
  • 马尔可夫链算法(markov算法)的awk、C++、C语言实现代码
  • C语言位图算法详解
  • C语言实现魔方阵算法(幻方阵 奇魔方 单偶魔方实现)
  • C语言实现的排列组合问题的通用算法、解决方法
  •  
    本站(WWW.)旨在分享和传播互联网科技相关的资讯和技术,将尽最大努力为读者提供更好的信息聚合和浏览方式。
    本站(WWW.)站内文章除注明原创外,均为转载、整理或搜集自网络。欢迎任何形式的转载,转载请注明出处。












  • 相关文章推荐
  • C语言二维条形码解析库 libqrencode
  • C语言的XML解析器 iksemel
  • 用C语言如何组装和解析XML报文???
  • C 语言的 JSON 解析器 BeneJSON
  • 表达式语言解析器 Commons EL
  • C语言的异步DNS解析库 c-ares
  • C语言DNS解析器 dns.c
  • C语言的HTML解析库 libhtml
  • 请问脚本语言 解析器的工作原理?
  • 在做FTP服务端,请问哪位有解析LIST命令的C语言代码?
  • 纯C语言实现的HTML5解析库 Gumbo
  • C语言解析XML报文的问题
  • 深入解析C语言中常数的数据类型
  • 解析c语言中"函数调用中缺少哨兵"的情况分析
  • C语言static修饰函数详细解析
  • C语言的指针类型详细解析
  • c语言中static和extern的用法详细解析
  • 解析c语言switch中break语句的具体作用
  • 解析C语言中位字段内存分配的问题
  • C语言typedef与复杂函数声明问题的深入解析
  • 2013年7月和2013年8月编程语言排行榜
  • C语言static修饰函数详细解析 iis7站长之家
  • 2017 年热门编程语言排行榜出炉,你的语言上榜没?
  • C语言中有指针,因此C语言可以创建链表,那么Java语言没有指针,那Java是否可以创建链表呢?
  • 苹果OS X和IOS下最新编程语言swift介绍
  • 求助,在linux下,c语言和汇编语言的接口是什么?
  • c语言判断某一年是否为闰年的各种实现程序代码
  • C语言中间语言 CIL
  • PHP编程语言介绍及安装测试方法
  • 最近学JSP,苦于HTML语言和JAVA语言太差,请教推荐几本书,thanks.
  • Linux下C语言strstr()查找子字符串位置函数详细介绍(strstr原型、实现及用法)


  • 站内导航:


    特别声明:169IT网站部分信息来自互联网,如果侵犯您的权利,请及时告知,本站将立即删除!

    ©2012-2021,,E-mail:www_#163.com(请将#改为@)

    浙ICP备11055608号-3