美文网首页
递归二总结

递归二总结

作者: 秸秆混凝烧结工程师 | 来源:发表于2021-01-26 20:55 被阅读0次

递归深入理解


笔来自网络记

本篇文章中,让我们深入递归,通过简单地故事,简单地示例以及详细的讲解,一起来了解,如何从普通的函数调用,一步一步转向递归?

1 说在前面的话

前面两节课,我们学习了函数的重要知识,了解了函数的重要概念!在学习今天的新知识之前,我们先复习一下前面讲解的内容。

1 温故知新

1、函数的组成

一个函数,至少需要如下几个部分:

函数类型

函数名

函数体

除此之外,大多数函数还要有函数参数。

2、为何要定义函数

主要出自两方面的考虑:

将多次使用的功能块封装起来,在使用过程中直接调用函数,无需将代码块重写,更加方便简洁。

将不同功能的代码块封装,在其他函数中调用,比全部都写在主函数里结构更加清晰。

3、函数的定义位置

一般来说,函数要定义在函数的调用之前!如果想把详细的定义过程写在后面,需要先在调用之前声明。

这种将声明和定义分开的方式,在信息学中用的比较少,但是在工程项目中,基本上都是分开的。一方面是考虑规范性,一方面是考虑代码的安全性。

4、局部变量与全局变量

局部变量和全局变量在如下几个方面不同:

作用域不同:局部变量的作用域是所在函数中,全局变量的作用域是整个程序;

初始值不同:局部变量的初值为随机值,全局变量的初值为0;

生命周期不同:局部变量的生命周期是所有函数的周期,全局变量的生命周期是整个程序的周期。

5、参数拓展

关于参数我们学习了如下方面的内容:

形参与实参:在函数声明与定义过程中的参数是形参,在函数的具体调用过程中是实参。

函数参数的引用调用:使用符号&,引用调用会改变实参本身的值。

2 老和尚讲故事

“从前有座山,山上有座庙,庙里有个老和尚,老和尚正在讲故事,讲的是:从前有座山,山上有座庙,庙里有个老和尚,老和尚正在讲故事,讲的是:从前有座山,山上有座庙,庙里有个老和尚,老和尚正在讲故事,讲的是:……”。

1 循环?

有人说,这个是循环结构,我只要不断地输出:

#include<iostream>

using namespace std;

void story(){

  cout<<"从前有座山,山上有座庙,庙里有个老和尚,老和尚正在讲故事,讲的是";

}

int main() {

  for(int i=0;i<100;i++){

    story();

  }

  return 0;

}

执行结果如下:

图片

2 递归!

如果用普通方法,我们称之为循环,也没有错!

但是如果是对于我们函数来说,我们称之为递归!

那什么是递归呢?让我们接着往后看吧!

3 递归

前面我们提到递归,这里,我们来一起了解一下递归吧!

1 函数之间的调用

我们定义函数,一般都要在主函数中进行调用,如果我们定义两个函数,一个函数可以调用另外一个函数吗?可以的!比如下面的例子:

#include<iostream>

using namespace std;

void a(){

  cout<<'a'<<endl;

}

void b(){

  a();

  cout<<'b'<<endl;

}

int main() {

  b();

  return 0;

}

我们要注意,要调用一个函数,在调用之前就要先把这个函数声明好。所以下面这种写法是不规范的:

如果我们有三个函数:

#include<iostream>

using namespace std;

void a(){

  cout<<'a'<<endl;

}

void b(){

  a();

  cout<<'b'<<endl;

}

void c(){

  b();

  cout<<'c'<<endl;

}

int main() {

  c();

  return 0;

}

我们在执行c函数的时候,调用b函数,然后执行b函数的时候,调用a函数,当a函数执行完了,就要接着执行b函数剩下的,当b函数执行完,就要接着执行c函数剩下的。

如果我们有多个函数不断这样调用,我们就一层一层的调用进去,每一层执行完,就会回到上一层继续执行,直到返回到最外层执行完毕。

现在再考虑一个问题,函数能不能调用自己呢?当然是可以哒!

我们把上面的代码使用自己调用自己的话,就只需要写一个函数就可以:

#include<iostream>

using namespace std;

void c(){

  cout<<'c'<<endl;

  c();

}

int main() {

  c();

  return 0;

}

对于前面的小故事,我们可以这么写:

#include<iostream>

using namespace std;

int n = 100;

int story(){

  if(n--){

    cout<<"从前有座山,山上有座庙,庙里有个老和尚,老和尚正在讲故事,讲的是";

    return story();

  }

  else return 0;

 

}

int main() {

  story();

  return 0;

}

我们再举个简单的例子,来分析一下:

#include<iostream>

using namespace std;

int output(int a){

  if(a>0){

    cout<<a<<endl;

    return output(--a);

  }

  else return 0;

}

int main() {

  output(5);

  return 0;

}

我们对于自己定义的output函数:

int output(int a){

  if(a>0){

    cout<<a<<endl;

    return output(--a);

  }

  else return 0;

}

我们在output函数里面调用了函数output,先不考虑函数名相同的问题,我们前面说,调用函数之前,我们要先定义该函数。所以,我们这个 函数之前,要先定义一个output函数:

int output(int a){ //a = 4

  if(a>0){

    cout<<a<<endl;

    return output(--a);

  }

  else return 0;

}

int output(int a){ //a = 5

  if(a>0){

    cout<<a<<endl;

    return output(--a); //--a = 4

  }

  else return 0;

}

而对于上面的output函数,也调用了output函数,所以我们也需要再定义一个output函数。以此推下去:

int output(int a){ //a = 0 ,返回0,结束

  if(a>0){

    cout<<a<<endl;

    return output(--a);

  }

  else return 0;

}

int output(int a){ //a = 1

  if(a>0){

    cout<<a<<endl;

    return output(--a); //--a = 0

  }

  else return 0;

}

int output(int a){ //a = 2

  if(a>0){

    cout<<a<<endl;

    return output(--a); //--a = 1

  }

  else return 0;

}

int output(int a){ //a = 3

  if(a>0){

    cout<<a<<endl;

    return output(--a); //--a = 2

  }

  else return 0;

}

int output(int a){ //a = 4

  if(a>0){

    cout<<a<<endl;

    return output(--a); //--a = 3

  }

  else return 0;

}

int output(int a){ //a = 5

  if(a>0){

    cout<<a<<endl;

    return output(--a); //--a = 4

  }

  else return 0;

}

但是我们注意到,这六个函数都是一样的,我们只需要写一个就可以了,但是对于程序来说,他是按照上面的这个程序一直去执行的。

2 递归

递归就是函数自己调用自己!

对于上面的代码,我们可以画一个流程图来表示:

图片

递归有去有回,其实是我们调用函数,就走进函数内部,我们称之为外层函数和内层函数。

执行外层函数的时候,我们调用了内层函数,想继续往下执行,就先要将内层函数执行完毕。而内层函数还有内层函数,我们就需要将更内层的函数执行完……执行完最内层函数,就可以执行它的外层函数,然后执行完毕执行再外层函数,一直到最外层函数,递归结束。

递归的难点在于,递归是自己调用自己,调用的层次比较深之后,很容易出错,所以我们一般在阅读程序的过程中,一般使用注释辅助我们阅读程序:

/* 使用注释,一层一层写进去分析:

output(5)

  if 成立:

    cout<<5<<endl;

    output(4);

      if 成立:

        cout<<4<<endl;

        output(3);

          if 成立:

            cout<<3<<endl;

            output(2);

              if 成立:

                cout<<2<<endl;

                output(1);

                  if 成立:

                    cout<<1<<endl;

                    output(0);

                      if 不成立

                      else return 1;

                  else

              else

          else

      else 

  else 

*/

3 递归案例分析

接下来,让我们通过更多的案例来理解递归吧!

1 顺序输出1-100

题目

使用递归的方式,将1-100输出;

分析

我们尝试从1开始输出,但我们进入函数是从100进入的,所以我们要先不断递去,在归来的时候,再输出。假设我们的输出函数为

show(int num);

那我们在show的后面,添加输出。(如果是逆序输出,就要先输出,后递归。)

图片

也就是如下流程:

我想输出n,我得先把n-1输出;

我想输出n-1,我得先把n-2输出;

我想输出3,我得先把2输出;

我想输出2,我得先把1输出;

我想输出1,我得先把0输出;

0不满足条件,返回。

1输出;

1输出结束,2输出;

2输出结束,3输出;

n-1输出结束,n输出。

代码

根据上面的分析,写代码如下:

#include<iostream>

using namespace std;

int output(int a){

  if(a>0){

    output(a-1);

    cout<<a<<endl;

  }

  else return 0;

}

int main() {

  output(100);

  return 0;

}

2 递归求阶乘

题目

编写一个递归程序,求解n的阶乘,即计算:

n! = n*(n-1)*……*2*1 = n*(n-1)!

其中1! = 1

分析

跟前面一样,只不过我们要把计算的内容换一下,前面是输出,我们这里是将数据乘到一个最终的结果上面去。以5为例流程如下:

我想计算5!,我得先把4!计算出来;

我想计算4!,我得先把3!计算出来;

我想计算3!,我得先把2!计算出来;

我想计算2!,我得先把1!计算出来;

1! = 1,返回;

1!计算完毕,2!是2*1!;

2!计算完毕,3!是3*2!;

3!计算完毕,4!是4*3!;

4!计算完毕,5!是5*4!;

代码

#include<iostream>

using namespace std;

int fac(int n){

  if(n>1) return n*fac(n-1);

  else return 1;

}

int main() {

  cout<<fac(5)<<endl;

  return 0;

}

4 深入理解递归

通过上面的内容,我想大家对递归有一定的了解了,现在让我们来更加深入的理解一下递归吧!

1 递归的理解

递归有两个流程,一个是递去,一个是归来。在递去跟归来之间有一个很重要的就是分界点,也就是递去的结束条件。

也就是说递归有三要素:

递的结束条件

如果没有递的结束条件,程序就陷入死循环。就如同走进一个循环隧道,永远找不到出口。

我们可以用词典去理解递归,我们想要了解一个汉字的含义,就要查汉字的释义,那我们想要了解释义,就要再去查释义中各个汉字的意思。直到查到我们了解的字或词为止,然后我们再返回来一层一层理解,直到理解我们最初的那个汉字的含义。

2 递归与循环

递归其实是另一种形式上的循环,但比循环结构更难理解。

不同点在于:

递归是指函数的反复调用,而普通的循环,只是循环体中代码的重复调用。

在我们自己写代码的过程中,除了一些必要算法是通过递归实现的外,其他的我们尽量使用循环,避免使用递归。这是因为递归存在容易栈溢出、逻辑混乱等问题。

5 习题

我们今天给大家出两道题目:

1 递归求前n项和

使用递归的方式,求1到n的和。

2 递归求斐波那契数列

编写一个递归程序,求斐波那契数列第n项的值。

相关文章

  • 递归二总结

    递归深入理解 笔来自网络记 本篇文章中,让我们深入递归,通过简单地故事,简单地示例以及详细的讲解,一起来了解,如何...

  • 【数据结构】【C#】026-二叉树(BT):🌱遍历介绍

    一、递归遍历: 1、先序遍历:2、中序遍历:3、后续遍历:总结规律: 二、非递归遍历:利用栈来实现 非递归算法实现...

  • 数据结构课程 第八周 遍历二叉树

    存储结构为二叉链表 遍历 先序遍历递归算法 中序遍历递归算法 后序遍历递归算法 总结 时间O(n) 空间(O(n)...

  • Leetcode.124.Binary Tree Maximum

    题目 给定义一个二叉树, 求二叉树的子路径的最大和. 思路 递归. 分别对左右子树递归. 总结 递归求最大值, 需...

  • LeetCode 专题:数组

    知识点总结 二分查找法(二分查找法是弱点)**以及相关的操作:递归实现和非递归实现,floor 和 ceiling...

  • 算法-二叉树算法总结

    二叉树算法总结 1 二叉树的遍历 1.1 前序遍历 递归 迭代 1.2 中序遍历 递归 迭代 1.3 后序遍历 递...

  • 数据结构——树

    有关树的算法题总结 实现二叉树的前序、中序、后序遍历(递归、非递归,mirros方法) 查找后继节点 二叉树的序列...

  • 递归

    本文主要内容有: 1、递归的样子2、递归简介3、递归特点4、递归分析方法5、递归程序模板6、递归程序调试7、总结8...

  • 二叉树的前序、中序、后序遍历(递归、非递归)

    二叉树 前序 递归: 非递归: 中序 递归: 非递归: 层序 递归: 非递归:

  • 常规排序--java实现

    感觉排序考到的频率特别高,这里把排序走总结一下。原理就不啰嗦了,应该烂熟于心。 二分查找 递归实现 非递归实现 选...

网友评论

      本文标题:递归二总结

      本文链接:https://www.haomeiwen.com/subject/jsenzktx.html