递归

当一个函数调用它自己来定义时称它为递归函数。(什么叫它自己调用它自己呢?)

1.1、引出递归

从一个简单的问题考虑递归,求0,1,2, 3,4,5......n的和。

首先定义一个求和公式:sum(n);

显然对于(n > 0): sum(n) = sum(n - 1) + n ;

​ (n = 0 ) : sum(0) = 0;

​ 成立。

将上述公式翻译成C++函数:

unsigned int sum(unsigned int n)
{
if(0 == n)
{
return 0; //基准情况(递归的出口),sum不能一直调用它自己吧,总归要有一个出口结束递归吧
}
else
{
return sum(n - 1) + n; //sum(unsigned int)调用了它自己
}
}

假设 n = 5 分析一下计算过程:

sum(5) = sum(4) + 5;

sum(4) = sum(3) + 4;

sum(3) = sum(2) + 3;

sum(2) = sum(1) + 2;

sum(1) = sum(0) + 1;

sum(0) = 0; 当sum(0)时,sum()不再调用它自己,作为递归的出口结束递归。

假设没有n = 0, sum(0) = 0 这个基准情况作为递归的出口跳出递归,递归就会一直递归下去,没完没了直至崩溃。因此递归函数必须有一个基准情况作为递归出口

1.2、失败的递归

给出一个所谓的递归函数:

int bad(unsigned int n)
{
if(0 == n)
{
return 0;
}
else
{
return bad(n/3 + 1) + n - 1;
}
}

分析一下以上函数,函数给出了 n = 0 的情况作为递归的出口,看似没什么问题。

还是假设n = 5;

bad(5) : 调用bad(5/3 + 1), 即bad(2);

bad(2) : 调用bad(2/3 + 1), 即bad(1);

bad(1) : 调用bad(1/3 + 1), 即bad(1);

bad(1) : 调用bad(1/3 + 1), 即bad(1)..........

bad(1)一直调用bad(1), 一直调用到程序崩溃。很明显bad()函数定义虽然给出了 n = 0 作为递归出口,但是bad()函数根本不会推进到n = 0 的这种情况。因此递归调用必须总能够朝着产生基准情况(递归出口)的方向推进

1.3、递归和归纳

考虑一个问题:现在需要将一个正整数 n 打印出来,但是I/O给出的函数接口(printDigit)只能处理单个数字(即n < 10)。

我们随便假设一个n值:n = 2019,那么单个数字打印的顺序就是2, 0, 1, 9。换句话说,9是最后一个打印的,在打印9之前要先打印201,即先打印“201”,再打印“9”;依次类推对于“201”先打印“20”,再打印“1”;对于“20”先打印“2”,再打印“0”;对于2已经是单个数字,可以直接打印了, 不需要再划分,再递归了,也就是说单个数字n < 10即为递归的出口。

我们按上述思路细致的分析一下:

对2019分成2部分: 201 = 2019 / 10; 9 = 2019 % 10;

对201分成2部分:20 = 201 / 10; 1 = 201 % 10;

对20分成2部分:2 = 20 / 10; 0 = 20 % 10;

对于 2 满足 n < 10 的条件,不再递归,直接打印。

现在递归已经很明显了,尝试编写一下代码:

//假设printDigit((unsigned int n)如下,
void printDigit(unsigned int n)
{
std::cout << n;
} void print(unsigned int n)
{
if(n >= 10)
{
print(n / 10);
}
printDigit(n % 10);
}

代码编写好了,现在需要证明以下代码是否正确:对于n >= 0,数的递归打印算法总是正确的。

证明:用k表示数字n的包含单个数字的个数。当k = 1,即 n < 10 时,很明显程序是正确的,因为它不需要递归,print()只调用一次printDigit(), 不调用它自己。然后假设print()对于所有k位数都能正常工作,任何k + 1位的数字n都可以通过它的前k位的数字和最低1位数字来表示。前k 位的数字恰好是[ n / 10], 归纳假设它能正常工作,而最低1位数字是[ n % 10],因此该程序能够正确的打印出任意k + 1位。于是根据归纳法[1],所有数字都能被正确打印出来。

由以上实例总结可以出一条递归的设计法则:假设所有递归调用都能运行。

1.4、递归的合成效益法则

用递归实现一个斐波那契数列:

//斐波纳契数列:1、1、2、3、5、8、13、21、34
int f(int n)
{
if(n < 1)
{
return 0;
}
else if(n <= 2)
{
return 1;
} return f(n-1) + f(n-2); }

假设n = 8, 函数调用f(8), 递归调用如下图:

graph TB
8-->7;
7-->6;
6-->5;
5-->4;
4-->3;
3-->2;
8-->id0(6);
id0(6)-->id1(5);
id1(5)-->id2(4);
id2(4)-->id3(3);
id3(3)-->id4(2);
7-->id5(5);
id5(5)-->id6(4);
id6(4)-->id7(3);
id7(3)-->id8(2);
6-->id9(4);
id9(4)-->id10(3);
id10(3)-->id11(2);
5-->id12(3);
id12(3)-->id13(2);
4-->id14(2);
3-->id15(1);
id12(3)-->id16(1);
id9(4)-->id17(2);
id10(3)-->id18(1);
id5(5)-->id19(3);
id19(3)-->id20(2);
id19(3)-->id21(1);
id6(4)-->id22(2);
id7(3)-->id23(1);
id0(6)-->id24(4);
id24(4)-->id25(3);
id24(4)-->id28(2);
id25(3)-->id26(2);
id25(3)-->id27(1);
id1(5)-->id29(3);
id29(3)-->id30(2);
id29(3)-->id31(1);
id2(4)-->id32(2);
id3(3)-->id33(1);

由上图我们不厌其烦的数一下:

n = 1时,f()调用1次;

n = 2时,f()调用1次;

n = 3时,f()调用3次;

n = 4时,f()调用5次;

n = 5时,f()调用9次;

n = 6时,f()调用15次;

n = 7时,f()调用25次;

n = 8时,f()调用41次;

增长的是不是太快了,在f()里加一个计数器测试一下,可以看到在n = 30 的时候,f()的调用次数大约在160万。

究其原因,是因为我们在求解的过程时,重复了大量的计算过程, 在n = 8 的时候单单是f(3)就重复调用了8次。

由上我们可以得出一个结论:在求解一个问题的同一实例时,在不同的递归中做重复性的工作,对资源的消耗可能是灾难性的。

最后归纳一下要牢记的递归四条基本法则:

  1. 基准情形。必须总有某些基准情况,它无须递归就能求解,即递归必须有出口。
  2. 不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解状态朝基准情形的方向推进。
  3. 设计法则。假设所有的递归调用都能运行。
  4. 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性的工作。

  1. 1、证明当n= 1时命题成立。2、假设n=m时命题成立,那么可以推导出在n=m+1时命题也成立。(m代表任意自然数)。3、归纳结论。 ↩︎

C/C++ 递归的更多相关文章

  1. .NET 基础 一步步 一幕幕[面向对象之方法、方法的重载、方法的重写、方法的递归]

    方法.方法的重载.方法的重写.方法的递归 方法: 将一堆代码进行重用的一种机制. 语法: [访问修饰符] 返回类型 <方法名>(参数列表){ 方法主体: } 返回值类型:如果不需要写返回值 ...

  2. 算法笔记_013:汉诺塔问题(Java递归法和非递归法)

    目录 1 问题描述 2 解决方案  2.1 递归法 2.2 非递归法 1 问题描述 Simulate the movement of the Towers of Hanoi Puzzle; Bonus ...

  3. Android 算法 关于递归和二分法的小算法

     // 1. 实现一个函数,在一个有序整型数组中二分查找出指定的值,找到则返回该值的位置,找不到返回 -1. package demo; public class Mytest { public st ...

  4. 二叉树的递归实现(java)

    这里演示的二叉树为3层. 递归实现,先构造出一个root节点,先判断左子节点是否为空,为空则构造左子节点,否则进入下一步判断右子节点是否为空,为空则构造右子节点. 利用层数控制迭代次数. 依次递归第二 ...

  5. 递归实现n(经典的8皇后问题)皇后的问题

    问题描述:八皇后问题是一个以国际象棋为背景的问题:如何能够在8×8的国际象棋棋盘上放置八个皇后, 使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行.纵行或斜线上 ...

  6. C语言用分别用递归和循环求数字的阶乘的方法

    以下代码均为 自己 实现,嘻嘻! 参考文章:http://blog.csdn.net/talk_8/article/details/46289683 循环法 int CalFactorial(int ...

  7. C#递归解决汉诺塔问题(Hanoi)

    using System;using System.Collections.Generic;using System.Linq;using System.Text; namespace MyExamp ...

  8. Java之递归求和的两张方法

    方法一: package com.smbea.demo; public class Student { private int sum = 0; /** * 递归求和 * @param num */ ...

  9. C#语言基础——递归

    递归 一.概念conception: 函数体内调用本函数自身,直到符合某一条件不再继续调用. 二.应满足条件factor: (1)有反复执行的过程(调用自身): (2)有跳出反复执行过程的条件(函数出 ...

  10. SQL Server封闭掉 触发器递归

    SQL Server关闭掉 触发器递归SQL Server  是有一个开关, 可以关闭掉 触发器递归的.EXEC sp_dboption '数据库名字', 'recursive triggers', ...

随机推荐

  1. js能否上传文件夹

    文件夹上传:从前端到后端 文件上传是 Web 开发肯定会碰到的问题,而文件夹上传则更加难缠.网上关于文件夹上传的资料多集中在前端,缺少对于后端的关注,然后讲某个后端框架文件上传的文章又不会涉及文件夹. ...

  2. 2018百度之星初赛B轮 p1m2

    p1m2 Accepts: 954 Submissions: 4063 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 131072/1310 ...

  3. VTK 编译过程中出现的hdf5长度(I64)错误解决办法

    最近在使用vtk和cuda做大规模图像处理方面的问题研究,在编译vtk的过程中发现第三方库hdf5不能够解决I64长度的探测识别问题.为了节约大家的时间,现在把我经过实践得到的解决方案共享出来,这里要 ...

  4. NOIP2018初赛划水记

    尽管初赛好像没什么好写的,但还是以此作为我的第一篇游记吧. 前夜 本来以为初赛水(去年分数线36来着),几周前做了一套普及组的卷子90多分感觉良好就没管了(不明白那些专门花时间看初赛书的人),结果Mr ...

  5. c++中的类(构造函数,析构函数的执行顺序)

    类对象的初始化顺序 新对象的生成经历初始化阶段(初始化列表显式或者隐式的完成<这部分有点像java里面的初始化块>)——> 构造函数体赋值两个阶段 1,类对象初始化的顺序(对于没有父 ...

  6. (转)WebRTC信令控制与STUN/TURN服务器搭建

    转:https://rtcdeveloper.com/t/topic/13742 本文将向大家介绍两个方面的知识: WebRTC信令控制 STUN/TURN服务器的搭建 在前面的文章中已经向大家介绍了 ...

  7. webpack的安装和运行

    webpack依赖于node,为了可以正靠运行,必须依赖node环境.node环境为了可以真正的执行很多代码,必须其中包含各种依赖的包npm工县(node packages manager)所以在进行 ...

  8. 快速入门分布式消息队列之 RabbitMQ(3)

    目录 目录 前文列表 前言 通道 Channel 一个基本的生产者消费者实现 消费者 生产者 运行结果 应用预取计数 应用 ACK 机制 最后 前文列表 快速入门分布式消息队列之 RabbitMQ(1 ...

  9. NIO组件之channel

    Java NIO指的是new IO ,相对OIO,也称non-blocking IO,对应四种基本IO类型中的IO多路复用,主要有有三大核心组件,Channel(管道),Buffer(缓冲区),sel ...

  10. 测开之路一百五十二:基于jquery的ajax实现之load、get、ajax

    ajax除了用原生的js实现之外,也可以使用jquery实现,而且用jquery更方便 看一个简单的示例,保留上一篇的content路由和html,实现上一篇一样的功能,点击获取内容,局部刷新 准备一 ...