备战蓝桥杯,第七章:函数与递归

一.函数是什么?

在数学中,我们其实就学习过函数的概念,比如:一次函数y=kx+b,k和b都是常数,给一个任意的x,就可以得到一个y值。其实在C/C++语言中也引用了函数的概念,有些翻译为:子程序,子程序的翻译比函数更为准确。函数就是一个完成某项特定任务的一小段代码,这段代码是有特殊的写法和调用方法的。其实我们在前面已经碰到函数了。比如:main函数、scanf函数、printf函数、pow函数、sqrt函数。

在C/C++语言中,程序是由若干个小函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数。同时一个函数如果能完成某项特定的任务的话,因为这个函数是可以复用的,就提升了软件开发的效率。如果要完成下面的任务:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int arr[10] = { 0 };
 //打印数组的内容 
 //给数组的元素赋值为1~10 
 //打印数组 
 return 0;
}

如果没有创建函数,我们就可以写出下面的代码一:

cpp 复制代码
//代码1 
#include<iostream>
using namespace std;
int main()
{
 int arr[10] = { 0 };
 //打印数组的内容 
 int i = 0;
 for (i = 0; i < 10; i++)
 {
 cout << arr[i] << " ";
 }
 cout << endl;
 //给数组的元素赋值为1~10 
 for (i = 0; i < 10; i++)
 {
 arr[i] = i + 1;
 }
 //打印数组 
 for (i = 0; i < 10; i++)
 {
 cout << arr[i] << " ";
 }
 cout << endl;
 return 0;
}

如果我们在程序中,创建了函数,就可以在程序逻辑重复的地方复用函数的代码。这样就可以写出效率更高的代码二:

cpp 复制代码
//代码2 
#include<iostream>
using namespace std;
void print_arr(int arr[])
{
 int i = 0;
 for (i = 0; i < 10; i++)
 {
 cout << arr[i] << " ";
 }
 cout << endl;
}
int main()
{
 int arr[10] = { 0 };
 //打印数组的内容 
 print_arr(arr);
 //给数组的元素赋值为1~10 
 for (int i = 0; i < 10; i++)
 {
 arr[i] = i + 1;
 }
 //打印数组 
 print_arr(arr);
 return 0;
}

在代码二中print_arr函数就是我们自定义的函数,该函数的目的是打印出arr数组的内容。使用函数有什么好处呢?

  1. 模块化开发:一个大的功能可以拆解成各种子功能,每个子功能都可以设计成一个函数,每个函数可以作为一个独立的模块存在,程序的逻辑更加清晰,逻辑关系更加明确。
  2. 代码可以复用,只要根据定义出一个函数,需要这个功能的地方直接调用函数即可,可以降低代码的冗余,提升开发的效率。
  3. 方便多个程序员之间的协作开发,方便程序的多个模块之间的相互交互
  4. 熟悉函数的使用后,代码的编写、阅读、调试、维护都会变得容易。

二.函数的分类

在C/C++语言中,函数一般分为库函数和自定义函数,下面我就逐个介绍一下这两种函数:

1.库函数

(1)库函数的介绍

库函数是标准库中提供的现成的函数,我们只需要学习函数的功能,就可以直接使用。有了库函数,一些常见的功能就不需要我们自己实了,一定程度上提高了效率。同时库函数的质量和执行效率上都更有保障。编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行声明。下面是库函数的学习和查阅工具:
C/C++语言第三方网站https://legacy.cplusplus.com/reference库函数中有数学相关的,有日期相关的、有算法相关的等。这些库函数相关的信息都有自己对应的头文件,每个头文件都包含了相关的一组函数和类型等信息,库函数的学习不是一蹴而就的,需要慢慢的研究,各个击破即可。C++语言是兼容C语言的,所以C++中包含了C语言的头文件,在C++中会在原来的头文件的基础上进行封装,在C++程序中更推荐C++的头文件写法,以下是常见的头文件:

|---------|----------|---------|
| 解释 | C语言头文件 | C++头文件 |
| 常用数学函数 | math.h | cmath |
| 浮点类型的极限 | float.h | cfloat |
| C风格输入输出 | stdio.h | cstdio |
| 字符串处理 | string.h | cstring |

(2)库函数的使用举例

举例:sqrt函数,下面是该函数的原型:

cpp 复制代码
double sqrt (double x);
  • sqrt是该函数的名字
  • x是函数的参数,表示调用该函数,需要向其传递一个double类型的值。
  • double是返回值类型,表示函数调用完毕之后会返回double类型的结果。

功能:计算一个数的平方根,返回平方根的结果。下面是该函数的代码实践:

cpp 复制代码
#include <iostream>
#include <cmath> //数学函数头⽂件,不包含则⽆法使⽤sqrt函数 
using namespace std;
int main()
{
 double d = 16.0;
 double r = sqrt(d);
 cout << r << endl;
 return 0;
}

上述代码的具体解释:首先包含需要用到函数的头文件,防止代码出错。接下来创建一个double类型的变量,用于储存需要运算的数。最后调用sqrt函数,将需要运算的数作为参数传递给创建的变量r。这样就可以求出变量d的平方根。

2.自定义函数

了解完库函数,我们知道库函数不需要我们知道怎样具体实现。只需要学会库函数的使用即可。接下来要学习的自定义函数在实际开发过程中会起重大作用。所谓自定义函数就是自己设计和实现的函数。

(1)函数的语法形式

cpp 复制代码
ret_type fun_name(形式参数)
{
    //函数实现的具体内容
}
  • ret_type是用来表示函数计算的结果类型的,有时候可以是void,表示该函数没有返回值。
  • fun_name表示的是函数的名字,函数名字会更加方便调用。
  • 函数的形式参数表示需要执行函数操作不可或缺的原材料。函数的参数也可以是void,表示函数没有参数。如果有参数,需要交代参数的类型和名字。将函数比作工厂的话,函数的形式参数就是这个工厂的原材料。
  • 大括号内是函数的具体实现,被称为函数体。函数体就是完成函数计算的过程。

(2)函数的定义

下面我们举例说明函数是如何定义的。

例如:写一个加法函数,完成两个整型变量的加法操作。

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
 int a = 0;
 int b = 0;
 //输⼊ 
 cin >> a >> b;
 //任务:调⽤加法函数,完成a和b的相加 
 //求和的结果放在r中 
 //to do
 
 //输出 
 cout << r << endl;
 return 0;
}

在定义函数之前,我们需要给函数起名为:Add,函数需要接受两个形式参数,分别作为运算的两个操作数。根据这些分析我们就可以写出下面的函数:

cpp 复制代码
#include<iostream>
using namespace std;
//这就是函数的定义 
int Add(int x, int y)
{
 int z = 0;
 z = x + y;
 return z;
}
int main()
{
 int a = 0;
 int b = 0;
 //输⼊ 
 cin >> a >> b;
 //调⽤加法函数,完成a和b的相加 
 //求和的结果放在r中 
 int r = Add(a, b);
 //输出 
 cout << r << endl;
 return 0;
}

当然在函数体中,return的作用是返回函数最后的计算结果。所以我们可以直接将两数相加的结果作为函数的返回值。利用return将其返回。如下代码:

cpp 复制代码
int Add(int x, int y)
{
 return x + y;
}

下方图片是自定义Add函数的调用步骤:

上面图片可以看出来,函数会在出现其名字的地方开始调用,随后将实际参数传递给形式参数,之后在函数体内,根据给出的两个形式参数进行相关运算。最后返回运算结果由调用地方接收。

三.函数的参数和返回值

1.实参和形参

实际上,在函数的定义和使用的过程中,函数的参数分为两种:实参和形参。下面我们回顾下面的代码:

cpp 复制代码
#include<iostream>
using namespace std;
int Add(int x, int y)
{
 int z = 0;
 z = x + y;
 return z;
}
int main()
{
 int a = 0;
 int b = 0;
 //输⼊ 
 cin >> a >> b;
 //调⽤加法函数,完成a和b的相加 
 //求和的结果放在r中 
 int r = Add(a, b);
 //输出 
 cout << r << endl;
 return 0;
}

(1)实参

实际参数就是真实传递给函数的参数,在上面的代码中3-8行是Add函数的定义,有了函数之后,倒数第4行是函数的调用。我们把函数调用时,函数名括号内的变量a和变量b叫做函数的实际参数。

(2)形参

在上面的代码中,函数定义时,在函数名后面括号内的变量x和变量y,就称为函数的形式参数,简称形参。为什么将其称为形式参数呢?

实际上,定义Add函数而不去调用的话,该函数的形式参数只是形式上存在的,不会向内存中申请空间,所以叫做形式参数。只有在调用函数时,才会向内存申请空间来存储实际参数的值。所以形参也是实参的临时拷贝。

(3)实参和形参的关系

上面提到的,在调用函数时,实参会将值传递给形式参数。但是形参和实参各自拥有着独立的空间。这种现象可以根据调试查看双方的地址观察。请看下方代码和调试演示:

cpp 复制代码
#include<iostream>
using namespace std;
int Add(int x, int y)
{
 int z = 0;
 z = x + y;
 return z;
}
int main()
{
 int a = 0;
 int b = 0;
 //输⼊ 
 cin >> a >> b;
 //调⽤加法函数,完成a和b的相加 
 //求和的结果放在r中 
 int r = Add(a, b);
 //输出 
 cout << r << endl;
 return 0;
}

在有关调试的图片上可以看出:形式参数和实际参数在内存中存储的地址是不同的,相同的仅仅是对应的值而已。所以可以得出结论:形式参数是实际参数的临时拷贝。

2.函数传参

(1)数组作函数参数

在使用函数解决问题时,有时数组会作为参数传递给函数,在函数的内部对数组进行操作。比如:写一个函数将一个整型数组的内容全部改为-1,再写一个函数将数组的内容打印在屏幕上。下面是函数设计的雏形:

cpp 复制代码
#inculude <iostream>
using namespace std;
int main()
{
 int arr[] = {1,2,3,4,5,6,7,8,9,10};
 set_arr(); //设置数组内容为-1 
 print_arr(); //打印数组内容 
 return 0;
}

这里的set_arr函数需要能对数组内容进行重置的能力,所以需要将数组作为函数参数传递给函数,同时函数内部在设置数组的每个元素时,也得遍历数组,需要知道数组的元素个数。所以set_arr需要两个参数,一个是数组,另一个是数组元素的个数。仔细分析的话,另一个print_arr函数也是这样的,只有拿到数组和元素个数才能打印数数组的每个元素。下面是定义函数的进一步代码:

cpp 复制代码
int main()
{
 int arr[] = {1,2,3,4,5,6,7,8,9,10};
 int sz = sizeof(arr)/sizeof(arr[0]);
 set_arr(arr, sz); //设置数组内容为-1 
 print_arr(arr, sz); //打印数组内容 
 return 0;
}

数组作为参数传递给上述两个函数,那么函数体的设计应该怎样设计呢?

  • 函数实参的名字可以与形参名字相同,也可以不同。
  • 函数的形参和实参的个数必须匹配。
  • 函数的实参是数组,形参也要写成数组的形式。
  • 形参如果是一维数组,数组大小可以省略不写,这里的数组大小指的是数组名后中括号的数字,不是上述代码的sz。
  • 形参如果是二维数组,行可以省略,但是列不能省略。
  • 数组传参如果是数组,形参是不会创建新数组的,会在实参的数组上进行一系列操作。对形参的修改会影响实参。

下面是两个函数的具体实现:

cpp 复制代码
void set_arr(int arr[], int sz)
{
 int i = 0;
 for(i = 0; i < sz; i++)
 {
 arr[i] = -1;
 }
}
void print_arr(int arr[], int sz)
{
 int i = 0;
 for(i = 0; i < sz; i++)
 {
 cout << arr[i] << " ";
 }
 cout << endl;
}

(2)字符串作为函数参数

当字符串作为函数参数时,直接在形参部分使用字符串接收即可。这里的形参是实参的一份临时拷贝,对形参的修改不影响实参。

cpp 复制代码
void test(string s) 
{
 cout << s << endl;
}
int main()
{
 string s("hello world");
 test(s);
}

(3)全局变量作为函数参数

全局变量的作用域很大,在整个程序中都可以使用,那么只要把变量、数组定义成全局变量,就可以在函数调用时不用传参。当然,有时候变量或数组定义成全局时,依然不可以省略传参,比如:递归场景,这时候就应该考虑传参解决问题。在竞赛中,为了方便经常这样做。但是在软件工程中,应该减少这种做法。下面是相关代码:

cpp 复制代码
#include <iostream>
using namespace std;
int arr[10] = { 0 };
void print_arr()
{
 int i = 0;
 for (i = 0; i < 10; i++)
 {
 cout << arr[i] << " ";
 }
 cout << endl;
}
int main()
{
 //打印数组的内容 
 print_arr();
 //给数组的元素赋值为1~10 
 for (int i = 0; i < 10; i++)
 {
 arr[i] = i + 1;
 }
 //打印数组 
 print_arr();
 return 0;
}

(3)返回值

我们在设计函数时,函数在经过计算会得到计算结果,有时候不会得到计算结果,仅仅是完成某项任务。当后续需要使用函数的计算结果时,函数就需要return进行返回。下面我们来讨论下return关键字:

1.return后面可以是一个数值,也可以是一个表达式。如果是表达式,则先执行表达式,再返回表达式的结果。函数返回的值,可以用变量来接受;如果不需要这个返回值,也可以不接受。

cpp 复制代码
// 这⾥使⽤简化版本的加法函数 
int Add(int x, int y)
{
 return x + y; // 1. 先执⾏x+y,得到该表达式计算的结果 2. 执⾏return,返回结
果 
}
//在main函数中使⽤return 0; 返回的就是⼀个数值 

2.return后面也可以什么都没有,直接写return,这种写法适合函数的返回类型是void的情况。

cpp 复制代码
void test(int n)
{
 if(n == 2)
 return; //只要执⾏这⾥的return,函数就提前返回,不再打印数据 
 cout << n << endl;
}

3.return返回的值和函数的返回类型不一致,系统会自动将返回值的类型隐式转换为函数的返回类型

cpp 复制代码
#include <iostream>
using namespace std;
 
int test()
{
 return 3.14;
}
int main()
{
 int ret = test();
 cout << ret << endl;
 return 0;
}

4.return语句执行之后,函数就会彻底返回,后面的代码不再执行。

cpp 复制代码
#include <iostream>
using namespace std;
void print_arr(int arr[], int n)
{
 int i = 0;
 for(i=0; i<n; i++)
 {
 if(i == 5)
 return;//对⽐换成break 
 cout << arr[i] << " ";
 }
 cout << endl;
 cout << "打印完毕" << endl;
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 print_arr(arr, 10);
 return 0;
}

5.如果函数中return语句被执行,则函数直接返回,即使函数中return语句后还有其他代码,也不再执行。

四.函数的声明和调用

1.函数的声明

一般在使用函数的方式写代码时,函数定义好之后,在后续代码中对函数进行调用,例如:代码一。有时候我们也会将函数定义放在函数调用的后面,例如:代码二。这时候编译器在编译时就会出现报错,提示这个函数可能不存在,为了消除这种警告,我们需要在函数调用之前先声明一下这个函数,这就是函数声明。函数声明就是告诉编译器,有一个函数的名字和参数。至于这个函数是否真正存在,就需要看函数的定义了。函数调用必须满足先声明后使用。

cpp 复制代码
//代码1 
#include <iostream>
using namespace std;
//函数定义 
int Add(int x, int y) 
{
 return x + y;
}
int main()
{
 int a = 10;
 int b = 20;
 int c = Add(a, b);
 cout << c << endl;
 return 0;
}
cpp 复制代码
//代码2 
#include <iostream>
using namespace std;
//函数声明 
int Add(int x, int y);
int main()
{
 int a = 10;
 int b = 20;
 int c = Add(a, b);
 cout << c << endl;
 return 0;
}
int Add(int x, int y) //函数定义 
{
 return x + y;
}

2.函数调用

函数调用的方式有俩个:传址调用(传引用调用)和传值调用,前面我们学的都是传值调用。下面讲解一下两种函数的调用方式:

(1)传值调用

写一个函数Max,求出两个数的较大值:

cpp 复制代码
#include <iostream>
using namespace std;
int Max(int x, int y)
{
 return x > y ? x : y;
}
int main()
{
 int a = 0;
 int b = 0;
 cin >> a >> b;
 int c = Max(a, b);
 cout << c << endl;
 return 0;
}

上述代码的详细解释:代码在调用Max函数时,就是传值调用。传值调用就是将实参的数据直接传递给形参。这个过程实际上是实参的值拷贝一份给形参的过程。这时形参和实参是不同的变量,所以对形参的修改不影响实参。这种情况下参数的传递方式只能是实参到形参,属于单向传递。为了介绍单向传递,请看下方案例:

案例二:写一个函数Swap,交换两个整型变量的值。

如果按照常规的思路,会写出下方代码:

cpp 复制代码
#include <iostream>
using namespace std;
void Swap(int x, int y)
{
 int z = x;
 x = y;
 y = z;
}
int main()
{
 int a = 0;
 int b = 0;
 cin >> a >> b;
 cout << "交换前, a = " << a << " b = " << b << endl;
 Swap(a, b);
 cout << "交换后, a = " << a << " b = " << b << endl;
 return 0;
}

运行完上述代码会发现:Swap函数中的x和y确实会被函数体的逻辑交换,但是main函数的变量a和b没有交换。这是传值调用的特点,形参和实参是不同的内存空间。对形参的修改不会影响实参,数据仍然是单向传递的,所以交换的效果没有达到。

结论:

  1. 传值调用方式应用的场景:仅需要通过传参的方式将实参的值传递给被调函数,被调函数就可以完成工作,而不需要改变实参的值。
  2. 如果需要像Swap函数那样真正改变两个实参的位置就需要使用传址调用,这里的址指的是指针也可以是引用

接下来讲解C++语言的传引用调用方式:

(2)引用

<1>引用的概念

引用不是新定义的变量,而是为已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块空间。下面是引用的使用格式:

类型&引用变量名=引用实体。下面是具体的代码演示:

cpp 复制代码
void TestRef()
{
 int a = 10;
 int& ra = a; // 定义引⽤类型 
 //通过printf输出a,ra的地址 
 printf("%p\n", &a);
 printf("%p\n", &ra);
}

上述代码首先创建了一个整型变量a,其次为变量a起了一个别名ra。两个变量共用一块空间,差别仅是名字不同。a和ra其实是一个变量。

<2>引用特性
  1. 引用在定义时必须初始化
  2. 一个变量可以拥有多个引用
  3. 引用一旦引用一个实体,就不能引用其他实体
cpp 复制代码
void TestRef()
{
 int a = 10;
 // int& ra;   // 该条语句编译时会出错 
 int& ra = a;
 int& rra = a;
 printf("%p %p %p\n", &a, &ra, &rra);
}
<3>传址调用

下面我们根据学习的引用知识,改造以前写的问题Swap函数:

cpp 复制代码
#include <iostream>
using namespace std;
void Swap(int& x, int& y)
{
 int z = x;
 x = y;
 y = z;
}
int main()
{
 int a = 0;
 int b = 0;
 cin >> a >> b;
 cout << "交换前, a = " << a << " b = "<< b << endl;
 Swap(a, b);
 cout << "交换后, a = " << a << " b = "<< b << endl;
 return 0;
}

上面这种实现Swap函数实现方式是函数的传引用调用。这种调用方式的本质是:将实参变量的地址传递给形参,而形参使用指针直接找到实参来进行相关操作。所以在传引用调用函数时,形参的改变会引起对应的实参改变。这就是传引用调用。当然也可以用传址调用,两者的本质是相同的。因为交换两组值在标准库内也有相关的函数,所以是使用库函数调换位置的代码演示:

cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;
int main()
{
 int a = 0;
 int b = 0;
 cin >> a >> b;
 cout << "交换前, a = " << a << " b = " << b << endl;
 swap(a, b);//直接使⽤库函数swap交换两个变量 
 cout << "交换后, a = " << a << " b = " << b << endl; 
 return 0;
}

下面是swap函数调换两数组的值:

cpp 复制代码
#include <iostream> 
#include <utility> //swap函数需要 
using namespace std;
int main () 
{
 int arr1[4]; // arr1: 0 0 0 0
 int arr2[] = {10,20,30,40}; // arr1: 0 0 0 0 arr2: 10 20 30 40
 swap(arr1, arr2); // arr1: 10 20 30 40 arr2: 0 0 0 0
 
 for (int e: arr1) 
 cout << e << " ";
 cout << endl;
 return 0;
}

下面是调用函数实现调换字符串值:

cpp 复制代码
#include <iostream>
using namespace std;
void printString(string& s) 
{
 cout << s << endl;
}
int main()
{
 string s("hello world");
 printString(s);
}
<4>传值、传址的效率对比

下面代码定义了一个全局字符串s,然后分别以传值调用和传引用调用的方式进行对比,结果如下:

cpp 复制代码
#include<iostream>
#include<ctime>
using namespace std;
//定义全局字符串s 
string s("hello world");
void TestFunc1(string s) {}
void TestFunc2(string& s) {}
void Test()
{
 // 以值作为函数参数 
 size_t begin1 = clock();
 for (size_t i = 0; i < 10000000; ++i)
 {
 TestFunc1(s);
 }
 size_t end1 = clock();
 // 以引⽤作为函数参数 
 size_t begin2 = clock();
 for (size_t i = 0; i < 10000000; ++i)
 {
 TestFunc2(s);
 }
 size_t end2 = clock();
 // 分别计算两个函数运⾏结束后的时间 
 cout << "TestFunc1(string)-time:" << end1 - begin1 << endl;
 cout << "TestFunc2(string&)-time:" << end2 - begin2 << endl;
}
int main() 
{
 Test();
 return 0;
}

根据上面的结果可以看出:采用传值调用过程中,函数将实参传给形参时,形参会创建新的空间,再将实参的数据拷贝一份给形参;但是传引用调用的方式就不会存在数据的拷贝,只有在形参的部分建立引用的关系,形参就是实参,实参就是形参。所以传引用调用的效率高于传值调用。

注意:数组在传参的时候,形参和实参本身就是同一个数组,所以在数组传参时,不需要传引用调用。

五.函数重载

1.重载的概念

案例:写一个函数,求两个整数的和:

cpp 复制代码
#include <iostream>
using namespace std;
int IntAdd(int x, int y)
{
 return x + y;
}
int main()
{
 int a = 0;
 int b = 0;
 cin >> a >> b;
 int c = IntAdd(a, b);
 cout << c << endl;
 return 0;
}

但是当两个整数变为小数时,就需要继续写出下面的函数:

cpp 复制代码
double DoubleAdd(double x, double y)
{
 return x + y;
}

上面的两种求和函数的功能是类似的,都是求两个数的和,只是参数的类型不同。既然函数的逻辑和功能都是类似的。那能不能将函数名统一为Add函数呢?其实是可以的,这就是C++语言的函数重载功能。

函数重载:在同一个作用域中可以有多个同名函数,它们的函数名不同,但是参数列表相同。可以不同的是参数数量、类型、顺序。这三样至少就一个不同就会构成函数重载。函数的返回类型并不影响函数的重载,因为编译器不会根据返回类型区分不同的函数。

2.函数重载举例

cpp 复制代码
#include<iostream>
using namespace std;
// 1、参数类型不同 
int Add(int a, int b)
{
 return a + b;
}
double Add(double a, double b)
{
 return a + b;
}
// 2、参数个数不同 
void f()
{
 cout << "f()" << endl;
}
void f(int a)
{
 cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同 
void f(int a, char b)
{
 cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
 cout << "f(char b, int a)" << endl;
}
int main()
{
 Add(10, 20);
 Add(10.1, 20.2);
 f();
 f(10);
 f(10, 'a');
 f('a', 10);
 return 0;
}

六.递归

1.递归的概念

递归是一种在计算机中非常重要的技术,它可以简化许多复杂的问题。递归具体指的是函数在定义时直接或间接调用自身的方式。下面展示一个最简单的递归例子:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 cout << "hehe" << endl;
 main();//main函数中⼜调⽤了main函数 
 return 0;
}

上述代码就是递归,在main函数中不停的调用main函数自身。但是会死循环的打印hehe,这种代码会陷入死递归,最终导致程序崩溃。

2.递归的思想

递归可以把一个大型复杂的问题层层转换为一个与原问题相似,但规模较小的子问题来解答,直到子问题不可再被拆分,可以直接求解该子问题时递归就结束了。所以递归的思想就是将大事化小的过程。递归的递就是递推的意思,归就是回归的意思。

3.递归的必要条件

  1. 递归存在限制条件,当满足这个限制条件时,递归便不会再继续。
  2. 每次递归调用之后越来越接近这个限制条件。直到子问题可以解出。

4.递归举例:计算阶乘

题目:计算n的阶乘(不考虑内存溢出),n的阶乘就是1~n的数字累计相乘。

(1)递归分析

n的阶乘公式:n! = n*(n-1)!:利用该公式可以看出n的阶乘可以转换为求n-1的阶乘。将大问题转换为小问题就可以用阶乘的思想。下面是阶乘思想的递推公式:

(2)代码实现

下面是阶乘函数的实现代码:

cpp 复制代码
int Fact(int n)
{
 if (n == 0)
 return 1;
 else
 return n * Fact(n - 1);
}

测试:

cpp 复制代码
#include <iostream>
using namespace std;
int Fact(int n)
{
 if (n == 0)
 return 1;
 else
 return n * Fact(n - 1);
}
int main()
{
 int n = 0;
 cin >> n;
 int ret = Fact(n);
 cout << ret << endl;
 return 0;
}

(3)画图演示

7.4.4 递归和循环(迭代)

关于求n的阶乘,如果根据公式就会很容易的写出递归的代码,但是这个问题也可以由循环的方式解决。要想计算n的阶乘,只要能产生1~n的数字,然后累计相乘即可。代码演示如下:

cpp 复制代码
int Fact(int n)
{
 int i = 0;
 int ret = 1;
 for(i = 1; i <= n; i++)
 {
 ret *= i;
 }
 return ret;
}

递归和循环的差异:

在C语言中,每一次函数的调用,都需要为其调用在内存的栈区申请一块内存空间来保存函数调用期间的各种变量的值,这块空间被称为运行时堆栈,或者函数栈帧。函数不返回就会一直调用函数栈帧,所以如果函数存在递归的话,每一次函数调用都会开辟属于自己的栈帧空间,直到函数递归的结束,当函数回归时,才逐层释放函数的栈帧空间。

如果采用递归的思想实现代码且递归层次太深,就会浪费太多的栈帧空间。可能导致栈溢出的问题。所以要考虑栈溢出问题的话,使用循环解决更好,效率高。递归实现的问题思路清晰,其实现问题的简洁性可以补偿其带来的运行开销。

5.递归举例:计算斐波那契数列

写一个代码,求出第n个斐波那契数。下面是代码实现之前推导出的斐波那契数的公式:

下面是求斐波那契数的函数实现:

cpp 复制代码
int Fib(int n)
{
 if(n <= 2)
 return 1;
 else
 return Fib(n-1) + Fib(n-2);
}

下面是测试函数的代码演示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
 int n = 0;
 cin >> n;
 int ret = Fib(n);
 cout << ret << endl;
 return 0;
}

当我们试图求50项的斐波那契数会等好长时间才会得到计算结果,这说明了递归实现该代码的效率低下,原因如下:

递归在运行时会不断展开,在展开的过程中,很容易发现:在递归的过程中会重复计算,且递归层次越深,冗余的计算就会越多。下面进行代码测试:

cpp 复制代码
#include <iostream>
using namespace std;
int count = 0;
int Fib(int n)
{
 if(n == 3)
 count++;//统计第3个斐波那契数被计算的次数 
 if(n <= 2)
 return 1;
 else
 return Fib(n - 1) + Fib(n - 2);
}
int main()
{
 int n = 0;
 cin >> n;
 int ret = Fib(n);
 cout << ret << endl; 
 cout << "count = " << count << endl;
 return 0;
}

通过上述测试代码可以得出:在计算第40项目的斐波那契数时,使用递归的方式,只第三个斐波那契数就被重复计算了39088169次,这样计算是非常冗余的。所以递归实现斐波那契数是不合适的。下面我们利用迭代的方式实现斐波那契数的求解:

cpp 复制代码
int Fib(int n)
{
 int a = 1;
 int b = 1;
 int c = 1;
 while(n > 2)
 {
 c = a + b;
 a = b;
 b = c;
 n--;
 }
 return c;
}

在使用迭代实现该代码时,效率就高了很多。所以在编写代码时,应该考虑代码的效率问题。

相关推荐
七禾页丫6 小时前
面试记录14 上位机软件工程师
面试·职场和发展
jiayong238 小时前
Vue2 与 Vue3 核心原理对比 - 面试宝典
vue.js·面试·职场和发展
敲敲了个代码11 小时前
从N倍人力到1次修改:Vite Plugin Modular 如何拯救多产品前端维护困境
前端·javascript·面试·职场和发展·typescript·vite
熊猫钓鱼>_>11 小时前
深入理解Java堆栈:从原理到面试实战
java·开发语言·面试·职场和发展·面向对象·堆栈·oop
jiayong2312 小时前
Vue2 与 Vue3 生态系统及工程化对比 - 面试宝典
vue.js·面试·职场和发展
学历真的很重要12 小时前
【系统架构师】第一章 计算机系统基础知识(详解版)
学习·职场和发展·系统架构·系统架构师
仰泳的熊猫12 小时前
题目1433:蓝桥杯2013年第四届真题-危险系数
数据结构·c++·算法·蓝桥杯·深度优先·图论
Warren9812 小时前
Pytest Fixture 到底该用 return 还是 yield?
数据库·oracle·面试·职场和发展·单元测试·pytest·pyqt
雁于飞13 小时前
【无标题】
笔记·面试·职场和发展·跳槽·产品经理·创业创新·学习方法