C语言函数、函数声明、定义、递归、汉诺塔问题、青蛙跳台阶问题

1.函数的定义

函数就是子程序

2.C语言函数的分类

  1. 库函数
  2. 自定义函数

2.1 库函数:

为什么会有库函数?

  1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
  2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
  3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。

2.2自定义函数

所有更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
函数的组成:

ret_type fun_name(para1, * )
{
 statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1    函数参数

创造求较大值函数get_max(int x,int y):

#include<stdio.h>
int get_max(int x, int y) 
{
	return(x > y ? x : y);
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);

	//求最大值
	int max = get_max(a, b);
	printf("max = %d\n", max);
	return 0;
}

在这里插入图片描述

写一个函数可以交换两个整形变量的内容。

#include<stdio.h>
void Swap(int x, int y) 
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}
int main()
{
	int a = 10;
	int b = 20;
	
	printf("交换前:a=%d b=%d\n", a, b);
	//函数
	Swap(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

很多人会这样写,但是会达不到效果,看一下运行结果吧
在这里插入图片描述
发现a和b的值并没有交换,为什呢?

  1. 实参a和b传给形参x和y的时候,形参将是实参的临时拷贝
  2. 改变形参变量x和y,不会影响实参a和b

那我们就传实参的地址给形参

 #include<stdio.h>
void Swap(int* px, int* py)
{
	int z = 0;
	z = *px;
	*px = *py;
	*py = z;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a=%d b=%d\n", a, b);
	//函数
	Swap(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

在这里插入图片描述
可以发现,a和b的值交换成功

3.函数的参数

3.1 实际参数(实参):

  1. 真实传给函数的参数,叫实参。
  2. 实参可以是:常量、变量、表达式、函数等。
  3. 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

3.2形式参数(形参):

  1. 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
  2. 上面Swap1和Swap2函数中的参数 x,y,px,py 都是形式参数。在main函数中传给Swap1的num1,num2和传给Swap2函数的&num1,&num2是实际参数。

4.函数的调用:

4.1 传值调用

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

4.2 传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

4.3 练习

  1. 写一个函数可以判断一个数是不是素数。
//1. 写一个函数可以判断一个数是不是素数。
#include<stdio.h>
#include<math.h>
int is_prime(int n)
{
	//试除法
	//2--n-1
	//2--sqrt(n)
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)
	{
		if (n % j == 0)
			return 0;
	}
	return 1;
}
int main()
{
	//100--200之间的素数
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断是否为素数
		if (is_prime(i) == 1)
		{
			printf("%d ", i);
		}
	}
	return 0;
}

在这里插入图片描述
2. 写一个函数判断一年是不是闰年。
写一个函数is_leap_year(int n)判断n是否为闰年,是闰年返回1,不是闰年返回0;根据返回结果打印闰年

#include<stdio.h>
int is_leap_year(int year)
{
	if ((year % 100 != 0 && year % 4 == 0) || (year % 400 == 0))
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

int main()
{
	//1000--2000之间的闰年
	int y = 0;
	for (y = 1000; y <= 2000; y++)
	{
		//判断是否为闰年
		//如果是闰年,返回1
		//不是闰年,返回0
		if (is_leap_year(y) == 1)
		{
			printf("%d ", y);
		}
	}
	return 0;
}

在这里插入图片描述
3. 写一个函数,实现一个整形有序数组的二分查找。
写查找函数int binary_search(int arr[],int n, int size)查找k=7,找到了返回下标,找不到返回-1

第一种写法:

//3. 写一个函数,实现一个整形有序数组的二分查找。
#include<stdio.h>
int binary_search(int arr[],int n, int size)
{
	int left = 0;
	int right = size - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (n < arr[mid])
			right = mid - 1;
		else if (n > arr[mid])
			left = mid + 1;
		else
			return mid;
	}
	return -1;
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;
	//计算数组元素的个数
	int sz = sizeof(arr) / sizeof(int);

	//找到了返回下标
	//找不到返回-1
	int ret = binary_search(arr, k, sz);//测试驱动开发
	if (ret == -1)
		printf("找不到\n");
	else
		printf("找到了,下标是:%d\n",ret);
	return 0;
}

在这里插入图片描述

第二种写法:

#include<stdio.h>
int binary_search(int arr[], int n, int left,int right)
{
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (n < arr[mid])
			right = mid - 1;
		else if (n > arr[mid])
			left = mid + 1;
		else
			return mid;
	}
	return -1;
}


int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 3;
	//计算数组元素的个数
	int sz = sizeof(arr) / sizeof(int);

	//找到了返回下标
	//找不到返回-1
	int ret = binary_search(arr, k, 0, 9);//测试驱动开发
	if (ret == -1)
		printf("找不到\n");
	else
		printf("找到了,下标是:%d\n", ret);
	return 0;
}

在这里插入图片描述

  1. 写一个函数,每调用一次这个函数,就会将num的值增加1。
    题目的意思就是函数可以改变变量num的值
#include<stdio.h>
void Add(int* p)
{
	*p = *p + 1;
}
int main()
{
	int num = 0;
	Add(&num);
	printf("%d\n", num);//1
	Add(&num);
	printf("%d\n", num);//2
	Add(&num);
	printf("%d\n", num);//3
	return 0;
}

在这里插入图片描述
或者:

#include<stdio.h>
int Add(int n)
{
	return n + 1;
}
int main()
{
	int num = 0;
	num = Add(num);
	printf("%d\n", num);//1
	num = Add(num);;
	printf("%d\n", num);//2
	num = Add(num);
	printf("%d\n", num);//3
	return 0;
}

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

函数和函数之间可以有机的组合的。

5.1嵌套调用

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

函数可以嵌套调用,但是不能嵌套定义。

5.2 链式访问

把一个函数的返回值作为另外一个函数的参数。

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[20] = "hello";
	int ret = strlen(strcat(arr, "bit"));//这里介绍一下strlen函数
	printf("%d\n", ret);
	return 0;
}

在这里插入图片描述

#include <stdio.h>
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));
	//结果是啥?
	//注:printf函数的返回值是打印在屏幕上字符的个数
	return 0;
}

printf返回在屏幕打印字符的个数,

printf("%d", printf("%d", printf("%d", 43)));
printf("%d", printf("%d", 2));
printf("%d", 1)

在这里插入图片描述

6. 函数的声明和定义

6.1 函数声明:

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
  3. 函数的声明一般要放在头文件中的。

6.2 函数定义:

函数的定义是指函数的具体实现,交待函数的功能实现

//test.h的内容
//放置函数的声明
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
//test.c的内容
//放置函数的实现
#include "test.h"
//函数Add的实现
int Add(int x, int y)
 {
 return x+y; 
 }

7. 函数递归

7.1 什么是递归?

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

7.2 递归的两个必要条件

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

7.2.1 练习1:

接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4.
参考代码:

#include <stdio.h>
void print(int n) {
 if(n>9)
 {
 print(n/10);
 }
 printf("%d ", n%10);
}
int main()
{
 int num = 1234;
 print(num);
 return 0; 
 }

在这里插入图片描述

7.2.2 练习2:

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

//递归求字符串长度
#include<stdio.h>
int my_strlen(char* s)
{
	if (*s != '\0')
	{
		return 1 + my_strlen(s + 1);
	}
	else
	{
		return 0;
	}
}
int main() {
	//求字符串长度
	char arr[10] = "abcdef";

	//数组名是数组首元素地址
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

在这里插入图片描述

7.3 递归与迭代

7.3.1 练习3:

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

#include<stdio.h>
//int Fac(int n)//非递归
//{
//	int i = 0;
//	int ret = 1;
//	for (i = 1; i <= n; i++)
//	{
//		ret *= i;
//	}
//	return ret;
//}


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", ret);
	
	return 0;
}

在这里插入图片描述

7.3.2 练习4:

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

//求第n个斐波那契数。(不考虑溢出)
//1 1 2 3 5 8 13 21 34 55
//前两个数字之和为第三个数字
//Fib(int n)
//n<=2, 1
//n>2,  Fib(n-1)+Fib(n-2)

//递归
#include<stdio.h>
int Fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d ", ret);
}

但是,用递归方法不合适,我们发现 fib 函数在调用的过程中很多计算其实在一直重复。

非递归效率提升很多:

#include<stdio.h>
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;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d", ret);
	return 0;
}

在这里插入图片描述
提示:

  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开
    销。

7.4函数递归的几个经典题目:汉诺塔问题、青蛙跳台阶问题

  1. 汉诺塔问题
    汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?
    在这里插入图片描述
//汉诺塔问题
//假设总共需要移动n个盘子
//1.将A柱上的n - 1个盘子借助C柱移向B柱
//2.将A柱上仅剩的最后一个盘子移向C柱
//3.将B柱上的n - 1个盘子借助A柱移向C柱
#include<stdio.h>
void move(int x, int y)
{
	printf("%c->%c\n", x, y);
}
void hanoi(int n, char a, char b, char c)
{
	if (n == 1)
	{
		move(a, c);
	}
	else
	{
		hanoi(n - 1, a, c, b);//将A座上的n-1个盘子借助C座移向B座
		move(a, c);//将A座上最后一个盘子移向C座
		hanoi(n - 1, b, a, c);//将B座上的n-1个盘子借助A座移向C座
	}
}
//move中的实参与hanoi函数中的形参相对应,而hanoi函数中形参a,b,c所对应的值也是在有规律的变化
int main()
{
	int n = 0;
	scanf("%d", &n);
	hanoi(n, 'A', 'B', 'C');
	return 0;
}

在这里插入图片描述
2. 青蛙跳台阶问题
一只青蛙一次可以跳上 1 级台阶,也可以跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法。
分析:当n = 1, 只有1中跳法;当n = 2时,有两种跳法;当n = 3 时,有3种跳法;当n = 4时,有5种跳法;当n = 5时,有8种跳法;…
从第三个起,每个数字是前两个数字之和,类似于规律Fibonacci数列:
在这里插入图片描述

#include<stdio.h>
int Fib(int n)
{
	if (n <= 0)
	{
		printf("error");
		return -1;
	}

	if (1 == n)
	{
		return 1;
	}
	else if (2 == n)
	{
		return 2;
	}
	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;
}

当输入:10
在这里插入图片描述

7.5递归使用建议:

1.当解决一个问题递归和非递归都可以使用,且没有明显问题,那就可以使用递归
2.当问题用递归简单二非递归较难且递归没有明显问题,那就可以用递归
3.如果说用递归解决问题,写起来简单,但是有明显问题,就不能用递归,应该用非递归的方式解决问题

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值