当前位置: 首页 > news >正文

C++蓝桥杯之函数与递归

1. 函数是什么

概念

数学中我们其实就见过函数的概念,比如:一次函数 y = kx + b ,k和b都是常数,给一个任意的 x ,就得到一个 y 值。其实在C/C++语⾔中就引⼊了函数(function)的概念,有些翻译为:子程 序,子程序这种翻译更加准确⼀些。函数就是⼀个完成某项特定的任务的一小段代码,这段代码是有 特殊的写法和用方法的。

其实大家在前面学习的时候,就见过函数了,比如:main函数、scanf函数、printf函数、pow函数、 sqrt函数等。 C/C++语言程序其实是由若干个小的函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率。

如果我们要完成这样下面这样的任务:

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

如果没有函数,我们能写出的代码如下的代码

#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;
}

仔细看,这里面有两部分是重复的,那如果我能把这部分封装成一个函数,在需要的时候调用,就方便很多,看下面

#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;
}
//输出 
//0 0 0 0 0 0 0 0 0 0
//1 2 3 4 5 6 7 8 9 10

上述代码 print_arr 就是我们自己定义的一个函数,使用函数能有什么好处呢?  

1. 模块化开发,一个大的功能,总能拆分成各种子功能,每个子功能都可以设计成一个函数,每个函数可以作为一个独立的模块存在,程序的逻辑更加清晰,逻辑关系更加明确。

2. 代码可以复用,只要根据需要定义出一个函数,需要这个功能的地方,直接调用函数就行,降低了代码的冗余,提升了开发效率。

3. 方便多个程序员之间协作开发,方便程序的多个模块之间互相交互。

4. 熟悉函数的使用之后,代码的编写、阅读、调试、维护都变得更加容易。

2. 函数的分类

在C/C++中,函数一般分为库函数和自定义函数,那我们接下来就介绍一下。

库函数

库函数介绍

前面内容中提到的 printf 、 scanf 都是库函数,库函数是标准库中提供的现成的函数,我们只要学习函数的功能,就能直接使用。有了库函数,一些常见的功能就不需要程序员自己实现了,一定程 度提升了效率;同时库函数的质量和执行效率上都更有保证。 编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了 声明。 库函数的学习和查阅工具很多,比如:

C/C++官方参考手册:https://zh.cppreference.com/w/cpp/header

C/C++第三方网站:https://legacy.cplusplus.com/reference/

库函数中有数学相关的,有日期相关的,有算法相关的等等。这些库函数相关的信息都有自己对应的头文件,每一个头文件中都包含了相关的一组函数和类型等信息,库函数的学习不用着急一次性全部学会,慢慢学习,各个击破就行。

C++是兼容C语言,所以在C++中也包含了⼀些来自C语言的头文件,这些头文件的后缀是 .h ,如果需要也可以直接包含使用;有一些来自C语言的头文件,在C++中会在原来C语言的头文件进行了封装, 在C++程序中更推荐C++的头文件写法。

库函数使用举例

举例:sqrt

• sqrt 是函数名

• x 是函数的参数,表示调用

• do uble 是返回值类型,表示函数计算的结果是 sqrt 函数需要传递一个 double 类型的值 double 类型的值

功能:

Computesquareroot 计算平方根

Returns the square root of x.(返回平方根)

头文件包含:

库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件,不包含是可能会出现一些问题的。

using namespace std;
int main(){double d = 16.0;double r = sqrt(d);cout << r << endl;//输出 4 return 0;}

自定义函数

了解了库函数,其实库函数的功能是有限的,实际开发过程中还是得根据需要将代码封装成自定义的函数;自定义的函数就是自己设计和实现的函数。

函数的语法形式

其实⾃定义函数和库函数是一样的,形式如下:

ret_type fun_name(形式参数)
{}

语法说明如下:

• ret_type 是用来表示函数计算结果的类型,有时候返回类型可以是void,表示什么都不返回

• fun_name 是为了方 便使用函数;就像人的名字⼀样,有了名字方便称呼,函数有了名字反便调 用,所以函数名尽量要根据函数的功能起的有意义。

• 函数的参数就相当于工厂加工需要的原材料,函数的参数也可以是 void ,明确表示函数没有参 数。如果有参数,多个参数用逗号隔开,每个参数要分别交代清楚参数的类型和名字。

• {} 括起来的部分被称为函数体,函数体就是完成计算的过程。

我们可以把函数想象成小型的⼀个加工厂,工厂得输入原材料,经过工厂加工才能生产出产品,那函数也是一样的,函数一般会输⼊一些值(可以是0个,也可以是多个),经过函数内的计算,得出结果。

函数定义

那函数是如何定义的,接下来我们举一个实际的例子,来讲解。 例如:写一个加法函数,完成2个整型变量的加法操作。

#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,函数Add 需要接收2个整型类型的参数,函数计算的结果也是整型。

所以我们根据上述的分析写出函数:

#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;//输入 3 4cin >> a >> b;//输出//调用加法函数,完成a和b的相加//求和的结果放在r中 int r = Add(a, b);cout << r << endl;//输出7 return 0;}

Add 函数也可以简化为:

int Add(int x, int y)
{return x + y;
}

函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。 上⾯只是一个例子,未来我们是根据实际需要来设计函数,函数名、参数、返回类型都是可以灵活变化的。函数的实现就是函数的定义。

3. 函数参数和返回值

实参和形参

实际上,在函数定义和使用的过程中,函数的参数要被分为两种:

• 实际参数,简称实参

• 形式参数,简称形参

为了更好进行解释上面的概念,先回顾我们前面写的代码:


#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;}

实参

实际参数就是真实传递给函数的参数。 在上面代码中,第4~6行是 Add 函数的定义,有了函数后,再第19行调用Add 函数的。 我们把第19行调用Add 函数时,传递给函数的参数 a 和 b ,称为实际参数,简称实参

形参

在上行代码中,第4行定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称形参。

为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,而不去调用的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在 函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。

实参和形参的关系

虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各自是独立的内存空 间。

这个现象是可以通过调试来观察的。请看下面的代码和调试演示:

#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;}

我们在调试的可以观察到, x 和 y 确实得到了 a 和 b 的值,但是 x 和 y 的地址和 a 和 b 的地址 是不一样的,当 a 和 b 传参给形参x和y的时候, x 和 y 只是得到了 a 和 b 的值,他们得有自己独立的空间。所以我们可以理解为形参是实参的一份临时拷贝。

函数传参

数组做函数参数

在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。 比如:写一个函数将一个整型数组的内容,全部置为-1 ,再写一个函数打印数组的内容。 简单思考一下,基本的形式应该是这样的:

#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 传递2个参数,一个是数组,另外一个是数组的元素个数。仔细分析 print_arr 也是⼀样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。

数组作为参数传递给了 set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢?

这里我们需要知道数组传参的几个重点知识:

• 函数的实参的名字和形参的名字可以相同,也可以不同

• 函数的形式参数要和函数的实参个数匹配

• 函数的实参是数组,形参也写成数组形式的

• 形参如果是一维数组,数组大小可以省略不写

• 形参如果是二维数组,行可以省略,但是列不能省略 

• 数组传参,形参是不会创建新的数组的

形参操作的数组和实参的数组是同一个数组

根据上述信息,我们可以实现以下两个函数

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;}

字符串做函数参数

那如果将字符串做函数参数呢?其实也很简单,直接在形参的部分使用字符串来接收就可以。这里的形参s也是实参s的一份临时拷贝,对形参的修改不能影响实参。

void test(string s) 
{cout << s << endl;
}int main()
{string s("hello world");test(s);
}

全局变量不用传参

全局变量的作用域很大,在整个程序中都可以使用,那么只要把变量、数组等定义成全局变量,在函数中使用,是可以不用传参的,在竞赛中为了方便经常使用,但是在软件工程中很少这么使用。

#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;
}

当然,有时候变量或者数组,定义成全局的时候,是不能解决问题,⽐如:递归等场景,这时候,就 得考虑传参的问题。

返回值

我们在设计的函数的时候,函数在经过计算后,有时候需要带回一些计算好的数据,这时候往往使用 return 来返回,这里我们就讨论一下使用 return 返回。

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

#include <iostream>// 这里使用简化版本的加法函数
int Add(int x, int y)
{return x + y;       
}// 1. 先执行x+y,得到该表达式计算的结果//2. 执行return,返回结果 return 0; 
}////在main函数中使用return 0; 返回的就是一个数值

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

void test(int n)
{if(n == 2)return;//只要执行这里的return,函数就提前返回,//不再打印数据cout << n << endl;}

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

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

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

#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);//输出1 2 3 4 5 return 0;
}

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

4. 函数的声明和调用

函数声明

⼀般在使用函数的方式写代码的时候,函数定义好之后,在后续的代码中对函数进行调用,例如 代码 1 。有时候我们也可能将函数定义放在了函数调用的后边,例如 代码 2 ,这时候编译器在编译的时候 就会有警告出现,提这个函数可能不存在,为了消除这种警告,我们⼀般在函数的调用 之前先声明一下这个函数,这就是函数声明。函数声明就是告诉编译器,有⼀个函数名字叫什么,参数是什么,返回类型是什么,至于这个函数是否真的存在,是要看函数的定义了。函数调用必须满足先声明后使用。

#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;//输出30 return 0;}
#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;
}
//输出30 

函数调用

函数调用的方式,一般会分为两类:传值调用和传址(引用)调用,我们前面讲过的都是传值调用。因为暂时还没有学习指针,这里下面我们会讲解一下传引用调用

传值调用

写⼀个函数Max,求两个整数的较大值。

#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;//5 3int c = Max(a, b);cout << c << endl;//5return 0;
}

第14行调用 Max 函数时,就是传值调用。传值调用就是将实参的数据直接传递给形参。这个过程其 实是将实参的值拷贝一份给 Max 函数使用,这份副本其实就是形参变量。这时形参和实参是不同的变 量,所以对形参的修改,不会影响实参。这种情况下参数传递的方式只能从实参到形参,也就是单向 传递。为了理解什么是单向传递,我们看一下下面的代码。

写一个函数Swap,交换两个整型变量的值。 如果我们按照常规的思路,写出下面的代码。

#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;//3 5Swap(a, b);cout << "交换前, a = " << a << " b = " << b << endl;cout << "交换后, a = " << a << " b = " << b << endl;//输出 //交换前, a = 3 b = 5//交换后, a = 3 b = 5return 0;}

运行程序你会发现,其实 Swap 函数中的 x 和 y 确实会交换,但是  main 函数的中的 a 和 b 其实 没有交换。这就是值传递的特点,形参和实参是不同的内存空间,对形参的修改不会影响实参,数据 仍然是单向传递的,所以交换的效果没有达到。

这里我想给大家两个启发:

1. 传值调用的方式在什么时候使用?

其实传值调用的方式一般应方的场景就是:

仅需要通过传参的方式将实参的值传递给被调函数,被调 函数就可以完成工作,而不需要改变实参的值。比如,案例1中的Max函数,将 a 和 b 的值传递给形参 x 和 y ,在 Max 函数中使用 x 和 y 来求较大值,就能得到 a 和 b 中的较大值。

2. 如何改造 Swap 函数才能实现交换的效果呢? 这里可以使用指针,也可以使用引用。接下来我们讲一下C++中的引用。

引用

引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量是同一块内存空间。比如:李逵,在家称为"铁牛",江湖上⼈称"黑旋风"

引用使用格式:

类型&引用变量名=引用实体;


void TestRef()
{int a = 10;int& ra = a;            // 定义引用类型//通过printf输出a,ra的地址printf("%p\n", &a);printf("%p\n", &ra);
}
引用特性

1. 引用在定义时必须初始化

2. 一个变量可以有多个引用

3. 引用一旦引用一个实体,再不能引用其他实体

void TestRef()
{int a = 10;// int& ra;  // 该条语句编译时会出错int& ra = a;int& rra = a;printf("%p %p %p\n", &a, &ra, &rra);
}

传址(引用)调用

有了前面引用的学习,那就可以利用引用来重新改造前面写的 Swap 函数了。

#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;//5 3Swap(a, b);cout << "交换前, a = " << a << " b = "<< b << endl;cout << "交换后, a = " << a << " b = "<< b << endl;//交换前, a = 3 b = 5//交换后, a = 3 b = 5return 0;
}

上面这种实现 Swap 函数实现方式就是函数的传引用调用。这种调用方式的本质是将实参变量的地址传递给了形参,而形参使用指针直接找到实参来进行操作。所以修改形参的时候,直接改变的就是实参,这就是引用的应用。当然这里也可以使用指针来实现,比引用稍微复杂一些,这里等学习到指针,自然就明白了。

拓展学习:swap函数

其实在C++的STL中也提供了⼀个库函数 swap ,这个函数可以用来交换两个变量,也可以交换两个数组(容器的值),如果掌握了,我们就不需要自己再自己实现交换的逻辑,直接使用现成的函数。

swap 函数需要的头文件  utility。

#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 交换两个数组也是可以的:

#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  0for (int e: arr1) cout << e << " ";cout << endl;//输出 10 20 30 40 return 0;
}

那么字符串能不能使用引用的方式传参呢?当时是可以的。

#include <iostream>
using namespace std;void printString(string& s) 
{cout << s << endl;}int main()
{string s("hello world");printString(s);//输出 hello wold 
}

那有些函数在实现时候,既可以采用传值调用的设计,又可以采用传址(引用)调用的设计,那么我们该如何选择呢?

传值、传引用效率比较

定义了一个全局字符串 s ,然后以传值和传引用的方式进行对比,看效率的差异。

#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;}//输出 
//TestFunc1(string)-time:77
//TestFunc2(string&)-time:31

采用传值调同过程中,函数传参,将实参传递给形参的时候,形参会创建新的空间,再将实参的数据给形参拷贝一份;但是引用传参的方式,就不存在数据的拷贝,只是在形参的部分建立引用的关系, 形参就是实参。所以引用传参的效率要高于传值调用的方式。

小提示:

数组在传参的时候,形参和实参本来就是同一个数组,所以数组传参的时候,不需要使用引用参数。

5. 函数重载

重载概念

引入:

比如:我们现在要写一个函数,求两个整数的和,那么我们可以这么写:

#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;
}

那如果还想要实现一个函数求两个 double 类型浮点数的和,怎么办呢?

double DoubleAdd(double x, double y)
{return x + y;}

那么上面的 IntAdd 和 DoubleAdd 函数的功能是类似的,都是完成求两个数的求和,只是参数类 型有差异而已,既然功能是类似的,能不能函数名字统意义下都叫 Add() 呢?这样使用 Add() 函数 的人,不需要记忆很多名字,就比较方便。这是C++中引入的函数重载的功能。

函数重载:C++中的函数重载(FunctionOverloading)是指在同一个作用域中可以有多个同名函数, 它们的函数名称相同,但是参数列表不同。

1 函数返回类型_函数名(参数1, 参数2,...);

这里的“不同”指的是参数的数量、类型或顺序至少有⼀个不同。函数的返回类型并不影响函数的重 载,因为C++编译器不会根据返回类型来区分不同的函数。

重载举例

#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;}

拓展

库函数max和min

max

在C++中,max 函数用于返回两个值中的较大值。它是C++标准库 algorithm 头文件中的一个函数。

max 函数可以用于各种类型,包括内置类型(如 int ,double)以及⽤⼾⾃定义类型(如类或结构体),只要这些类型支持比较操作。

#include <algorithm>
template <class T>
const T& max(const T& a, const T& b);//默认比较
template <class T, class Compare>
const T& max(const T& a, const T& b, Compare comp); 
//自定义比较器

a: 要较较的第一个值。
b: 要较的第二个值。
comp (可选): 
自定义比较函数对象或比较函数,用于确定
“比较”值的标准。

比较函数应当返回一个布尔值,表较第一个参数是否
 小于” 第一个参数。

返回值:

• 返回 a 和 b 中较大的那个值。如果两个值相等,则返回 a 。

接下来来看几个代码

#include <iostream>
#include <algorithm>
using namespace std;int main()
{int x = 10;int y = 20;int m = max(x, y);cout << "较较值: " << m << endl;return 0;
}

再来一个

 
#include <iostream>
#include <algorithm>
#include <string>bool compareLength(const string &a, const string &b) 
{return a.size() < b.size(); //这里必须给一个判断小于的标准}int main() 
{string str1 = "apple";string str2 = "banana";string max_str = max(str1, str2, compareLength);   cout << "?度更?的字符串是" << max_str << endl;return 0;} 
min

在C++中, min 函数用于返回两个值中的较小值。它和 max 函数类似,也是在C++标准库头文件中的一个函数。使用和max函数一模一样,只是实现的效果恰好相反。

#include <iostream>
#include <algorithm>
#include <string>bool compareLength(const string &a, const string &b) 
{return a.size() < b.size(); //这里必须给一个判断小于的标准}int main() 
{ string str1 = "apple";string str2 = "banana";string min_str = min(str1, str2, compareLength);cout << "长度更长的字符串是" << min_str << endl;return 0;}

有了max函数,那么上面的题目中就可以直接使用了。代码也可以改写为:

 
#include <iostream>
using namespace std;int Max(int x, int y, int z)
{int m = max(x, y);return max(m, z);
}int main()
{int a, b, c;cin >> a >> b >> c;double ret = Max(a, b, c) * 1.0 / (Max(a + b, b, c) * Max(a, b, b + c));printf("%.3lf\n", ret);return 0;} 

7. 递归

递归的概念

递归是一种在计算机科学中非常重要的编程技术,它可以简化许多复杂的问题。递归具体是指函数在 定义的时候直接或间接调用自身0的方式。 这里给大家展示一个最简单的递归程序:

#include <iostream>
using namespace std;int main()
{cout << "hehe" << endl;main();//main函数中又调用了main函数return 0;}

上述就是一个简单的递归程序,只不过上⾯的递归只是为了演示递归的基本形式,不是为了解决问 题,代码最终也会陷入死递归,最终导致程序崩溃。

递归的思想

递归可以把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,可以直接求解,递归就结束了。递归的思考方式就是把大事化小的过程。 递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。

递归的必要条件

递归在书写的时候,有2个必要条件:

1.递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。

2.每次递归调用之后越来越接近这个限制条件。 在下面的例子中,我们逐步体会这2个限制条件。

题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。 一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。 自然数n的阶乘写作n!。

递归分析

n 的阶乘的公式: n ! = n∗(n−1)!

 举例:        5! = 5*4*3*2*14! = 4*3*2*1所以:5! = 5*4!

从这个公式不难看出:如何把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解 的。 n-1 的阶乘和 n 的阶乘是相似的问题,但是规模较小,其中有一种有特殊情况是:当 n==0 的时 候, n 的阶乘是 1 ,而其余 n 的阶乘都是可以通过上面的公式计算。

代码实现

那我们就可以写出函数 Fact 求 n 的阶乘,如果 Fact(n) 就是求 n 的阶乘,那么 Fact(n-1) 就 是求 n-1 的阶乘,函数如下:

int Fact(int n)
{if (n == 0)return 1;elsereturn n * Fact(n - 1);}

得出


#include <iostream>
using namespace std;int Fact(int n)
{if (n == 0)return 1;elsereturn n * Fact(n - 1);}int main()
{int n = 0;cin >> n;//输入4 int ret = Fact(n);cout << ret << endl;//输出24return 0;} 

递归和循环(迭代)

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

int Fact(int n)
{int i = 0;int ret = 1;for(i = 1; i <= n; i++){ret *= i;}return ret;}

这里简单的对比一下递归和循环的差异:

!!:在C语言中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。 函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐 层释放栈帧空间。 所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stackoverflow)的问题。 所以就当前的问题来看,使用循环来解决更好,效率更高。 我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。当一个问题非常复杂,难以使用迭代的方式实现时,此时 递归实现的简洁性便可以补偿它所带来的运行时开销。

eg:求第n个斐波那契数

写一个代码,求第n个斐波那契数,关于斐波那契数列,之前我们做过题目了,斐波那契数列是: 1 2 3 5 8 13 21 34 55 ... ,该数列的特点是前两个数相加等于第三个数。

递推公式即

这样我们很容易写出代码


int Fib(int n)
{if(n <= 2)return 1;elsereturn Fib(n-1) + Fib(n-2);}

即完整代码为

int Fib(int n)
{if(n <= 2)return 1;elsereturn Fib(n-1) + Fib(n-2);}#include <iostream>
using namespace std;int main()
{int n = 0;cin >> n;int ret = Fib(n);cout << ret << endl;return 0;}

但是当我们输入50时,程序不给结果了,这是为什么呢?

其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计 算,而且递归层次越深,冗余计算就会越多。

那我们可以优化为

#include <iostream>
using namespace std;
int count = 0;int Fib(int n)
{if(n == 3)count++;//统计第3个斐波那契数被计算的次数if(n <= 2)return 1;elsereturn Fib(n - 1) + Fib(n - 2);
}int main()
{int n = 0;cin >> n;//输入40 int ret = Fib(n);cout << ret << endl; cout << "count = " << count << endl;//输出 102334155//count = 39088169  return 0;}

这⾥我们看到了,在计算第 40 个斐波那契数的时候,使用递归方式,第 3 个斐波那契数就被重复计 算了 39088169 次,这些计算是非常常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。 我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了

这样就有下面的代码:

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;}

迭代的方式去实现这个代码,效率就要高出很多了。 所以在编写代码的时候,我们把要斟酌一下,有时候,使⽤递归很方便的写出代码,但是也会引入一 些问题,那么这种情况下,我们就要写出迭代的版本。

#include <iostream>
using namespace std;
int count = 0;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;}int main()
{int n = 0;cin >> n;//输入40 int ret = Fib(n);cout << ret << endl; cout << "count = " << count << endl;//输出 102334155//count = 0 return 0;}

好啦~,到此我们函数递归也是完结咯,断断续续的写了近一个星期,希望大家也能继续坚持呀,下一篇预告结构体~

http://www.dtcms.com/a/487435.html

相关文章:

  • Oracle AWR报告分析:诊断RAC Global cache log flush性能故障
  • python - 第四天
  • 领取流量网站药剂学教学网站的建设
  • 端端网站开发网络广告网站怎么做
  • threejs(五)纹理贴图、顶点UV坐标
  • debug - MDK - arm-none-eabi - 将MDK工程编译过程的所有命令行参数找出来
  • 网站怎么维护百度会收录双域名的网站么
  • Oracle数据库基本命令的8个模块
  • Vue3中的计算属性和监视属性【5】
  • Docker部署WordPress及相关配置
  • 大自然的网站设计营销型企业网站源码
  • 网站如何做线上支付功能免费刷推广链接的网站
  • 使用Flask部署PyTorch模型
  • 新版视频直播点播平台EasyDSS用视频能力破局!
  • python_视频切分
  • vscode 侧边文件夹名字体大一点
  • C++ 进阶特性深度解析:从友元、内部类到编译器优化与常性应用
  • Linux 线程与页表
  • 做产地证的网站江苏和住房建设厅网站
  • 西安网站制作开发深圳专业建站多少钱
  • QT for Android 安卓开发之调用Java程序
  • 攻防世界-Web-题目名称-文件包含
  • **云迁移之旅:探索发散创新的路径**随着云计算技术的日益成熟,越来越多的企业开始
  • 实例分割演进史:从Mask R-CNN到多模态通用分割(2017-2025)
  • 西安高端网站设计公司设一个网站需要多少钱
  • 石家庄平山网站推广优化大连外贸网站制作
  • 第一次作业
  • SAR信号处理重要工具-傅里叶变换(二)
  • 平面设计网站模板浏览不良网页的危害
  • e4a做网站python app开发