第3章 递归

1、基本递归

假设想计算整数n的阶乘,比如4!=4×3×2×1。

迭代法:循环遍历其中的每一个数,然后与它之前的数相乘作为结果再参与下一次计算。可正式定义为:n! = (n)(n-1)(n-2)…(1)。

递归法:将n!定义为更小的阶乘形式。可以正式定义为:

递归过程中的两个基本阶段:递推与回归

递推阶段,每一个递归调用通过进一步调用自己来记住这次递归过程。当其中有调用满足终止条件时,递推结束。每一个递归函数都必须拥有至少一个终止条件;否则,递推阶段就永远不会结束了。一旦递推阶段结束,处理过程就进入回归阶段,在这之前的函数调用以逆序的方式回归,直到最初调用的函数返回为止,此时递归过程结束。

 示例3-1:以递归方式计算阶乘的函数实现

/* fact.c */
#include "fact.h"
/* fact */
int fact(int n) {
if (n < )
return ;
else if (n == )
return ;
else if (n == )
return ;
else
return n * fact(n - );
}

补充知识点 : C程序在内存中的组织方式

基本上来说一个可执行程序由4个区域组成:代码段、静态数据区、堆与栈(见图3-2a)。

  1)代码段:包含程序运行时所执行的机器指令;

  2)静态数据区:包含在程序生命周期内一直持久的数据,如全局变量和静态局部变量;

  3)堆:包含程序运行时动态分配的存储空间,比如用malloc分配的内存;

  4)栈:包含函数调用的信息。

按照惯例,堆的增长方向为从程序低地址高地址向上增长,而栈的增长方向刚好相反(实际情况可能不是这样,与CPU的体系结构有关)。

注意:此处的堆与数据结构中的堆没有什么关系。

图3-2:a)C程序在内存中的组织形式  b)一份活跃记录

当C程序中调用了一个函数时,中会分配一块空间来保存与这个调用相关的信息。每一个调用都被当做是活跃的。栈上的那块存储空间称为活跃记录,或者称为栈帧

栈帧由5个区域组成(见图3-2b):

  1)输入参数:传递到活跃记录中的参数

  2)返回值空间

  3计算表达式时用到的临时存储空间

  4函数调用时保存的状态信息

  5)输出参数:传递给在活跃记录中调用的函数所使用的参数。

一个活跃记录中的输出参数就成为栈中下一个活跃记录的输入参数。函数调用产生的活跃记录将一直存在于中直到这个函数调用结束。

回到示例3-1,考虑一下当计算4!时栈中都发生了些什么。

初始调用fact会在栈中产生一个活跃记录,输入参数n=4(见图3-3,第1步)。

由于这个调用没有满足函数的终止条件,因此fact将继续以n=3为参数递归调用。这将在栈上创建另一个活跃记录,但这次输入参数(见图3-3,第2步)。这里,n=3也是第一个活跃期中的输出参数,因为正是在第一个活跃期内调用fact产生了第二个活跃期。

这个过程将一直继续,直到n的值变为1,此时满足终止条件,fact将返回1(见图3-3,第4步)。

图3-3:递归计算4!时的C程序的栈

是用来存储函数调用信息的绝好方案,这归功于其后进先出的特点满足了函数调用和返回的顺序。然而,使用栈也有一些缺点。

  1)栈维护了每个函数调用的信息直到函数返回后才释放,占用空间大,尤其是在程序中递归调用很多的情况下。

  2)因有大量的信息需保存和恢复,故生成和销毁活跃记录需要耗费一定的时间。

解决方法:可以采用一种称为尾递归的特殊递归方式来避免前面提到的这些缺点。

2、尾递归

若一个函数中所有递归形式的调用都出现在函数的末尾,则称该递归函数是尾递归的

当递归调用是整个函数体中最后执行的语句,且它的返回值不属于表达式的一部分时,该递归调用就是尾递归

尾递归函数的特点是:在回归过程中不用做任何操作,大多数现代的编译器会利用该特点自动生成优化的代码。

当编译器检测到一个函数调用是尾递归时,它就覆盖当前的活跃记录,而不是在栈中去创建一个新的,从而将所使用的栈空间大大缩减,这使得实际的运行效率会变得更高。因此,只要有可能我们就需要将递归函数写成尾递归的形式。

之前对计算n!的定义:在每个活跃期计算n倍的(n-1)!的值,让n=n-1并持续这个过程直到n=1为止。这种定义不是尾递归的,因为每个活跃期的返回值都依赖于用n乘以下一个活跃期的返回值,因此每次调用产生的栈帧将不得不保存在栈上直到下一个子调用的返回值确定。

以尾递归的形式来定义计算n!的过程,函数可定义为以下形式:

图3-4说明了用尾递归计算4!的过程。

注意在回归的过程中不需要做任何操作,这是所有尾递归函数的标志。

图3-4:以尾递归的方式计算4!
示例3-2:以尾递归的形式计算阶乘的一个函数实现
/* facttail.c */
#include "facttail.h"
/* facttail */
int facttail(int n, int a) {
/* Compute a factorial in a tail-recursive manner. */
if (n < )
return ;
else if (n == )
return ;
else if (n == )
return a;
else
return facttail(n - , n * a);
}

图3-5:以尾递归形式计算4!时栈的情况

3、问与答

问:以下递归定义中有错误,请指正。归并排序将一组数据一分为二,然后分别将两份数据各自再进行分半处理,一直持续这个过程直到每一份都只含一个元素。然后在回归过程中完成各份数据的合并最终产生一个有序的集合。

答:该定义的问题在于当n的初始值大于0时将永远无法满足终止条件n=0。为了解决问题,需要一个满足要求的终止条件。n-1这个条件就能很好满足,这意味着也要修改函数中的第二个条件。合适的递归定义应该是这样的:

问:以递归的思想描述一种求解整数质因子的方法。分析该方法是否是尾递归的并解释原因。

答:递归是一种求解整数质因子的很自然的方法,因为因子分解无非就是不断地解决同样的问题。每当确定了一个因子,剩余因子的集合就变得越来越小。针对这个问题的递归方法可以定义为如下式子:

这个定义的意思是说:为了递归地确定整数n的质因子,先确定它的最小质因子i并把它记录到集合P中,然后对整数n=n/i重复这个过程直到n本身成为质数为止,这就是终止条件。这个定义是尾递归的,因为在回归过程中不需要做任何处理,如图3-6所示。

图3-6:以尾递归的方式计算整数2409的质因子

问:思考当执行递归函数时栈的使用情况,当递归过程的递推阶段永远不会终止时会出现什么情况?

答:如果递归函数的终止条件永远得不到满足,最终栈的增长会超过可接受的值,程序会因为栈溢出而终止运行。当程序执行时,一个称为帧指针的特殊指针会寻址栈顶的帧。正是栈指针指向实际的栈顶(即,下一个栈帧将被压入的位置。因此,虽然某个系统可能使用来判断栈溢出,但是它可能是通常会使用的栈指针。)

算法精解(C语言描述) 第3章 读书笔记的更多相关文章

  1. 机器学习|线性回归算法详解 (Python 语言描述)

    原文地址 ? 传送门 线性回归 线性回归是一种较为简单,但十分重要的机器学习方法.掌握线性的原理及求解方法,是深入了解线性回归的基本要求.除此之外,线性回归也是监督学习回归部分的基石. 线性回归介绍 ...

  2. GC算法精解(五分钟让你彻底明白标记/清除算法)

    GC算法精解(五分钟让你彻底明白标记/清除算法) 相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底 ...

  3. GC算法精解(五分钟教你终极算法---分代搜集算法)

    GC算法精解(五分钟教你终极算法---分代搜集算法) 引言 何为终极算法? 其实就是现在的JVM采用的算法,并非真正的终极.说不定若干年以后,还会有新的终极算法,而且几乎是一定会有,因为LZ相信高人们 ...

  4. [转帖]算法精解:DAG有向无环图

    算法精解:DAG有向无环图 https://www.cnblogs.com/Evsward/p/dag.html DAG是公认的下一代区块链的标志.本文从算法基础去研究分析DAG算法,以及它是如何运用 ...

  5. 数据结构与算法分析——C语言描述 第三章的单链表

    数据结构与算法分析--C语言描述 第三章的单链表 很基础的东西.走一遍流程.有人说学编程最简单最笨的方法就是把书上的代码敲一遍.这个我是头文件是照抄的..c源文件自己实现. list.h typede ...

  6. 【2018.08.13 C与C++基础】C++语言的设计与演化读书笔记

    先占坑 老实说看这本书的时候,有很多地方都很迷糊,但却说不清楚问题到底在哪里,只能和Effective C++联系起来,更深层次的东西就想不到了. 链接: https://blog.csdn.net/ ...

  7. 算法精解(C语言描述) 第4章 读书笔记

    第4章 算法分析 1.最坏情况分析 评判算法性能的三种情况:最佳情况.平均情况.最坏情况. 为何要做最坏情况分析: 2.O表示法 需关注当算法处理的数据量变得无穷大时,算法性能将趋近一个什么样的值.一 ...

  8. 算法精解(C语言描述) 第5章 读书笔记

    第5章 5.1 单链表 /* -------------------------------- list.h -------------------------------- */ #ifndef L ...

  9. JVM内存管理------GC算法精解(复制算法与标记/整理算法)

    本次LZ和各位分享GC最后两种算法,复制算法以及标记/整理算法.上一章在讲解标记/清除算法时已经提到过,这两种算法都是在此基础上演化而来的,究竟这两种算法优化了之前标记/清除算法的哪些问题呢? 复制算 ...

随机推荐

  1. Unix/Linux环境C编程入门教程(19)Red Hat Entetprise Linux 7.0环境搭建

    位架构,包括英特尔X-86_64.Power和s390.动态定时能力将降低内核内部中断数量,Open vSwitch 2.0功能可调节虚拟机之间的流量.RHEL 7中默认的文件系统是XFS,包含了一个 ...

  2. Eclipse安装Weblogic插件

    1.启动Eclipse,打开window>References>server>Runtime server. 2.点击“add”按钮,添加新的web服务器. 注:上图Oracle是W ...

  3. JavaWeb学习—Servlet

    1.什么是Servlet Servlet是一个继承HttpServlet类的Java类 Servlet必须部署在web服务器端,用来处理客户端的请求 2.Servlet运行过程 Web Client ...

  4. [ACM] hdu 2191 珍惜如今,感恩生活 (多重背包)

    Problem Description 急!灾区的食物依旧短缺! 为了拯救灾区同胞的生命,心系灾区同胞的你准备自己採购一些粮食支援灾区,如今如果你一共同拥有资金n元,而市场有m种大米,每种大米都是袋装 ...

  5. 使用Vitamio打造自己的Android万能播放器(4)——本地播放(快捷搜索、数据存储)

    前言 关键字:Vitamio.VPlayer.Android播放器.Android影音.Android开源播放器 本章节把Android万能播放器本地播放的主要功能(缓存播放列表和A-Z快速查询功能) ...

  6. TCP协议三次握手过程分析

    TCP(Transmission Control Protocol) 传输控制协议 TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接: 位码即tcp标志位,有6种标 ...

  7. node.js(八) 有趣的东西才开始哦

    ### Express介绍 npm提供了大量的第三方模块,其中不乏许多Web框架,比如我们本章节要讲述的一个轻量级的Web框架 ——— Express. Express是一个简洁.灵活的node.js ...

  8. mysql-5.7.10-winx64 MySQL服务无法启动,服务没有报告任何错误的解决办法

      总结报错原因:在my.init文件下新增data目录(datadir = F:\mysqldata ) 最新解压版本的mysql 解压安装的时候报错D:\mysql\mysql-5.7.10-wi ...

  9. C# List<T>中Select List Distinct()去重复

    List<ModelJD> data = myDalJD.GetAllDataList(); List<string> list= new List<string> ...

  10. QF——OC内存管理详解

    堆的内存管理: 我们所说的内存管理,其实就是堆的内存管理.因为栈的内存会自动回收,堆的内存需要我们手动回收. 栈中一般存储的是基本数据类型变量和指向对象的指针(对象的引用),而真实的对象存储在堆中.因 ...