实验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;
}
+运行测试截图

- 问题回答
问题1
函数score_to_grade的功能是根据输入的score分数输出对应字符型grade等第。
其中形参类型为int整型。返回值类型是char字符型。
问题2
运行测试截图

分析
如果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;
}
- 运行测试截图

- 问题回答
问题1
函数sum_digits的功能是根据输入数字,返回整型各数位上的数字之和。
问题2
运行测试截图

分析
肯定,能实现同样的输出。
关于两种实现方法的思维区别:
使用迭代的方法,思维上是将问题看做一个整体,通过规划好每一次迭代的工作,依靠循环分步得到答案;
使用递归的方法,思维上是将问题拆解成一个个子问题,通过寻找最小的子问题,使子问题的解回归原问题,依靠回归积累得到答案。
实验任务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;
}
}
- 运行测试截图

- 问题回答
问题1
函数power的功能是根据输入的x和n,返回整型x的n次方。
问题2
函数power是递归函数。
数学公式模型
MathJax3源码

问题3
运行测试截图


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


- 分析小结
对于计算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;
}
- 运行测试截图

实验任务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_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个数
*/
- 运行测试截图

实验任务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的因数
以上任意一个不满足,循环继续
*/
- 运行测试截图

实验任务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");
}
}
- 运行测试截图

五、实验总结
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}\)。最上面一斜行为\(C_k^0\),最右边一列为\(C_k^k\)。每一行分别表示\(C_k^0\)到\(C_k^k\)。
这些正好对应了task5中func函数的每一次递归、递归出口、输入参数的含义。
对递归极差性能的考虑与挂钟时间误会的解除
对于task5组合数问题,我最终认为递归在性能上远远落后于迭代算法,毕竟其有着极高的时间复杂度(O(\(2^n\)))且极易栈溢出。这导致我以为在某次运行测试时递归计算\(C_5^k\)便已耗时颇久。
图例:

然而这是一个误解。事实上,很多编译器(包括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文档时,出现了源文档和预览排版错位情况。已知的是这些排版中存在大量缩进,也可能是显示延迟吧。

浙公网安备 33010602011771号