递归

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

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. BZOJ 1901 洛谷 P2617 ZOJ 2112 Dynamic Rankings

    以下时空限制来自zoj Time limit 10000 ms Memory limit 32768 kB OS Linux Source Online Contest of Christopher' ...

  2. (45)FreeRTOS学习之二

    一:架构概述 FreeRTOS是一个相对较小的应用程序.最小化的FreeRTOS内核仅包括3个(.c)文件和少数头文件,总共不到9000行代码,还包括了注释和空行.一个典型的编译后(二进制)代码映像小 ...

  3. [BZOJ4804]欧拉心算:线性筛+莫比乌斯反演

    分析 关于这道题套路到不能再套路了没什么好说的,其实发这篇博客的目的只是为了贴一个线性筛的模板. 代码 #include <bits/stdc++.h> #define rin(i,a,b ...

  4. object-fit 用于图片适应

    参考文章: object-fit

  5. djangle中模板系统的使用

    django相关的命令行命令: 创建一个djaongo的应用:在已经创建号的应用文件夹中运行:django-admin.py startproject projectName 开启系统自带的服务器在网 ...

  6. 打开远程桌面时总提示无法打开连接文件default.rdp

    删除C:\Users\Administrator\Documents\default.rdp,再启动远程就好了 http://www.chahushequ.com/read-topic-94-2fa9 ...

  7. 2、maven仓库位置设置

    根据我们maven的安装目录找到config文件夹,并找到下面的setting.xml文件,在该文件里面添加下面的代码: <localRepository>F:\apache-maven- ...

  8. ES6实现数组去重

    ES6 提供了新的数据结构 Set.它类似于数组,但是成员的值都是唯一的,没有重复的值. Array.from方法可以将 Set 结构转为数组. 扩展运算符内部调用的是数据结构的 Iterator 接 ...

  9. jest 的 coverage 提示 unknown 的解决方案

    概述 这几天玩 jest ,我在运行单元测试之后 coverage 总是显示 unknown,花了很多时间排查原因,最后终于想明白了,记录下来,供以后开发时参考,相信对其他人也有用. coverage ...

  10. Stream介绍

    一.Stream介绍 现在有这样的需求:有个菜单list,菜单里面非常多的食物列表,只选取小于400卡路里的并且按照卡路里排序,然后只想知道对应的食物名字. 代码: package com.cy.ja ...