实验3

四、实验结论

实验任务1

  • 源代码task1.c
//task1 score_to_grade

#include <stdio.h>

char score_to_grade(int score); // 函数声明 

int main()
{
	int score;
	char grade;
	
	while(scanf("%d", &score) != EOF)
	{
		grade = score_to_grade(score); // 函数调用 
		printf("分数: %d, 等级: %c\n\n", score, grade);
	}
	
	return 0;
}

//函数定义
char score_to_grade(int score)
{
	char ans;
	
	switch(score/10)
	{
		case 10:
		case 9:		ans='A';break;
		case 8:		ans='B';break;
		case 7:		ans='C';break;
		case 6:		ans='D';break;
		default:	ans='E';
	}
	
	return ans;
}

+运行测试截图

task1

  • 问题回答
问题1

函数score_to_grade的功能是根据输入的score分数输出对应字符型grade等第。
其中形参类型为int整型。返回值类型是char字符型。

问题2
运行测试截图

task1_1

分析

如果line26-34(问题中为line21-28)改变为问题中形式,将导致每一个case情况结算完后不会跳出switch,而是顺序执行下一行case(或default)。最终导致ans被多次赋值且最终恒为'E'。

实验任务2

  • 源代码task2.c
//task2 sum_digits

#include <stdio.h>

int sum_digits(int n); // 函数声明 

int main()
{
	int n;
	int ans;
	
	while(printf("Enter n: "), scanf("%d", &n) != EOF)
	{
		ans = sum_digits(n); // 函数调用 
		printf("n = %d, ans = %d\n\n", n, ans);
	}
	
	return 0;
}

//函数定义
int sum_digits(int n)
{
	int ans = 0;
	
	while(n != 0)
	{
		ans += n % 10;
		n /= 10;
	}
	
	return ans;
}
  • 运行测试截图

task2

  • 问题回答
问题1

函数sum_digits的功能是根据输入数字,返回整型各数位上的数字之和。

问题2
运行测试截图

task2_1

分析

肯定,能实现同样的输出。
关于两种实现方法的思维区别:
使用迭代的方法,思维上是将问题看做一个整体,通过规划好每一次迭代的工作,依靠循环分步得到答案;
使用递归的方法,思维上是将问题拆解成一个个子问题,通过寻找最小的子问题,使子问题的解回归原问题,依靠回归积累得到答案。

实验任务3

  • 源代码task3.c
//task3 power

#include <stdio.h>

int power(int x, int n); // 函数声明 

int main()
{
	int x, n;
	int ans;
	
	while(printf("Enter x and n: "), scanf("%d%d", &x, &n) != EOF)
	{
		ans = power(x, n); // 函数调用 
		printf("n = %d, ans = %d\n\n", n, ans);
	}
	
	return 0;
}

//函数定义
int power(int x, int n)
{
	int t = 0;
	
	if(n == 0)
		return 1;
	else if(n % 2)	// (n % 2) != 0 
		return x * power(x,n-1);
	else			// (n % 2) == 0 && n != 0
	{
		t = power(x, n/2);
		return t*t;
	}
}
  • 运行测试截图

task3

  • 问题回答
问题1

函数power的功能是根据输入的x和n,返回整型x的n次方。

问题2

函数power是递归函数。

数学公式模型

\[P(x, n) = \begin{cases} 1, & \text{当 } n = 0 \text{ (递归出口)} \\ x \cdot P(x, n-1), & \text{当 } n \text{ 为奇数} \\ P \left( x, \frac{n}{2} \right)^2, & \text{当 } n \text{ 为偶数} \end{cases} \]

MathJax3源码

image

问题3
运行测试截图

task3_1

task3_2

分析
  • 子问题① 算法逻辑上依然正确。
    原因:原line33-34(问题中为line26-27)为先求x的n/2次方并保存结果,然后返回结果的平方;改后代码为求2次x的n/2次方并返回它们的乘积。故两种算法返回值相同。
  • 子问题② 性能上,原代码显著优于改代码。
    改代码每次返回值时调用两次power(x,n/2),这会导致大量的重复计算,使时间复杂度和空间复杂度急剧增大。
计算power(3, 16)
  • 分析如图

task3_3_1

task3_3_2

  • 分析小结
    对于计算power(3, 16),原算法共需调用6次power()函数,改算法共需调用63次power()函数。
总结

算法逻辑上改后代码依旧正确,因为算法的返回值没有改变,只改变了中间过程。
但通过对power(3, 16)的分析,发现原算法的时间复杂度为O(log n)对数阶,改算法的时间复杂度为O(n)线性阶。即改后算法性能急剧下降。
通过前面分析得知,导致性能下降的原因是计算n为偶数的子问题时,每次调用都会将相同的power(x, n/2)再调用两次,使得该子问题的重复计算次数呈树形蔓延,所需的计算次数在原本的基础上指数爆炸。

实验任务4

  • 源代码task4.c
//task4 classify_triangle

#include <stdio.h>

int classify_triangle(int a, int b, int c);

int main()
{
	int a, b, c;
	
	while( scanf("%d%d%d", &a, &b, &c) != EOF )
	{
		switch( classify_triangle(a, b, c) )
		{
			case 1:		printf("普通三角形\n\n");break;
			case 2:		printf("等边三角形\n\n");break;
			case 3:		printf("等腰三角形\n\n");break;
			case 4:		printf("直角三角形\n\n");break;
			default:	printf("不能构成三角形\n\n");break;
		}
	}
	
	return 0;
}

int classify_triangle(int a, int b, int c)
{
	if( a>=b+c || b>=a+c || c>=a+b)
		return 0;
	
	if( a*a == b*b+c*c || b*b == a*a+c*c || c*c == b*b+a*a )
		return 4;
	else if( a==b && b==c)
		return 2;
	else if( a==b || b==c || a==c )
		return 3;
	else
		return 1;
}
  • 运行测试截图

task4

实验任务5

递归方法

  • 源代码task5.c
//task5 combination_number

#include <stdio.h>

int func(int n, int m);	// 函数声明 

int main()
{
	int n, m;
	int ans;
	
	while(scanf("%d%d", &n, &m) != EOF)
	{
		ans = func(n, m); // 函数调用 
		printf("n = %d, m = %d, ans = %d\n\n", n, m, ans);
	}
	
	return 0;
}

 // 函数定义 
int func(int n, int m)
{
	if(n < m)
		return 0;
	if( m == 0 || m == n )
		return 1;
	return func(n-1, m) + func(n-1, m-1); 
}
/*
						1
					1	1
				1	2	1
			1	3	3	1
		1	4	6	4	1
	1	5	10	10	5	1
1	6	15	20	15	6	1
*/
  • 运行测试截图

task5

迭代方法

//task5_1 combination_number_better

#include <stdio.h>

int func(int n, int m);	// 函数声明 

int main()
{
	int n, m;
	int ans;
	
	while(scanf("%d%d", &n, &m) != EOF)
	{
		ans = func(n, m); // 函数调用 
		printf("n = %d, m = %d, ans = %d\n\n", n, m, ans);
	}
	
	return 0;
}

 // 函数定义 
int func(int n, int m)
{
	if(n < m)
		return 0;
	
	int nu=1, de=1;
	
	for(int i=n-m+1 ; i<=n ; i++)
		nu *= i;
	for(int  i = 1  ; i<=m ; i++)
		de *= i;
	
	return nu/de;
}
/*
 n-m+1 --> n   	合计m个数 
 1 --> m     	合计m个数 
*/
  • 运行测试截图

task5_1

实验任务6

  • 源代码task6.c
//task6 gcd_intelligently_exhaustively_3

#include <stdio.h>

// 函数声明
int gcd(int a, int b, int c);

int main() {
    int a, b, c;
    int ans;

    while(scanf("%d%d%d", &a, &b, &c) != EOF) {
        ans = gcd(a, b, c);     // 函数调用
        printf("最大公约数: %d\n\n", ans);
    }

    return 0;
}

// 函数定义
int gcd(int a, int b, int c)
{
	int ans;
	
	//find min
	if( a<b )
	{
		if(a<c)
			ans=a;
		else //c<a
			ans=c;
	}
	else//b<a
	{
		if(b<c)
			ans=b;
		else //c<b
			ans=c;
	}
	
	while( a%ans || b%ans || c%ans )
		ans--;
	
	return ans;
}
/*
	a%ans!=0 即非a的因数 
	b%ans!=0 即非b的因数
	c%ans!=0 即非c的因数
	以上任意一个不满足,循环继续 
*/
  • 运行测试截图

task6

实验任务7

  • 源代码task7.c
//task7 print_charman

#include <stdio.h>
#include <stdlib.h>

// 函数声明
void print_charman(int n);

int main() {
    int n;

    printf("Enter n: ");
    scanf("%d", &n);
    print_charman(n); // 函数调用
       
    return 0;
}

// 函数print_charman定义
//print (2n-1)*(n)/2 man <(1-1)>*(2n-1)*n/2
void print_charman(int n)
{
	//print (2i-1) man <(1-1)>*(2i-1)
	for(int i=0 ; i<n ; i++)
	{
		int j=0;
		//print i air" "
		for(j=0 ; j<i  ; j++)
			printf("\t");
		//print 2*(n-i)-1 head
		for(j=0 ; j<2*(n-i)-1; j++)
			printf("\t O ");
		printf("\n");
		
		//print i air" "
		for(j=0 ; j<i ; j++)
			printf("\t");
		//print 2*(n-i)-1 body
		for(j=0 ; j<2*(n-i)-1; j++)
			printf("\t<H>");
		printf("\n");
		
		//print i air" "
		for(j=0 ; j<i ; j++)
			printf("\t");
		//print 2*(n-i)-1 feet
		for(j=0 ; j<2*(n-i)-1; j++)
			printf("\tI I");
		printf("\n");
	}
}
  • 运行测试截图

task7

五、实验总结

while()引出的逗号表达式用法

在解决task1和task2相关问题时,我发现两份源代码的line12(问题中为line9)存在while(printf("Enter n: "), scanf("%d", &n) != EOF)这种写法,让我对while()语句中能使用复合语句感到惊讶。
但是这不是复合语句,这是逗号表达式。经查询,C语言中逗号 , 也是一个运算符(逗号运算符)。它的执行顺序为从左往右依次计算,最终整个逗号表达式的值等于最后一个表达式的值。
而复合语句,是用一对大括号 { } 把零、一或多个语句(或声明)括起来,在C语言中最大的作用是限制变量的作用域。复合语句也被称作语句块(Block),而while括号内需要一个表达式,语句块不能算出具体值。

由复制代码找到的错误与对测试的再反思

我的实验2中task5的line24是错误的!
我在实验3的task4尝试直接复制上述源代码时发现了这个错误。
判断等边三角形,我当时使用了else if( a == b || b == c),但是事实上,这样会导致只要是a、b边相等或者b、c边相等的等腰三角形,都会被按照等腰三角形输出。
改过代码的时候,更让我深思的是“为什么上次测试没有测出来”。查看上次测试数据,唯一要求输出等腰三角形的数据是2 3 2,a和b不等,b和c不等,偏偏a和c相等;而唯一要求输出等边三角形的数据是2 2 2,总归不可能被错过。尽管如此,这确实是我一眼就能看出的错误,本不应该存在。
我不是不知道每次实验给予的测试数据无法覆盖所有情况,也不是不知道OJ上同样如此。但我最近偏偏会纵容这样的情况发生,最开始是偷点小懒,少测一组特殊情况,到我渐渐对测试有些麻木了,能坐视错误结果在我眼前出现,却没看出来。这是行为上的懒惰反应到思维上了。而我在最近提交的OJ上也因为这些犄角旮旯的问题吃了不少苦头,做了不少反思亦无济于事。总结下来,一方面,我对于一个合格的测试员多了几分尊敬;另一方面,“你可长点心吧”。

杨辉三角和帕斯卡恒等式

写task5时,莫名对递归出口感到混乱,又恰巧想起曾经看过的杨辉三角,作图后整个人豁然开朗。
如你所见,这是帕斯卡恒等式:

\[C_n^m = C_{n-1}^m + C_{n-1}^{m-1} \]

如你所见,这是杨辉三角:

QQ_1794586918252

其中,目标数\(C_n^m\)正上方为\(C_{n-1}^m\),斜上方为\(C_{n-1}^{m-1}\)。最上面一斜行为\(C_k^0\),最右边一列为\(C_k^k\)。每一行分别表示\(C_k^0\)到\(C_k^k\)。
这些正好对应了task5中func函数的每一次递归、递归出口、输入参数的含义。

对递归极差性能的考虑与挂钟时间误会的解除

对于task5组合数问题,我最终认为递归在性能上远远落后于迭代算法,毕竟其有着极高的时间复杂度(O(\(2^n\)))且极易栈溢出。这导致我以为在某次运行测试时递归计算\(C_5^k\)便已耗时颇久。
图例:

task5_0

然而这是一个误解。事实上,很多编译器(包括Dev-C++)显示的 "Process exited after X seconds" 是挂钟时间(Wall-clock time),包括了程序从启动到结束的物理时间。
也就是说,这包含了用户敲键盘、看结果、思考人生等等的时间。

不要让注释误导你的代码与虚数的发现

在完成task6时,我尝试用注释引导自己模块化编程,最终结果是我受到注释的错误引导。导致2*j写成了2j,并且得到了有关complex int的报错。习惯性的,数学上2i或2j这类表示2乘变量i或j。但是在C语言C99标准中,complex int复数被引入(需要头文件<complex.h>才能做具体数学运算),而i和j正好可以作为虚数单位。此时2i(或2j)被编译器解析为 0+2i这一复数,且不能与实数比较大小。
这个故事告诉我们,规范使用注释,不要让注释误导你的代码。同时,C语言中存在复数,也许在未来会有应用.

从Dev-c++到Markdown,疑似的排版错位问题

本次在博客园编辑Markdown文档时,出现了源文档和预览排版错位情况。已知的是这些排版中存在大量缩进,也可能是显示延迟吧。

posted @ 2026-11-14 01:12  Exosister233  阅读(3)  评论(0)    收藏  举报