学C的第十天(继续深入学习函数:函数的嵌套调用,链式访问,声明和定义;函数递归:了解递归和其两个条件;练习:1.接收并打印整形值、2.时变不用临量求字符串长度、3.求n的阶乘、4.求第n个斐波那契数)-2

简介: 8.函数递归(难使用,会导致栈溢出):8.1 什么是递归:

8.函数递归(难使用,会导致栈溢出):

8.1 什么是递归:

程序调用自身的编程技巧称为递归(recurion)。


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


递归的主要思考方式在于:把大事化小


(递归和循环类似,但递归每次都要开辟空间,而循环每次都是使用固定的数据,所以循环不会让程序崩溃,而递归可能会让程序崩溃)

8.2 递归的两个必要条件:

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。

(两个条件有了不一定对没了一定错。)

(以下面练习1为例:)

def18b401486474ea77dbeca68097bf8.png

[不满足这两个条件的情况下,易导致 栈溢出(Stack overflow) ]

//递归
#include <stdio.h>
int main() 
{
  printf("hehe\n");
  // 递归:函数自己调用自己
  main();
  return 0;
}

85f03816e82c411cbe55f0f35b4ab82a.png

image.png

8.2.1 练习1:(画图理解)

接受一个整型值(无符号),按照顺序打印它的每一位

例如:

输入:1234 ; 输出:1 2 3 4

(画图理解:)


image.png

递归的 理解递推 会好些)

//递归 练习1:
#include <stdio.h>
void print(unsigned int n) // 1234
{
  if (n > 9) // 大于9说明是两位数
  {
    print(n / 10); // 1234进来后变为123
  }
  printf("%d ", n % 10); //  
}
int main() 
{
  unsigned int num = 0;
  //输入
  scanf("%d", &num);
  print(num); // 调用自定义函数
  return 0;
}

(递归过程涉及 函数的调用堆栈 ,也叫 函数栈帧 。)

补充:函数的调用堆栈函数栈帧

(下篇再进行详细补充)


6f6e2bdc518a46f9ab497d4c38671a60.png

[每一次函数调用都要创造函数栈帧,整个函数栈帧的空间都在栈区上(函数栈帧的开辟都是在栈区上进行的)]

image.jpeg

8.2.1 练习2:(画图理解)

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

使用临时变量的解法:)

//编写函数不允许创建临时变量,求字符串的长度
#include <stdio.h>
#include <string.h>
int my_strlen(char* s) // 返回字符串长度,所以返回类型是int
// 因为实参是数组名称,是地址,所以形参使用 char*
// 数组末尾包括 \0 , 求字符串长度是求 \0 前有多少个字符(注意)
{
  int count = 0; // 临时变量
  while (*s != '\0') // 如果没有到\0(结束标志),说明数组还有值,就继续循环
    // 使用 s指针 往后遍历字符串 ,字符串放在数组中是连续的
  {
    count++; // 加入循环说明有值,计数加1
    s++; // 让指针往后偏移,判断下一位
  }
  return count;
}
int main() 
{
  char arr[] = "abc"; 
  // 这个数组相当于 {a b c \0]
  int len = my_strlen(arr);
  // 调用自定义函数
  printf("%d\n", len); 
  // 字符数组的数组名是首元素的地址
  return 0;
}

eecdee632aef4022b89c48cdbb7329ca.png

无临时变量解法:)

// 使用递归
// my_strlen("abc") --> 1 + my_strlen("bc")
// --> 1 + 1 + my_strlen("c") --> 1 + 1 + 1 + my_strlen("\0") 
// --> 1 + 1 + 1 + 0 (\0 不计数 ,记为0)
// -> 把字符一个一个剥离下来(大事化小)
int my_strlen(char* s)
{
  if (*s == '\0')
  {
    return 0; // 首地址数为\0,说明为空数组
  }
  else
  {
    // 进入else说明首地址有值,所以先给个1
    return 1 + my_strlen(s + 1); // 使用递归
    // (s + 1)相当于让指针往后偏移,判断下一位
  }
}
int main() 
{
  char arr[] = "abc"; 
  // 这个数组相当于 {a b c \0]
  int len = my_strlen(arr);
  // 调用自定义函数
  printf("%d\n", len); 
  // 字符数组的数组名是首元素的地址
  return 0;
}

a71cb901a51f4d978f4c7835f9b16aeb.png

8.3 递归与迭代(循环就是一种迭代):

许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。

但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。

当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

8.3.1 练习3:

求n的阶乘。(不考虑溢出)

(使用迭代解法:)

// 求n的阶乘(迭代)
#include <stdio.h>
// 循环(迭代)
int Fac(int n)
{
  int r = 1;
  int i = 0;
  for ( i = 1; i <= n; i++ ) // 产生1-n的数
  {
    r = r * i; 
  }
  return r;
}
int main() 
{
  int n = 0;
  scanf("%d", &n);
  int ret = Fac(n); // 调用自定义函数
  printf("%d\n", ret);
  return 0;
}


(使用递归解法:)

image.png

(出现函数自己调用自己,使用递归

// 求n的阶乘(迭代)
#include <stdio.h>
// 递归
int Fac(int n)
{
  if (n <= 1)
  {
    return 1;
  }
  else
  {
    return n * Fac(n - 1);
  }
}
int main()
{
  int n = 0;
  scanf("%d", &n);
  int ret = Fac(n); // 调用自定义函数
  printf("%d\n", ret);
  return 0;
}

b5e878cdd74148aab4d256c16d085a52.png

8.3.2 练习4:

求第n个斐波那契数。(不考虑溢出)

9f8d3c9a89a648fe8a7908e72486dbd7.png

(出现函数自己调用自己,使用递归

// 求第n个斐波那契数
// 1 1 2 3 5 8 13 21 34 55 ...
// 前2个的数的和是第三个数
#include <stdio.h>
int Fib(int n)
{
  if (n <= 2)
  {
    return 1; // 前两个斐波那契数都是1
  }
  else
  {
    return Fib(n - 1) + Fib(n - 2);
  }
}
int main() 
{
  int n = 0;
  scanf("%d", &n);
  int ret = Fib(n); // 调用自定义函数
  printf("%d\n", ret);
  return 0;
}

image.png

(上面解法有弊端)


栈溢出:


在调试 Fib 函数的时候,如果你的参数比较大,


那就会报错: stack overflow(栈溢出) 这样的信息。


系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者死递归,


这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

8c09a1339e164306be828ded491d00dd.png

64590ed443744ed28fd5a389e4fc0154.png

递归反倒使计算量变多了,递推出去成千上万个分支,再回归成千上万个分支,效率太低了)

(使用迭代的解法:)

// 求第n个斐波那契数(迭代)
// 1 1 2 3 5 8 13 21 34 55 ...
// 前2个的数的和是第三个数
#include <stdio.h>
int Fib(int n)
{
  int a = 1;
  int b = 1;
  int c = 1;
  while (n >= 3)
  {
    c = a + b;
    a = b;
    b = c;
    n--;
  }
  return c;
}
int main() 
{
  int n = 0;
  scanf("%d", &n);
  int ret = Fib(n); // 调用自定义函数
  printf("%d\n", ret);
  return 0;
}

image.png

(值太大了,所以结果不对,此题是不考虑溢出

相关文章
|
域名解析 监控 算法
阿里云拨测:主动探测Web应用质量,助力提升用户体验
阿里云拨测是一种针对互联网应用(Web页面、网络链路等)进行应用性能和用户体验监测的服务,无需嵌码即可为云上用户提供开箱即用的企业级主动拨测式应用监测解决方案。
8315 111
阿里云拨测:主动探测Web应用质量,助力提升用户体验
|
应用服务中间件 网络安全 nginx
Nginx学习研究-Nginx 安装 SSL 配置 HTTPS
Nginx学习研究-Nginx 安装 SSL 配置 HTTPS
614 0
|
人工智能 自然语言处理 前端开发
产品经理也能“开发”需求?淘宝信息流从需求到上线的AI端到端实践
淘宝推荐信息流业务,常年被“需求多、技术栈杂、协作慢”困扰,需求上线周期动辄一周。WaterFlow——一套 AI 驱动的端到端开发新实践,让部分需求两天内上线,甚至产品经理也能“自产自销”需求。短短数月,已落地 30+ 需求、自动生成 5.4 万行代码,大幅提升研发效率。接下来,我们将揭秘它是如何落地并改变协作模式的。
311 37
产品经理也能“开发”需求?淘宝信息流从需求到上线的AI端到端实践
|
存储 Kubernetes 块存储
kubernetes的简单化数据存储StorageClass(建立和删除以及初步使用)(一)
kubernetes的简单化数据存储StorageClass(建立和删除以及初步使用)
747 0
|
7月前
|
机器学习/深度学习 人工智能 自然语言处理
人工智能技术的探讨
人工智能的概念,人工智能的发展,人工智能的各种学派,人工智能的应用领域
319 4
|
Kubernetes Cloud Native 安全
阿里云原生容器服务产品体系-ACK Pro 托管集群
阿里云原生容器服务产品体系-ACK Pro 托管集群
阿里云原生容器服务产品体系-ACK Pro 托管集群
|
12月前
|
机器学习/深度学习 人工智能 缓存
【AI系统】GPU 基础
GPU,即图形处理器,是计算机系统中处理图形和图像的核心组件,从早期的简单图形加速到如今的高性能计算和深度学习加速,GPU 经历了显著的技术革新。本文将介绍 GPU 的发展历程、与 CPU 的区别、在 AI 领域的关键作用及其在游戏、消费电子、自动驾驶等多个领域的广泛应用。
659 4
|
运维 Devops API
阿里云云效操作报错合集之调用api报错:没有权限,是什么原因
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
|
网络协议 Unix Linux
docker部署Portainer
Portainer可以在Docker上运行,而且部署起来非常简单 Portainer是Docker的图形化管理工具,提供状态显示面板、应用模板快速部署、容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作)、事件日志显示、容器控制台操作、Swarm集群和服务等集中管理和操作、登录用户管理和控制等功能。功能十分全面,基本能满足中小型单位对容器管理的全部需求
docker部署Portainer
|
存储 Python
理解云存储的成本结构与计费模式
【6月更文挑战第1天】云存储成本结构复杂,包括存储容量、数据传输和请求次数的费用。计费模式多样,如按用量、订阅或峰值计费。通过Python示例展示了上传下载文件操作。理解并合理选择云存储方案,避免不必要的费用,成为云存储的明智使用者。一起来探索这个“魔法盒子”吧!
264 1