C语言学习-函数

学习 专栏收录该内容
18 篇文章 2 订阅


在这里插入图片描述

函数介绍

函数是什么?

函数是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性

一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

为什么使用函数?

在编写代码的过程中,经常会重复需求某些相同的功能,如果每一次功能的实现都需要编写一段相关的代码,那么对于程序员来说工作量是十分巨大的,同时也增加了整个工程的复杂程序、降低了代码的可读性。而如果使用函数来实现功能,可以编写合适的代码封装为函数,哪里需要就哪里调用,省去重复编写相同代码的工作,降低了工程的复杂程度,提高了代码的可读性,同时也将整个工程模块化,利于后期的维护和修改。

int Add(int x, int y) // 形参 
{
	return x + y; // 返回值 int类型
}
int main()

{
	int a = 10;
	int b = 20;
	int sum = Add(a,b); // 实参 
	printf("%d\n", sum); 
	return 0;
}

C语言函数的分类

C语言中函数分为 库函数自定义函数

库函数

C语言提供给程序员使用的一系列具备基础通用功能的函数,C语言本身并没有库函数,但是随着C语言的发展,程序员在编写程序时会经常使用到一些通用的功能,这些功能每个程序员都可能有需求(例如 输出、输入、字符串操作、公式计算等等…),但是每个程序员对功能的实现不同,因此C语言将这些具备通用的功能整合为库函数提供给开发人员使用,提高了系统可移植性和程序开发的效率。

C语言常用的库函数

  • IO函数(输入输出函数)
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

头文件

C语言的库函数在使用时需要先引用相应的头文件。

// 预处理指令 <头文件名>
#include <stdio.h>

在这里插入图片描述

利用文档学习

对于库函数的学习可以在线查找文档,例如:

cplusplus.com - The C++ Resources Network

https://zh.cppreference.com/w/c

举例

  1. strcpy
char * strcpy ( char * destination, const char * source );

将源地址指向的字符串复制到目的地指向的数组中,复制包括字符串结束标志\0,并在此位置结束。

strcpy 函数返回类型是 char * ,指针,返回值是目的地地址。

参数

  • 源地址
  • 目的地地址
#include <stdio.h>
#include <string.h> // strcpy()
int main()
{
	char str1[] = "abc";
	char str2[] ="#######";
	// strcpy(目标地址,源地址) 源的长度要小于目标的长度,不然会报BUG
	strcpy(str2, str1); // 数组本质上存放的是地址
	printf("%s\n", str2);
	return 0;
}

通过调试可以看到,strcpy 会将 str1 的所有字符全部拷贝至str2,并在\0结束,\0之后的内容不会被打印。

在这里插入图片描述

在这里插入图片描述

  1. memset
void * memset ( void * ptr, int value, size_t num );

从内存块空间开始位置往后设置n个字节的字符,返回值是ptr。

参数

  • ptr

    被设置的内存块的地址。

  • value

    被填充进内存块里的值,以整型数值传递,但被解释为无符号字符型。

  • num

    被设置的字节个数,size_t == unsigned int。

int main()
{
	char arr[] = "hello world";
    //memset(ptr,value,num)
    // The value is passed as an `int` 
	memset(arr, 5, 5); 
	printf("%s", arr);  
	return 0; 
}

在这里插入图片描述

自定义函数

程序员根据不同的任务需求,自己编写的函数。自定义函数和库函数一样,有函数名、返回值类型和函数参数。

函数的组成

函数通常由函数名、参数、函数体、返回值类型组成

// 定义函数
// 返回值类型  函数名  (参数)
ret_type fun_name (para1, *) // 形参
{
   statement; // 函数体, 交代函数的实现
}
fun_name(para1, *); //函数调用 ,实参

举例

  1. 函数实现 找出2个数的较大值
// 定义函数
int Max(int x, int y)
{
	return x > y ? x : y;
}
int main()
{
    
	int a = 10;
	int b = 20;
	int max = Max(a, b); // 函数调用
	printf("%d\n", max);
    
	return 0;
}
  1. 写一个函数可以交换两个变量的内容
// 交换内容不需要返回值。
void Swap(int* pa, int* pb)// 利用指针变量接收地址
{                          // 解引用操作
    int tmp = *pa;         // *pa代表的就是main函数里的a
    *pa = *pb;             // *pb 同理
    *pb = tmp;             // 通过解引用操作在函数内部曹操纵外部变量。
}
int main()
{
    int a = 10;
    int b = 20;
    Swap(&a, &b);          // 将a、b的地址传递给函数
    printf("a = %d\nb = %d\n", a, b);
    return 0;
}

在这里插入图片描述

函数的使用

函数的使用一般分成3个步骤,函数声明函数调用函数定义

下面的代码是一个完整的函数使用。

int Add(int x,int y);  // 函数声明  x和y可以省略

int main()
{
    int a = 10;
    int b = 20;
    int sum = 0;
    sum = Add(a,b);    // 函数调用
    return 0;
}


int Add(int x, int y)  // 函数定义
{
    return x + y;
}

函数声明

上面例子中的第一行的代码就是一个函数的声明。

int Add(int x,int y);
//返回值类型  函数名(参数1,参数2...);

告诉编译器有个Add函数,他有两个整型参数,函数的返回值是整型。

  1. 函数声明是告知编译器有一个函数,它们函数名是什么,它的参数是什么,以及它的返回类型是什么,至于函数存不存在,并不重要。
  2. 在函数声明中并没有实际创建变量。
  3. 函数声明一般出现在函数调用之前。要满足先声明后使用。
  4. 函数声明一般放在头文件中。

函数调用

上面第8行代码就是一个函数调用。

Add(a,b);
//函数名 (实参1,实参2...);
  1. 当程序执行到函数调用语句是,会调用函数的定义并执行函数定义中的内容。
  2. 函数调用本质上是一个表达式语句,圆括号是运算符,括号左边的函数名是运算对象。在C11标准中,这种表达式是一种后缀表达式。
  3. 函数调用由函数名和实参组成(实参后面会介绍)
  4. 函数调用又有传值调用和传址调用两种用法(后面会介绍)。

传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参,如例1。

传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式,如例2。

这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量

两种调用的使用时机

传值调用:当调用函数不考虑改变外部变量时。

int get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{// 这里只是求两数的最大值,并将结果返回给接收变量max,并没有改变原来外部变量a和b的值
	int a = 10;
	int b = 20;
	int max = get_max(a,b); 
	printf("max=%d\n", max); // max=20
	max = get_max(100, 200);
	printf("max=%d\n", max); // max=200
	return 0;
}

**传址调用:**当调用函数考虑通过操纵函数内部变量来改变外部变量时。

// 写一个函数可以交换两个整形变量的内容
void Swap(int* x, int* y) 
{
	int tmp = 0;
	tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{// 这里需要改变交换a和b的值,而如果是传值操作,x,y与a,b不在同一内存空间,只能是实参a,b将值赋给形参下x,y 不能逆向操作,因此使用传址操作将a,b的地址传给指针变量x,y,同过解引用操作来远程控制a,b值得互换
	int a = 10;
	int b = 20;
	Swap(&a, &b); 
	printf("a=%d,b=%d", a, b);
	return 0;
}

函数定义

上面13~16行就是一个函数定义

int Add(int x, int y)  // 函数定义
{
    return x + y;
}
  1. 当函数被调用,会执行函数定义里的语句。
  2. 函数定义通常是由,返回值类型,函数名,形参,函数体组成。
  3. 函数定义里的形参与函数体里的变量都是局部变量,不会与函数外的参数冲突。
    在这里插入图片描述

注意:函数定义的每个形参都必须指定类型。

int add(int x,y)      // 错误
int add(int x, int y) // 正确

函数声明的使用场景

我们经常在代码中省略函数声明,它似乎显得有些多此一举,其实函数声明的正真使用场景是当有一些功能需要被模块化时,此时的函数定义应该单独写在一个.c源文件中,而要使用这个函数就需要创建一个.h的头文件并在其中声明这个函数。

例如,将一个实现加法的函数,单独写在add.c文件中。
在这里插入图片描述

并创建一个对应的add.h头文件,在其中声明这个变量。
在这里插入图片描述

其他源文件想使用,需要用#include,引用这个头文件。自定义的头文件名用"" 引发括起来,这点和库函数的引头文件方式有所差异。
在这里插入图片描述

函数的参数

实参

实参是写在函数调用内的参数,实参的值传递给函数定义中的形参,实参可以是:常量变量表达式函数等。无论实参是何种类型的变量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形参

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中猜实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁。因此形式参数只在函数中有效。

ret_type fun_name (形参1, ...) 
{
   statement; 
}
fun_name(实参1..); 

当实参传递值给形参时(实例化),形参其实是实参的一份临时拷贝,对形参的修改是不会改变实参的。

在这里插入图片描述
在这里插入图片描述

练习

函数实现判断素数

#include <stdio.h>
#include <math.h>
int is_Prime(int x)
{  
      int j = 0;
      if (x < 2)
      {
          return 0;
      }
      else {
          for (j = 2; j <= sqrt(x); j++)
          {
              if (x % j == 0)
              {
                  return 0;
              }
          }
      }
          return 1;         
}
int main()
{
    int count = 0;
    for (int i = 101; i <= 199; i += 2)
    {
        if (is_Prime(i))
        {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n%d\n", count);
    return 0;
}
// 计算范围内素数的个数
#include <stdio.h>
#include <math.h>

int count_prime(int x, int y)
{
	int i = 0;
	int j = 0;
	int count = 0;
	if (2 == x)                               // 判断最小值是否为2,如果是count++(2为素数);
	{
		count++;
	}
	x = x % 2 == 0 ? x + 1 : x;               // 判断x是不是偶数,是偶数+1,跳过所有偶数 
	y = y & 2 == 0 ? y - 1 : y;               // 判断y是不是偶数,是偶数-1,跳过所有偶数

	for (i = x; i <= y; i += 2)               // 在指定范围内,循环遍历所有奇数
	{
		for (j = 3; j <= sqrt(i); j += 2)    // 试除 3~sqrt(i) 之间的所有奇数  
		{
			if (i % j == 0)                  // 判断为真,则是合数,跳出内存循环,判断下一个数
			{
				break;
			}
		}
		if (j > sqrt(i))                     // 只有当j > sqrt(i) 时,说明没有能整除i的数,则是素数。
		{
			count++;
		}
	}

	return count;                           // 返回范围内的素数的数量
    
}
int main()
{
	int min = 0;
	int max = 0;
	printf("请输入最小值和最大值中间用\",\"分隔>:");
	scanf("%d,%d", &min, &max);
	int count = count_prime(min, max);
	printf("\n%d\n", count);
	return 0;
}

函数实现 判断闰年

// 判断是否为闰年
int is_leap_year(int y)
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    {
        return 1;
    }
    return 0;
}
int main()
{
    int year = 0;
    scanf("%d", &year);
    if (is_leap_year(year))
    {
        printf("%d是闰年", year);
    }
    else
    {
        printf("%d是平年", year);
    }
    return 0;
}
// 统计范围内的闰年
int is_leap_year(int y)
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    {
        return 1;
    }
    return 0;
}

int main()
{
    int i = 0;
    int count = 0;
    for (i = 1000; i <= 2000; i += 2)
    {
        if (is_leap_year(i))
        {
            printf("%d ", i);
            count++;
        }
    }

    printf("\ncount = %d\n", count);
}

函数实现 整型有序数组二分查找(重要)

// 写函数 先考虑函数如何使用,在考虑函数如何实现
// 函数里的形参arr本质上是一个指针,存放的是arr[0]的地址
// []相当于解引用操作
int binary_search(int arr[],int k,int sz)
{
	// 2.数组传参不能使用下面的方法计算元素个数
	// sizeof计算指针大小为4byte(x86)/8byte(x64),而arr[0]是int类型也是4byte
	// 因此sz得到的结果为 1, 这明显是错误的
	// 可以将计算公式放在函数外完成
	//int sz = sizeof(arr) / sizeof(arr[0]);
	int left = 0;
	int right = sz -1;
    
	while (left <= right)
	{
		int mid = (left + right) / 2; 
		if (k > arr[mid])
			left = mid + 1; 
		else if (k < arr[mid])
			right = mid - 1; 
		else
			return mid;
	}
		return -1; // 当找不时返回-1,返回负数是因为下标不会小于0,所有不会和下标冲突。
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;
	// 3. 在函数外实现计算数组元素个数
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("请输入1-10的值\n");
	scanf("%d", &k);
	
	// 1. 实参arr实际上传给形参的是数组"首元素"的地址
	int idx = binary_search(arr,k,sz);
	if (idx == -1)
		printf("该数字不存在于数组中");
	else
      	printf("数字%d的下标是:%d\n", k, idx);
	return 0;
}

写一个函数,每调用一次这个函数,num+1

void slef_inc(int* p_num)
{
    // *p_num++; // * 和 后置++ 都是操作符 ,后置++优先级高于*,但是后置++是先使用后自增,因此得到的结果为0;
    // 正确写法
    (*p_num)++; //  这两种写法都是可以,()的运算级高于++
    // ++*p_num;  // 前置自增是先自增再使用
}
int main()
{
    int num = 0;
    slef_inc(&num);
    printf("%d\n", num); // 0
    slef_inc(&num);
    printf("%d\n", num); // 0
    slef_inc(&num);
    printf("%d\n", num); // 0
    return 0;
}

函数的嵌套调用和链式访问

嵌套调用

函数可以嵌套调用另一个函数。

#include <stdio.h>
void new_line()
{
    printf("hehe\n");
}
void three_line()
{
    int i = 0;
    for(i=0;i<3;i++)
    {
 		new_line(); // three_line函数 中调用 new_line函数
    }
}
int main()
{
    three_line(); // main函数 中调用 three_line函数
    return 0;
}

链式访问

链式访问是把一个函数的返回值,作为另外一个函数的参数。

int main()
{
    printf("%d\n", strlen("abc"));
    // strlen的返回值是一个整型,作为printf的参数
}

下面代码的输出结果是?

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));
    // printf的返回值是打印字符的数量,
    // 最内层printf先打印43,返回值为2
    // 第二个printf继续打印上一个printf的返回值,也就是2,返回值为1
    // 第一个printf打印打印第二个printf的返回值,也就是1
    // 结果为4321
    return 0;
}

intf(“hehe\n”);
}
void three_line()
{
int i = 0;
for(i=0;i<3;i++)
{
new_line(); // three_line函数 中调用 new_line函数
}
}
int main()
{
three_line(); // main函数 中调用 three_line函数
return 0;
}


## 链式访问

链式访问是把一个函数的返回值,作为另外一个函数的参数。

```c
int main()
{
    printf("%d\n", strlen("abc"));
    // strlen的返回值是一个整型,作为printf的参数
}

下面代码的输出结果是?

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));
    // printf的返回值是打印字符的数量,
    // 最内层printf先打印43,返回值为2
    // 第二个printf继续打印上一个printf的返回值,也就是2,返回值为1
    // 第一个printf打印打印第二个printf的返回值,也就是1
    // 结果为4321
    return 0;
}

函数递归

C语言允许函数自己调用自己,这个调用得过程被称作递归。递归作为一种算法,在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法。它通常把一个大型复杂得问题层层转化为一个与原问题相似但规模较小得问题来求解,递归策略只需要少量得程序就可以描述出解题过程所需要得多次重复计算,大大减少了程序的代码量。递归的主要思考方式在于:把大事化小

实现过程

通过下面的代码来演示函数递归的实现过程

void Recur(int);
int main()
{
    int a = 1;
    Recur(a);
    return 0;
}
void Recur(int x)
{
    printf("第%d次递归,x地址为%p\n", x, &x);
    if (x < 4)
    {
        Recur(x + 1);
    }
    printf("第%d次递归,x地址为%p\n", x, &x);
    return;
}

执行过程

  1. 从main函数进入,调用函数Recur(a),将实参a(1)的值传给形参x,执行函数体内容,打印第一段文字。(此时为第一次调用)
  2. 判断x<4, 成立,执行if语句,调用函数Recur(a+1)。
  3. 将实参a+1(2) 的值传给形参x,执行函数体内容,打印第一段文字。(此时为第二次调用)
  4. 再次判断x<4,成立,执行if语句,调用函数Recur(a+1)。
  5. 将实参a+1(3) 的值传给形参x,执行函数体内容,打印第一段文字。(此时为第三次调用)
  6. 再次判断x<4,成立,执行if语句,调用函数Recur(a+1)。
  7. 将实参a+1(4) 的值传给形参x,执行函数体内容,打印第一段文字。(此时为第四次调用)
  8. 再次判断x<4,不成立,不执行if语句,执行if后面的语句,打印第二段文字,第四次被调函数执行完毕。
  9. 被调函数执行完毕后会返回主调函数,而第四次被调函数的主调函数是第三次被调函数。因此返回上一级。
  10. 当返回第三次被调函数时,继续执行函数调用后的语句,打印第二段文字,第三次被调函数执行完毕。
  11. 返回第二次被调函数,继续执行函数调用后的语句,打印第二段文字,第二次被调函数执行完毕。
  12. 返回第一次被调函数,继续执行函数调用后的语句,打印第二段文字,第一次被调函数执行完毕。
  13. 返回main函数,执行return 0; 工程结束。

注意

每一次函数递归所创建的变量属于本级函数中的私有变量,作用域仅存在于本级函数中。

通过打印结果的地址可以看出,前面的次数相同,后面的地址就相同,在这相同的两次打印中的使用的变量x,其实是同一个变量x,它们的值相同,所指向的内存空间也相同。

限制条件

在上面的例子中使用了限制条件if (x < 4) 来限制递归的次数,如果不加这个限制条件程序会如何运行呢?

下面代码演示

void Recur(int);
int main()
{
    int a = 1;
    Recur(a);
    return 0;
}
void Recur(int x)
{
    printf("第%d次递归,x地址为%p\n", x, &x);
    Recur(x + 1);
    printf("第%d次递归,x地址为%p\n", x, &x);
    return;
}

在这里插入图片描述

从执行结果可以看到,因为没有条件的限制,递归一直重复执行下去,直到栈溢出(Stackoverflow)。

栈溢出

栈溢出是由于C语言中没有内置检查机制来限制复制到缓冲区中数据的大小,当数据大于缓冲区的容量时,将会溢出缓存区。

C语言中将内存划分为3个区域:

  1. 栈区

    栈区存放局部变量,函数形参等。

  2. 堆区

    堆区存放动态开辟的内存。

  3. 静态区。

    静态区存放全局变量,static修饰的变量等。

而递归函数中的形参就存放在栈区,每次递归都需要在栈区开辟一块空间,当栈区存放不下时,就会发生栈溢出现象。

总结

从这个例子可以看出,在进行函数递归时必须要设置限制条件,防止递归无限循环,发生栈溢出,并且在每次递归调用之后要越来接近这个限制条件

练习

顺序打印整型每一位数

描述

接收一个整型值(无符号),按照从高位到低位的顺序打印它的每一位。

分析

  1. 创建无符号整型数num接收一个值。
  2. 通过递归的方式,让n不断的除以10,直到n<=9时,在这个级别下的n是个位数,是num的最高位数。
  3. 当n<=9时不在继续递归,而是打印输出 n%10 得到当前级别下n的个位数,结束本级递归,返回上一级递归。上一级递归如此往复,直到回到main。

代码实现

void print_num(int n)
{
    if (n > 9 ) // n > 9 说明 n不是个位数,继续除
    {
        print_num(n / 10); // n / 10 不断获得
    }
    printf("%d ", n % 10); // 只有当判断语句里的函数递归结束才会执行这行代码
}

int main()
{
    unsigned int num = 0;
    scanf("%d", &num);
    print_num(num);
        return 0;
}

在这里插入图片描述

求字符串长度

描述

编写函数不允许创建临时变量,求字符串的长度。

分析

如果不用strlen计算字符串长度,先不考虑不允许创建临时变量的条件,可以进行如下的步骤:

  1. 首先创建一个字符数组来存放一个字符串。

  2. 创建一个函数实现计算字符串长度的功能。

  3. 将这个字符数组变量作为实参传递给形参。

    我们知道数组变量里存放的是第一个元素的地址,因此形参需要创建指针变量来接收这个地址。

    此时在解引用这个变量,就可以找到数组的第一个元素。

  4. 创建一个计数器变量来计数。

  5. 通过while循环来遍历数组里的每个元素来对比是否为’\0’,不是则count+1,并继续循环。

  6. 传过来的p_char是一个地址,数组里的每一个元素的地址都是连续的,因此p_char+1就可以访问下一个元素的之地,再解引用来判断是否为’\0’

  7. 直到被遍历的元素 == ‘\0’, 也就标志着数组结尾,不再进入while循环,返回count的值即为字符串长度。

my_strlen(char *p_char)      //3.  指针变量接收地址
{
    int count = 0;          //4. 创建计数器遍历
    while(*p_char != '\0')  //5. 解引用当前地址指向的元素与'\0'对比
    {
        count++;            //5. 条件为真count++
        p_char++;           //6. p_char里面存放的为地址,+1就是下一个元素的地址。
    }
    return count;           //7.
}
int main()
{
    char arr[] = "bit";     // 1. 创建字符串数组
    my_strlen(arr);         // 2. arr存放的是第一个元素的地址
    
    return 0;
}

如果不创建指针变量:

  1. 假设数组没有元素时,进入函数可以直接return 0;假如只有1个元素时,进入函数return 1;
  2. 因此在函数内可以使用双分支来判断被访问的数组元素 是否 !=0 ,为真返回1,为假返回0。
  3. 但是这种判断方式在数组元素>1时就不在准确,所以我们需要在条件为真的语句中,使用递归访问数组里的每一个元素。
  4. 每次访问一个元素都记为 1 + 剩下元素的长度。
  5. 而剩下元素长度的计算需要继续访问下一个元素,直到遇到\0 才返回, 所以用函数调用(指针变量+1) 的方法来访问下一个元素。
  6. 不断的循环调用这个函数,就等于是剩余元素的长度。
  7. 因此将返回值 设置为return 1 + 函数调用(指针变量+1)。
  8. 只要条件成立就不断调用函数,直到遇到 ‘\0’, 条件不成立,此时这一级函数的返回值为0 ,回到上一级函数 return 1 + 0, 继续返回上一级函数 return 1 + (1 + 0) … 一直返回直到回到最初的主调函数。则此时函数的返回值就是字符串的长度。
int my_strlen(char *p_char)           
{
    if (*p_char != '\0')             
    {
         //  1 +  剩余字符串长度
      return 1 + my_strlen(p_char+1);    
                                      
    }
    else {
        return 0;
    }
}
int main()
{
    char arr[] = "bit";
    int len = my_strlen(arr);
    printf("%d\n", len);
    return 0;
}
// 把大事化小
// my_strlen("bit");
// 1 + my_strlen("it");
// 1 + 1 + my_strlen("t");
// 1 + 1 + 1 + mu_strlen('\0');  遇到 '\0' 不再调用这次函数。
// 得到3 

求n的阶乘

分析

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

  1. 利用公式可以将的特性,可以在递归函数的实参设置为n-1。
  2. 然后 return n * n - 1,返回值即为n的阶乘。
int get_fact(int n)
{
    if (n > 1)
    {
        return n * get_fact(n - 1);
    }
    else
    {
        return 1;

    }
}
int main()
{
    int n = 0;
    scanf("%d", &n);
    if (n < 13)
    {
        printf("%d\n", get_fact(n));
    }
    return 0;
}

求第n个斐波那契数

分析

  1. 斐波那契数Fn = F n-1 + F n-2 (n > 2,F1 = 1 ,F2 = 1)

    F1 + F2 = F3 <=> 1 + 1 = 2

    F2 + F3 = F4 <=> 1 + 2 = 3

    F3 + F4 = F5 <=> 2 + 3 = 5

    F4 + F5 = F6 <=> 3 + 5 = 8

    F n-1 + F n-2 = Fn

  2. 利用公式特性,在函数内部设置判断条件

    (1) n < 3 则 return 1;

    (2) n > 2 则 return F n-1 + F n-2 ;

  3. 每一次递归调用函数都会调用调用自身两次,一次n-1 一次n-2 ,直到每一个递归函数内的 n<3

  4. 直到分裂出的所有函数内的 n 都满足n < 3 所有值然后 return 1。

  5. 上一级函数的返回值是下一级两个函数返回值的和。

代码实现

int fib(int n)
{
	return	n < 3 ? 1 : fib(n - 1) + fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int fib_num = 0;
	//TDD - 测试驱动开发
	fib_num = fib(n);
	printf("%d\n" , fib_num);
	return 0;
}

上面的算法在计算n为较大数时,效率十分低,因为每个分裂出去的子函数都会重复计算许多次相同的值。

int count = 0;
long long fib(int n)
{
	if (n == 3) 
	{
		count++;// 统计第三个斐波那契数被计算了多少次
	}
	return	n < 3 ? 1 : fib(n - 1) + fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	long long fib_num = 0;
	//TDD - 测试驱动开发
	fib_num = fib(n);
	printf("%d\n", fib_num);
	printf("count = %d\n", count);
	return 0;
}

统计第3个斐波那契数被计算的次数有512559680次,可以看出使用递归来计算这类问题十分繁琐,效率也很低。

而使用循环可以很简单的解决相同的问题。

int fib(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    for (int i = 3; i <= n; i++)
    {
        c = a + b;
        a = b;
        b = c;
    }
    return c;
}
int main()
{
    int n = 0;
    scanf("%d", &n);
    long long fib_num = 0;
    fib_num = fib(n);
    printf("%d\n", fib_num);
    return 0;
}

  • 8
    点赞
  • 3
    评论
  • 2
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 3 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页

打赏作者

庸人冲

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值