算法竞赛入门(4)学习笔记——函数和递归

本文介绍了C语言中自定义函数的使用,包括计算两点欧几里得距离、组合数和素数判定,强调了函数参数和返回值的重要性。此外,还讲解了函数调用、参数传递、递归的概念及其应用,以及在编程竞赛中需要注意的问题。习题部分涵盖了水仙花数、韩信点兵、倒三角形等算法问题。
摘要由CSDN通过智能技术生成


本节中,主要讲解

一:自定义函数和结构体

1.1【计算两点欧几里得距离】

#include<math.h>
double distance(double x1, double y1, double x2, double y2)
{
	return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}

反思
1.函数参数和返回值类型为一等公民。可以是常见的int、double、char,也可以是数组,但这个实现相对比较复杂。

2.函数不需要返回值时,比如只需要执行printf(),返回值类型为void。

3.函数执行过程中遇见return,会直接退出而不执行后面语句;当函数没有return时,将返回一个不确定的值。

4.main函数是整个程序的入口,可由操作系统、IDE、调试器、自动评测系统调用,0表示正常结束,返回给调用者。

//调用其他函数
#include<math.h>
double distance(double x1, double y1, double x2, double y2)
{
	double dx = x2 - x1;
	double dy = y2 - y1;
	return hypot(dx, dy);//hypot为计算平方和的开方。
}
//使用结构体
#include<math.h>
struct Point { double x, y; };
double distance(struct Point a, struct Point b )
{
	return hypot(a.x-b.x,a.y-b.y);
}

定义结构体: struct+名字+{域定义;}注意最后有;

//简化版结构体
#include<math.h>
typedef struct { double x, y; }Point;
double distance(Point a, Point b)
{
	return hypot(a.x - b.x, a.y - b.y);
}

为了方便,结构体通常定义:typedef struct{域定义;}类型名; 生成的和int类型等数据类型的相似。

//计算欧几里得距离的四种写法。
#include<math.h>
typedef struct { double x, y; }Point1;
//注意typedef前面没有# 使用的是花括号 第二个花括号前面有分号 名字写在最后
struct Point { double x, y; };
double distance1(double x1, double x2, double y1, double y2)
{
	return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
sqrt函数中的参数只有一个。
double distance2(double x1, double x2, double y1, double y2)
{
	double dx = x1 - x2;
	double dy = y1 - y2;
	return hypot(dx, dy);
}
//hypot中的参数有两个
double distance3(struct Point a, struct Point b)
{
	return hypot(a.x - b.x, a.y - b.y);
}
//未使用typedef的结构体,形参要带上 struct
double distance4(Point1 a, Point1 b)
{
	return hypot(a.x - b.x, a.y - b.y);
}

1.2【计算组合数】

编写函数,参数是两个非负整数n和m,返回组合数C(m,n) =m!/(n!*(m-n)!),其中,m<=n<=25。例如,n=25,m=12时答案为5200300。

long long factorial(int n)
{
	long long m = 1;
	for (int i = 1; i <= n; i++)
		m = m * i;
	return m;
}
long long c(int m, int n)
{
	return factorial(m) /factorial(n) *factorial(n - m);
}

上面算法漏洞:溢出
shame:factorial的递归没写出来-_-

//组合数算法改进
long long C(int m, int n)
{
    int ans = 1;
    if (n < m - n) n = m - n;
    for (int i = n + 1; i <= m; i++) ans *= i;
    for (int j = 1; j <= n; j++)ans /= j;
    return ans;
 }

对于复杂计算,能够通过进一步思考改进算法,可以减少中间溢出的风险。

1.3【素数判定】

题目:编写函数,参数是一个正整数n,如果它是素数,返回1,否则返回0。

//法一,自写
//当n=1时不能调用
int is_prime(int n)
{
	int tmp = 1;
	for (int i = 2; i <n; i++)
	{
		if (n % i == 0) tmp = 0;
	}
	return tmp;
}
//法二
//当n=1和n较大时不能调用
#include<stdio.h>
int judge(int n)
{
	for (int i = 2; i*i <n; i++)
	{
		if (n % i == 0) return 0;
	}
	return 1;
}

法二与法一在实质上是相同的。但是法二判断不是质数之后直接返回0,明显比自写效率高。同时,只从2扫描到sqrt(n),也提高了效率。

//改进版
int judge(int n)
{
	if (n <=1) return 0;
	int m = floor(sqrt(n) + 0.5);
	for (int i = 2; i <= m; i++)
		if (n % i == 0)return 0;
	return 1;
}

改进版加上了对1的判断,同时直接通过sqrt(),避免i*i溢出。
对于浮点数和整数
ceil(x) 返回不小于x的最小整数值(返回值为浮点型)。
floor(x) 返回不大于x的最大整数值(返回值为浮点型)。
round(x) 返回x的四舍五入整数值(返回值为浮点型)。

二:函数调用与参数传递

2.1【用函数交换变量】

2.2【调用栈】

2.3【用指针作参数】

#include<stdio.h>
void swap(int* a, int* b)
{
	int t = *a; *a = *b; *b = t;
}
int main()
{
	int a, b;
	scanf("%d%d", &a,& b);
	swap(&a, &b);
	printf("%d %d\n", a, b);
	return 0;
}

1.变量名前加上&,表示得到该变量的地址。
变量和内存的关系:
变量存在内存中,每个变量占一个或多个字节(可用sizeof计算得到)。
每个字节都有一个地址。
变量的地址为第一个字节的位置。

2.4【易犯错误】

2.5【数组作为参数和返回值】

题目:计算数组的元素和

#include<stdio.h>
int sum(int* a, int n)//n表示数组的长度
{
	int ans = 0;
	for (int i = 0; i < n; i++)
		ans += a[i];
	return ans;
}

注意: 数组做参数时,只有数组的首地址作为指针传递给了函数,无法使用sizeof(a)来计算长度。

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sum(a + 1, 3));
	return 0;
}

在调用函数sum时,并没有使用首地址,而是通过指针的加减法使用第二个元素的地址。

//sum函数的另外写法1
int sum(int* begin, int* end)
{
	int n = end - begin;
	int ans = 0;
	for (int i = 0; i < n; i++)
		ans += begin[i];
	return ans;
}
//sum函数的另外写法2
int sum(int* begin, int* end)
{
	int* p = begin;
	int ans = 0;
	for (int* p = begin; p != end; p++)
		ans += *p;
	return ans;
}

2.6【把函数作为函数的参数】

三:递归

3.1【递归定义】

3.2【递归函数】

3.3【C语言对于递归的支持】

3.4【段错误与栈溢出】

四:竞赛题选讲

五:注解与习题

例题: 数据统计
输入一些整数,求出它们的最小值、最大值、平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。
样例输入:
2 8 3 5 1 7 3 6
样例输出:
1 8 4.375

#include<stdio.h>
int main()
 {
	const int INF = 100000000;//INF(infinite,无限的)
	int x, n=0, max = -INF,min = INF, s=0;
	while (scanf("%d", &x) == 1) 
	{
		s += x;
		if (max < x)max = x;
		if (min > x)min = x;
		n++;
	}
	printf("%d %d %.3f\n", min, max, (double)s / n);
	return 0;
}

反思:
1.通过scanf("%d", &x)返回的是成功输入的变量的个数。当输入结束时,scanf无法再次读取x,返回0.
2.Windows系统下,输入结束后:按Enter,再按Ctrl+Z,最后按Enter结束输入。 Linux系统下,按Ctrl+D结束输入。
3.浮点计算在前面加上(double)即可。
4.max和min要制定初值,否则机器会开始随机生成数,结果报错。本处使用常量来,min初值要大,max初值要小。但问题:INF数据不够大或者运算溢出。

进阶:
暴露问题:输入要逐行输入,输出时一卷屏前面的就看不见,且与标准答案比较不方便。
解决办法:通过文件。
实际操作:输入输出重定向,在main函数的入口加上

freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);

注意事项:有的比赛不能使用文件,或者不能使用重定向方式读取文件,看请比赛要求是否是标准输入输出。并且输入输出文件名要仔细,不要使用相对路径或者绝对路径。
自我测试完成后删除重定向语句:

#define LOCAL
#include<stdio.h>
#define INF 1000000
int main() 
{
#ifdef LOCAL
	freopen("input.txt", "r", stdin);
	freopen("output.txt", "w", stdout);
#endif
	int x, n = 0, max = -INF, min = INF, s = 0;
	while (scanf("%d", &x) == 1)
	 {
		s += x;
		if (max < x)max = x;
		if (min > x)min = x;
		/*
			printf("%d %d %.3f\n", min, max, (double)s / n);
		*/
		n++;
	}
	printf("%d %d %.3f\n", min, max, (double)s / n);
	return 0;
}

注意:
1.重定向部分写在#ifdef和#endif中。表示只有定义了符号LOCAL,才编译中间的两条。上程序首部就定义了LOCAL,提交时删除即可。或者在编译选项中定义LOCAL。
2.printf在/* */中可以用来中间检测输出。
非重定向版:

#include<stdio.h>
#define INF 1000000
int main() 
{
FILE* fin, * fout;
fin=fopen("date.in", "rb");
fout=fopen("date.out", "wb");
int x, n = 0, max = -INF, min = INF, s = 0;
while (fscanf(fin,"%d",&x) == 1)
{
s += x;
if (max < x)max = x;
if (min > x)min = x;
n++;
}
fprintf(fout,"%d %d %.3f\n", min, max, (double)s / n);
fclose(fin);
fclose(fout);
/*声明变量fin和fout,把scanf换成fscanf
第一参数为fin;printf换成fprintf,第一参数为fout
最后执行fclose,关闭两个文件*/
	return 0;
}

思考:
1.重定向文件更简洁,但不能同时文件读写和标准输入输出。
fopen写法繁琐,可以重复打开读写文件。将上文件改为标准输入/出,只需要赋值:fin=stdin;fout=stdout;同时不调用fopen和fclose

例题: 数据统计
输入一些整数,求出它们的最小值、最大值、平均值(保留3位小数)。输入保证这些数都是不超过1000的整数。
输入包含多组数据,每组数据的第一行是整数个数n,第二行是n个整数。n=0为输入结束标志,程序忽略此数组。相邻两组数据之间应输出一个空行。
样例输入:
8
2 8 3 5 1 7 3 6
4
-4 6 10 0
0
样例输出:
Case 1:1 8 4.375
Case 2:-4 10 3.000

#include<stdio.h>
#define INF 1000000
int main() 
{
	int n=0, s = 0,x,kase=0;
	while (scanf("%d", &n) == 1 && n)
	 {
		int s = 0 ,max = -INF, min = INF ;
		for (int i = 0; i < n; i++)
		 {
			scanf("%d", &x);
			s += x;
			if (x < min)min = x;
			if (x > max)max = x;
		}
		if (kase)printf("\n");
		printf("Case %d: %d %d %.3f\n", ++kase, min, max, (double)s / n);
	}
	return 0;
}

反思:
1.输入循环:在n=0作为输入标记并仍然判断scanf的返回值,是增加程序的鲁棒性。
2.kase的使用:kase为数据编号计数器。当输入第二行数据时,会空一行。
3.min和max放置于循环内:在进行下一组运算时能够重置,否则将会影响下一组的结果。

五:习题

5.1 水仙花数

题目:
输出100~999中的所有水仙花数。若三位数ABC满足ABC=A3+B3+C3,则称其为水仙花数,例如153=13+53+33

#include<stdio.h>
int main() 
{
	for (int i = 100; i <= 999; i++) 
	{
		int k, l, m;
		k = i / 100;
		l = i / 10 % 10;
		m = i % 10;
		if (i == k * k * k + l * l * l + m * m * m)
			printf ("%d\n",i);
	}
	return 0;
}

思考:
本题中分出每位的数字很重要,%和/的运算要清楚。

5.2 韩信点兵

题目:相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入3个非负整数a,b,c ,表示每种队形排尾的人数(a<3,b<5,c<7),输出总人数的最小值(或报告无解)。已知总人数不小于10,不超过100 。
样例输入
2 1 6
2 1 3
样例输出
Case 1:41
Case 2:No answer

#include<stdio.h>
int main() 
{
	int a, b, c,kase=0;//计数变量
	scanf("%d%d%d", &a, &b, &c);
	for (int i = 2; i <= 19; i++) 
	{
		int n = 5 * i + b;
		if ((n - a) % 3 == 0 && (n - c) % 7 == 0)
		 {
			printf("Case%d :%d\n",++kase, n);
		}
	}
	if (kase == 0)printf("No Answer");
	return 0;
}

反思:
第一次卡壳:未能找到3、5、7的表示。
第二次卡壳:no answer这个条件语句不知道怎么放置,且没有使用计数变量进行输出。

5.3 倒三角形

输入正整数n<=20,输出一个n层的倒三角形。例如,n=5时输出如下:

#########
 #######
  #####
   ###
    #
#include<stdio.h>
int main()
 {
	int n;
	scanf("%d", &n);
	for (int i = n; i >= 1; i--)
	 {
		for (int j = n - i;j>0;j--) 
		{
			printf(" ");
		}
		for (int k = 2 * i - 1; k > 0; k--)
		 {
			printf("#");
		}
		printf("\n");
	}
	return 0;
}

反思:
本题需要深入理解遍历原理和顺序执行规则。

5.4 子序列的和

输入两个正整数n<m<106 ,输出1/n2+ 1/(n+1)2+…+1/m2,保留5位小数。输入包含多组数据,结束标记为n=m=0。提示:本题有陷阱。
样例输入:
2 4
65536 655360
0 0

样例输出:
Case 1: 0.42361
Case 2: 0.00001

5.4 分数化小数

输入正整数a, b, c,输出a/b的小数形式,精确到小数点后c位。a, b <= 106,c <= 100。输入包含多组数据,结束标记为a=b=c=0.

样例输入:
1 6 4
0 0 0

样例输出:

Case 1:0.1667

5.5 排列

用1,2,3…9组成3个三位数abc, def, ghi, 每个数字恰好使用一次,要求:abc:def:ghi=1:2:3,按照“abc def ghi”的格式输出所有解,每行一个解。

5.6 思考题

六:笔者注释

本文为学习《算法竞赛入门经典(第2版)》所做的学习笔记,仅作学习交流使用,切勿用作他途!如有侵权,联系立删。本文内容可能会有理解错误或遗漏,望乞指正,不胜感激!

七: 参考资料

刘汝佳. 算法竞赛入门经典[M]. 第2版. 北京 : 清华大学出版社, 2014

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值