目录

前言

一、从阶乘引入

二、递归模板

1.递归函数模板

2.举例分析

三、从数学归纳法理解递归

四、更多递归实例

1.用递归方法编程计算Fibonacci数列

题目分析

程序

2.汉诺塔(Hanoi)问题

题目分析

程序

3.转置链表

题目分析

程序

总结


前言

如果一个对象部分地由它自己组成或按它自己定义,则我们称它是递归(Recursive)的。在日常生活中,字典就是一个递归问题的典型实例,字典中的任何一个词汇都是由“其他词汇”解释或定义的,但是“其他词汇”在被定义或解释时又会间接或直接地用到那些由它们定义的词。在数学中,数学归纳法也是递归的一种体现。


一、从阶乘引入

初学编程时,计算正整数n的阶乘是利用阶乘的定义即n!=n*(n-1)*(n-2)*...*2*1来计算的。代码如下:

#include <stdio.h>
int main(void)
{int i, n;long p = 1;printf("Please enter n:");scanf("%d", &n);for (i = 1; i <= n; i++){p = p * i;printf("%d! = %ld\n",i, p);}return 0;
}

其实,还可以将n!=n*(n-1)*(n-2)*...*2*1写成n!=n*(n-1)!,即利用(n-1)!来计算n!,同理再用(n-2)!来计算(n-1)!,即(n-1)!=(n-1)*(n-2)!,以此类推,直到用1!=1逆向递推出2!,再依次递推出3!,4!,...,n!时为止。这说明阶乘是可以根据其自身来定义的问题,因此阶乘也是可递归求解的典型实例。这个递归问题可用如下的公式来表示:

下面采用递归方法来实现阶乘的计算:

#include<stdio.h>
long Fact(int n);
int main(void)
{int n;long result;printf("Input n:");scanf("%d", &n);result = Fact(n);//调用递归函数Fact()计算n!if (result == -1)//处理非法数据printf("n < 0, data error!\n");else//输出n!值printf("%d! = %ld\n", n, result);return 0;
}
//函数功能:用递归法计算n!,当n >= 0时返回n!,否则返回-1
long Fact(int n)
{if (n < 0)//处理非法数据return -1;else if (n == 0 || n == 1)//基线情况,即递归终止条件return 1;else//一般情况return (n * Fact(n-1));//递归调用,利用(n-1)!计算n!
}

可见,递归是一种可根据其自身来定义或求解问题的编程技术,它是通过将问题逐步分解为与原始问题类似的更小规模的子问题来解决问题的,即将一个复杂问题逐步简化并最终转化为一个最简单的问题,最简单的问题的解决,就意味着整个问题的解决。显然对于具体的问题首先需要关注的是:最简单的问题是什么?对于本例,n=0或1就是计算n!的最简单的问题。当函数递归调用到最简形式,即当n=1时,递归调用结束,然后逐级将函数返回值返回给上一级调用者。

二、递归模板

1.递归函数模板

一个递归函数必须包含如下两个部分:

(1)由其自身定义的与原始问题类似的更小规模的子问题,它使递归过程将持续进行,称为一般情况(General case)

(2)递归调用的最简形式,它是一个能够用来结束递归调用过程的条件,通常称为基线情况(Base case)

代码如下:

返回值类型 Recursive(类型 形式参数1, 类型 形式参数2,……)
{if (递归终止条件)//基本条件控制递归调用结束return 递归公式的初值;else//一般条件控制递归调用向基本条件转化return 递归函数调用返回的结果值;
}

像这种“在函数内直接或间接地调用自己”的函数调用,就称为递归调用(Recursive Call),这样的函数则称为递归函数(Recursive Function)

2.举例分析

以第一目为例,基线情况0!=11!=1一般情况则是将n!表示成n乘以(n-1)!,如第一目代码中在调用函数Fact()计算n!的过程中又调用了函数Fact()来计算(n-1)!。例如要计算3!,需要经历如下步骤:

(1)在main函数中调用Fact(3)。

(2)为了计算3!,需要先调用函数Fact(3-1)计算2!。

(3)为了计算2!,需要先调用函数Fact(2-1)计算1!.

(4)计算1!时,递归终止,返回1作为1!的计算结果。

(5)返回到(3)中,利用1!=1,求出2!=2×1!=2×1=2,返回2作为2!的计算结果。

(6)返回到(2)中,利用2!=2,求出3!=3×2!=3×2=6。

(7)返回到main函数中,得出Fact(3)=6。

三、从数学归纳法理解递归

有关数学归纳法的原理,详见《人民教育出版社数学选择性必修第二册(A版)》第四章 数列 4.4*数学归纳法[2]。

下面给出数学归纳法的定义:

一般地,证明一个与正整数有关的命题,可按下列步骤进行:

(1)(归纳奠基)证明当时命题成立;

(2)(归纳递推)以“当时命题成立”为条件,推出“当时命题也成立”.

只要完成这两个步骤,就可以断定命题对从开始的所有正整数都成立,这种证明方法称为数学归纳法(mathematical induction).

数学归纳法的核心就在于,只要我们能证明当n=1时等式成立(基线情况),且当n=k≥1时成立能够推出n=k+1时成立(一般情况),就能说明n≥1时成立。因为只要n=1成立,n=2就成立;n=2成立,n=3就成立;……;n=k成立,n=k+1就成立。

其实递归也蕴含了数学归纳法的思想:只要给出基线情况和一般情况的递推关系,就能得到基线情况以后的所有情况。因为知道了基线情况,通过递推关系就能得出基线情况后一级的情况;知道了基线情况后一级的情况,通过递推关系就能得出再后一级的情况……因此我们在编程时只需要关心:基线情况是什么,一般情况是什么。至于复杂的函数调用关系与返回关系,可以不用理会。

四、更多递归实例

1.用递归方法编程计算Fibonacci数列

题目分析

首先我们需要找出基线情况:fib(1)=fib(2)=1;然后找出一般情况:fib(n)=fib(n-1)+fib(n-2)(n>2),即:

程序

#include<stdio.h>
long Fib(int n);
int main(void)
{int n, i, x;printf("Input n:");scanf("%d", &n);for (i = 1; i <= n; i++){x = Fib(i);//调用递归函数Fib()计算Fibonacci数列的第n项printf("Fib(%d) = %d\n", i, x);}return 0;
}
//函数功能:用递归法计算Fibonacci数列中的第n项的值
long Fib(int n)
{if (n == 1)return 1;//基线情况else if (n == 2)return 1;//基线情况elsereturn (Fib(n-1)+Fib(n-2));//一般情况
}

2.汉诺塔(Hanoi)问题

如图,A杆上从下往上按大小顺序摞着n片黄金圆盘,规定每次只能移动一个圆盘,在小圆盘上不能放大圆盘,则把圆盘从下开始按大小顺序重新摆放到第二根上需移动多少次?

移动前:

移动后:


题目分析

首先我们需要找出基线情况:假设A杆上只有2个圆盘,即汉诺塔有2层,n=2,我们的移动步骤是先将1号圆盘从A移到C,再将2号圆盘从A移到B,最后将1号圆盘从C移到B。

移动前:

第一步:

第二步:

第三步:

然后找出一般情况:我们可以将n个圆盘分为两部分“上面n-1个圆盘”看成一个整体,于是我们的移动步骤为先将n-1个圆盘从A移到C,再将n号圆盘从A移到B,最后将n-1个圆盘从C移到B。

而把n-1个圆盘从A移到C就相当于先将n-2个圆盘从A移到B,再将n-1号圆盘从A移到C,最后将n-2个圆盘从B移到C……一直到只剩下两个圆盘,即回到基线情况。

程序

首先我们需要设计一个函数Hanoi(),Hanoi(n, 'A', 'B', 'C')表示将n个圆盘借助于C由A移动到B;接着我们要设计一个函数Move(),调用Move(n, 'A', 'B')可输出一个语句:Move n: from 'A' to 'B'.

于是得到代码如下:

#include <stdio.h>
void Hanoi(int n, char a, char b, char c);
void Move(int n, char a, char b);
int main()
{int n;printf("Input the number of disks:");scanf("%d", &n);printf("Steps of moving %d disks from A to B by means of C:\n", n);Hanoi(n, 'A', 'B', 'C');/*调用递归函数Hanoi()将n个圆盘借助于C由A移动到B*/return 0;
}
/* 函数功能:用递归方法将n个圆盘借助c从a移到b */
void Hanoi(int n, char a, char b, char c)
{if (n == 2){Move(n-1, a, c);Move(n, a, b);Move(n-1, c, b);}else{Hanoi(n-1, a, c, b);Move(n, a, b);Hanoi(n-1,c, b, a);}
}
/* 函数功能:将第n个圆盘从a移到b */
void Move(int n, char a, char b)
{printf("Move %d: from %c to %c\n", n, a, b);
}

这个程序只能实现n≥2的汉诺塔问题的求解。然而,进一步地,其实n=2也包含在n≥2的一般情况里,即可以把基线条件设成n=1,即A杆上只有一个圆盘,汉诺塔只有1层,移动操作便是将圆盘从A移到B。因此可以改写函数Hanoi()如下:

void Hanoi(int n, char a, char b, char c)
{if (n == 1){Move(n, a, b);}else{Hanoi(n-1, a, c, b);Move(n, a, b);Hanoi(n-1,c, b, a);}
}

3.转置链表

定义函数,原型为struct link *Reverse(struct link *head);,功能为转置传入的链表顺序。函数不能返回一个新建的链表,必须通过改变原来的链表来实现功能,最后的返回值为转置后的链表的头节点的地址值。[3]

注意:不能通过简单地改变链表的数据域来转置链表;相反,需要改变链表的指针域

链表节点的定义如下:

struct link
{int data;struct link *next;
};

题目分析

首先我们需要找出基线情况:当链表中只有2个节点时,我们只需新建一个结构体指针变量struct link* newhead;,让newhead指向第2个节点,然后让第2个节点的next指针指向第1个节点,最后让第1个节点的next指针指向NULL,最后返回newhead即可完成链表的转置。

接着我们需要找出一般情况:当链表中有n个节点(n>2)时,可把链表的后n-1个节点看成一个整体(先调用函数Reverse(head->next)使后n-1个节点的链表完成转置),这个整体与第1个节点组成一个2个节点的链表,再按照基线情况的操作,新建一个结构体指针变量struct link* newhead;,让newhead指向后n-1个节点组成的链表的头节点(这里为函数Reverse(head->next)的返回值),然后让head->next->next(第2个节点的next指针)指向头节点head,最后让头节点的next指针指向NULL,最后返回newhead即可完成链表的转置。

程序

struct link *Reverse(struct link *head)
{struct link *newhead = NULL;if (head->next->next == NULL)//基线情况:只有2个节点{newhead = head->next;head->next->next = head;//第2个节点的next指针head->next = NULL;//第1个节点的next指针}else//一般情况{newhead = Reverse(head->next);head->next->next = head;//当前节点的下一个节点的next指针head->next = NULL;//当前节点的next指针}return newhead;
}

这个程序只能完成n≥2个节点的链表的转置。然而,其实当n=2时,可以一并归入n≥2的情况中,于是我们可以把n=1甚至n=0作为基线条件(n=1即只有一个链表,n=0为空链表),修改后的程序如下:

struct link *Reverse(struct link *head)
{if (head == NULL || head->next == NULL)//基线情况:空链表或链表只有1个节点{return head;}else{struct link *newhead = Reverse(head->next);head->next->next = head;//当前节点的下一个节点的next指针head->next = NULL;//当前节点的next指针return newhead;}
}

总结

递归将复杂的情形逐次归结为较简单的情形来计算,一直到归并为最简单的情形为止,但任何递归函数都必须至少有一个基线情况,并且一般情况必须最终能转化为基线情况,否则程序将无限递归下去,导致程序出错。

在编写和阅读递归函数的代码时,我们只需注意两点:1.基线情况是什么?2.假设上一步已经做好了,当前这一步该怎么做?(一般情况的递推关系是什么?)倘若真的想要一行行debug代码,也要假定递归的次数较少,否则将进入无穷无尽的调用与返回之中,会导致程序非常的抽象难懂。

从上述的例子可以看出,用递归编写程序更直观、更清晰、可读性更好(若不深究函数之间复杂的调用与返回关系),更逼近数学公式的表示,更能自然地描述问题的逻辑,尤其适合非数值计算领域,如Hanoi塔、骑士游历、八皇后问题。但是从程序运行效率来看,递归函数在每次递归调用时都需要进行参数传递、现场保护等操作,增加了函数调用的时空开销,导致递归程序的时空效率偏低


参考文献:

[1]苏小红 赵玲玲 孙志岗 王宇颖 等编著 蒋宗礼 主审,C语言程序设计(第4版),高等教育出版社,P109,P158-161.

[2]主编:章建跃 李增沪,副主编:李勇 李海东 李龙才,本册主编:李龙才 周远方,编写人员:李龙才 宋莉莉 张艳娇 周远方 桂思铭 郭慧清,责任编辑:张艳娇,美术编辑:王俊宏,数学选择性必修第二册(A版),人民教育出版社,P44-52

[3]原题为:Define reverse, which takes in a linked list and reverses the order of the links. The function may not return a new list; it must mutate the original list. Return a pointer to the head of the reversed list. You may not just simply exchange the first to reverse the list. On the contrary, you should make change on rest. 题目来源于Ricky_Daxia。

C语言丨函数的递归调用和递归函数相关推荐

  1. 在c语言中允许函数递归调用,c语言允许函数的递归调用吗

    c语言允许函数的递归调用吗 允许.C语言中的函数直接或间接调用自己的过程叫递归. 一.递归的两个必要条件 1.存在限制条件,当满足这个条件时,递归便不再继续. 2.每次递归调用之后越来越接近这个限制条 ...

  2. C语言:函数的递归调用

    函数的递归调用:一个函数在它的函数体内,直接或者间接地调用了他本身. 直接递归调用:函数直接调用自身.                              间接递归调用:函数间接调用自身. 如下 ...

  3. c语言复习--函数的递归调用

    当一个程序自己调用自己时,就形成了递归现象.(可参照数学中阶乘的运算,每一步都需要前一步的值) 函数A直接调用函数A为直接递归,函数A调用函数B,函数B又调用函数A,称为间接递归. 写递归程序的关键在 ...

  4. C 语言:函数的递归调用

    题目概述:递归法求n! 编程: #include<stdio.h> int main() { int fac(int n); //fac函数声明 int n,y; printf(" ...

  5. C语言函数之递归调用

    提示:本文主要是掌握函数的递归 函数递归 前言 什么是递归 递归的两个必要条件 递归与迭代的关系 递归函数的优缺点 什么时候使用递归 总结 前言 函数是学习C语言的最重要知识点之一,要学好这门编程语言 ...

  6. 求222222c语言递归函数,C语言ch函数的嵌套调用和递归调用.pptx

    C语言ch函数的嵌套调用和递归调用.pptx 2012/10/221 上一节我们学到了 n函数的定义 n形参 n函数的声明 n函数的调用 n实参 n函数的调用过程 n局部变量(包括形参)何时分配内容. ...

  7. python入门day16——函数的递归调用、二分法、三元表达式、匿名函数

    文章目录 函数的递归调用 递归调用应该分为两个阶段 二分法 三元表达式 匿名函数 函数的递归调用 函数的递归调用:就是在调用一个函数的过程中又直接或间接地调用自己 示例1:直接调用自己 def foo ...

  8. <C语言> 函数与递归

    函数 1.函数的分类 库函数 自定义函数 1.1 库函数 C语言提供了许多库函数(library functions)来简化开发过程并提供常用功能的实现.库函数是预先编写好的函数,可以通过调用这些函数 ...

  9. Python函数的递归调用

    一:递归的定义 函数的递归调用:是函数嵌套调用的一种特殊形式 具体是指: 在调用一个函数的过程中又直接或者间接地调用到本身 # 直接调用本身 def f1():print('是我是我还是我')f1() ...

最新文章

  1. 【廖雪峰python入门笔记】列表生成式
  2. 基于Springboot实现旅游网站系统开发
  3. php的主要架构,php运行原理与基本结构
  4. SqlParameter的作用与用法
  5. 【11.5校内测试】【倒计时5天】【DP】【二分+贪心check】【推式子化简+线段树】...
  6. 4位大佬解读:“医疗人工智能、信息化、政策与科研”的新风向与新趋势
  7. 工厂模式,简单工厂模式,抽象工厂模式三者有什么区别
  8. Go 如何利用 Linux 内核的负载均衡能力
  9. Coolite 换肤
  10. python图片分析中央气象台降水_python 画降水量色斑图问题
  11. 良好的Coding习惯,从P3C开始--阿里P3C代码规范扫描插件
  12. Django restframework中Serializer序列化器-用法详解
  13. 几个简单规则改进你的SEO效果
  14. 【AcWing 学习】图论与搜索
  15. java判断全角_Javascript判断日文全角半角长度
  16. premiere输出图像抖动的最终解答--转
  17. 「军民链智合创」科技美学出海 BitCEO比特维度全球CEO发展大会参展台北
  18. 2020暑期腾讯小程序开发训练营结课心得
  19. 搭建简单的Netty开发环境
  20. 小甲鱼python猜题_[Python]小甲鱼Python视频第033课(except)课后题及参考解答

热门文章

  1. window下搭建linux虚拟机
  2. 怎样才算精通Linux
  3. mysql把某一列的数据更新到另一列中(涉及到多张表的数据)
  4. 矩阵分析与应用课后答案——张贤达版本
  5. SQL Server AlwaysOn读写分离配置
  6. Fiddler--QUICKEXER
  7. SUN公司经典linux教材转自http://blog.chinaunix.net/uid-20446831-id-1677336.html
  8. Silverlight 信息显示与编辑控件 示例
  9. 46 - 算法 - Leetcode 168 -26进制 --减一
  10. 微课|中学生可以这样学Python(3.1节):单分支选择结构