题目来源是俞欢军老师在 2021 年秋冬学期布置的作业和 PTA 上的浙大版《 C 语言程序设计(第4版)》题目集。希望能帮大家复习。

一、考前注意事项

避免初学者的经典错误:

  • 小心除法。C 语言中5/9等于 0,而5.0/9等于 0.555……
  • 检查该赋值的变量是否都赋值了或者初始化了。
  • 判断相等关系是==不是=,单个等号是赋值符。
  • for 循环的for(语句1; 语句2; 语句3)中是两个分号,不是逗号。
  • do-while 循环的while(循环条件);最后面是有分号的。
  • 检查 switch 语句中各个 case 后面是否需要break;
  • 填空题注意检查有没有通过++--和指针对变量的修改。
  • 别忘了函数最后一般有return语句。
  • 要用 strlen( ) 等函数的话别忘了引用 string.h 头文件。

那些需要背诵且容易忘的知识点:

  • C程序由函数构成。
  • 程序设计语言必须具备数据表达和流程控制的功能。
  • C语言中标识符由字母、数字、下划线构成,第一个字符不可以是数字。
  • C语言生成的目标代码质量高,运行效率高。
  • C语言是编译型语言,移植性比较不好。
  • 算法是一组明确的解决问题的步骤,它产生结果并在有限的时间内终止,可以用自然语言、伪代码、流程图描述。
  • 算数表达式是指用算数运算符(自增、自减运算符,四则运算符,求余数运算符)连接的合C语言语法的表达式,例如(i++) * j % k
  • 赋值表达式中,赋值运算符右侧的表达式的数据类型和左侧变量的类型不一样,会自动把右侧结果强制类型转换,再储存到左侧变量中。
  • 位运算是C语言的特色,可以让C实现很多汇编语言可以做到但其他高级语言不行的操作。位运算不属于逻辑表达式,即a & ba && b并不是同一类运算。

常见报错信息:

  • 段错误,多半是scanf( )忘了加&,也可能是用了空指针。
  • 运行超时,可能是因为用了太多pow( )这样很慢的函数,也可能是因为有死循环。
  • 格式错误,就是空格、换行有问题而已,好解决。
  • 非零返回,可能是主函数最后没有return 0;。但如果加上了return 0;主函数返回值依然是很大很怪的数字,可能是因为源代码中有不严重的语法错误,程序还能正常运行一部分,但最后不正常地结束运行了。

其他注意事项:

  • 老师没教怎么用 IDE 的调试功能不代表你不需要会。非常建议平时抽五分钟时间上 B 站找个视频学一下,可以提高 debug 效率。
  • Prof. 翁恺:“实际编程的时候有 25% 的问题出在读入数据的时候。建议着重检查 scanf( ),调试的时候要在 scanf( ) 之后立刻 printf( ) 一下,以确认 scanf( ) 真的读入了你想它读入的东西。这种在调试的时候让程序在运行中途输出一些数据来帮助我们确认程序是否按照我们所想的方式在运行的方法,就是所谓的‘输出日志’。”
  • 这门课的考试题目有很多坑题,考试的时候应该稳一点,把自己当成电脑,要在草稿纸上跟踪程序中每个变量的一步步变化。切忌自以为理解了代码的用意就想当然地写下一个答案,很可能这段程序里藏了小 bug。
  • 这门课考试是有题库的,会考往年真题,所以考前最好刷一下学解卖的往年真题,再记住那些坑题、难题,说不定考试的时候能捡回几分。
  • 来不及刷完真题也别太焦虑。我就没刷真题,最后绩点还是有 4.8 / 5.0,只要知识点理解了就不会有大问题。当然你要是跟图灵班卷王一样非 5.0 / 5.0 不可那当我没说。

    二、理论考

  1. 有一函数浙大 C 小程纠错本 - 图1用C语言表示,以下程序段中错误的是( C )。

    1. y = 0;
    2. if(x >= 0); //首先,这个分号就让第一个if语句提前结束了
    3. if(x > 0) y = 1;
    4. else y = -1;
    1. if(x >= 0)
    2. if(x > 0) y = 1;
    3. else y = 0; //这个else语句跟if(x > 0)
    4. else y = -1; //这个else语句跟if(x >= 0)

    if-else 嵌套 if-else 时,else 语句跟离它最近的那个不带 else 语句的 if 语句。
    想让 else 跟前面离它更远的 if,可以把后面的 if 用 { } 括起来,剩 else 在大括号外,如下:

    1. if(x >= 0)
    2. {
    3. if(x > 0) y = 1;
    4. }
    5. else y = -1 //这个else语句跟的是if(x >= 0)

    也可以用一个else;的空语句让后面的 if 不能再跟 else,接下来的 else 就会跟前面的 if。

    1. if(x >= 0)
    2. if(x > 0) y = 1;
    3. else; //这个else空语句跟的是if(x > 0)
    4. else y = -1 //这个else语句跟的是if(x >= 0)
  2. 设变量已正确定义,则以下( C )是合法的 switch 语句。

    1. switch(op){
    2. case '*': printf("%d\n", value1 * value2); break;
    3. case '+': printf("%d\n", value1 + value2); break;
    4. case '-': printf("%d\n", value1 - value2); break;
    5. case '*': printf("%d\n", value1 * value2); break;
    6. default: printf("Error\n"); break;
    7. }

    不合法,不能有两个带一样常量的 case。这里*用了两次。

    1. switch('/'){
    2. case '*': printf("%d\n", value1 * value2); break;
    3. case '-': printf("%d\n", value1 - value2); break;
    4. case '+': printf("%d\n", value1 + value2); break;
    5. default: printf("Error\n"); break;
    6. }

    合法(符合 C 语言语法),只是会一直 default 而已。

  3. 写出满足下列条件的C表达式:

“year 是闰年,即 year 能被 4 整除但不能被 100 整除,或 year 能被 400 整除。”
一种正确解答:year%4==0 && year%100!=0 || year%400==0
可以这么记:||长得更高,优先级却反而比&&低。
当然,记不住这个优先级是很正常的,写代码的人最好考虑到读代码的人的感受。所以更好的解答是:((year%4==0) && (year%100!=0)) || (year%400==0)

  1. 在C程序运行过程中,其值不能被改变的量称为常量,其值可以改变的量称为变量。(✔️)

有些同学学到后来会把“标识符”和“变量”搞混,以为标识符就是变量名。标识符是用户编程时使用的名字,用于给变量、常量、函数等命名,以建立起名称与实体之间的关系。
浙大 C 小程纠错本 - 图2

  1. 为了检查以下 if-else 语句的两个分支是否正确,至少需要设计 3 组测试用例。(✔️)

    1. if (x <= 15){
    2. y = 4 * x / 3;
    3. } else {
    4. y = 2.5 * x - 10.5;
    5. }

    测试的时候一定要单独测试临界点。尽管这里 x 等于 15 的情况也是进入if(x <= 15)的分支,但必须单独测试,也就是需要 x 小于 / 等于 / 大于 15,三种测试用例。

  2. 以下程序段的功能是输出 1~100 之间每个整数的各位数字之和。(❌)

    1. for(num = 1; num <= 100; num++){
    2. s = 0;
    3. do{
    4. s = s + num % 10;
    5. num = num / 10;
    6. }while(num != 0);
    7. printf("%d\n", s);
    8. }

    这个会进入死循环,一直输出 1,因为 do-while 直接用了 num,导致 num被反复归零。
    正确的做法是用另一个变量 n 储存每次循环中 num 的值,用 n 进行 do-while 语句内部的操作。

  3. 若变量已正确定义,以下 while 循环正常结束时,累加到pi的最后一项item的值满足( A )。

A. item 的绝对值小于 0.0001
C. item 的绝对值大于等于 0.0001

  1. flag = 1;
  2. denominator = 1;
  3. item = 1.0;
  4. pi = 0;
  5. while(fabs(item) >= 0.0001){ /*乙*/
  6. item = flag * 1.0 / denominator; /*甲*/
  7. pi = pi + item;
  8. flag = -flag;
  9. denominator = denominator + 2;
  10. }

甲处 item 的绝对值恰好大于 0.0001 时,回到乙后,再进入循环,在甲处计算出一个绝对值小于 0.0001 的新 item 再加上去,然后才结束循环。

  1. 在函数调用Func(exp1, exp2+exp3, exp4*exp5)中,实参的数量是( A )

A. 3
实参的定义说了实参可以是常量/变量/表达式。这里 exp1、exp2+exp3、exp4*exp5 各算一个实参。

  1. 下列程序的输出结果是( C )。

C. (3,6)

  1. # include <stdio.h>
  2. int f(int n)
  3. {
  4. static int k, s;
  5. n--;
  6. for(k=n; k>0; k--)
  7. s += k;
  8. return s;
  9. }
  10. int main(void)
  11. {
  12. int k;
  13. k = f(3);
  14. printf("(%d,%d)", k, f(k));
  15. return 0;
  16. }

最后的f(k)的确是f(3),但因为函数f( )中有静态局部变量,其值在f( )每次运行后会保留,并影响下一次调用f( ),所以不能套用前面算过的f(3)=3。

  1. 以下正确的函数定义形式是( A )。

A. double fun(int x, int y)
B. double fun(int x ; int y)
C. double fun(int x, int y);
D. double fun(int x, y)
这里问的“函数定义形式”是指函数定义时函数首部的形式,问“函数声明形式”才应该选C。函数定义/声明时,参数表中的参数之间是逗号,且每个参数都需要单独声明其数据类型。

  1. #include<stdio.h>
  2. void sayhello(int n); //这一行是函数声明
  3. int main()
  4. {
  5. sayhello(5); //这一行是函数调用
  6. return 0;
  7. }
  8. //下面这一块是函数定义
  9. void sayhello(int n) //第一行称为函数首部,包括返回值类型声明、函数名、参数表
  10. {
  11. int i = 0;
  12. for(; i<n; i++)
  13. printf("Hello World!\n");
  14. }
  1. 08是正确的整型常量。(❌)

0开头表示这个常量是八进制的,0x开头表示是十六进制的。八进制中不会出现8这个数码,因为数到8就该进位了。

  1. 表达式!x等价于x!=1。(❌)

C语言关系表达式中,不止1,所有除0之外的数,包括非0浮点数,都表示true。只有0表示false。当x为0时,!x的值是1;当x非0时,!x的值是0。所以!x等价于x==0
很多程序员会用!x代替x==0,因为位运算是最快的运算。

  1. 表达式(z=0, (x=2)||(z=1),z)的值是( 0 )

用printf( )打印这个逗号表达式的值,输出0。整个逗号表达式的值是最后一个分句的值。在||处,x=2的返回值是2,表示true,所以整个逻辑表达式的值一定是true,所以后面的z=1就不执行了,直接返回整个逻辑表达式的值为1。如果||左边是x=0,返回0,就得继续执行z=1才能判断整个逻辑表达式是true还是false,逻辑表达式的值为1。
这种只计算逻辑表达式的一部分就已经能确定整个逻辑表达式的值的现象称为“逻辑短路”。

  1. 设字符型变量x的值是064,表达式~x ^ x << 2 & x的值是( 0333 )

按运算符优先级,~最高,其次<<,其次&,其次^。
064是0开头,说明是八进制,转成二进制的一个Byte进行位运算,为00110100。先算~x,按位取反,11001011。再算x<<2,左移2位,00110100<<2即为11010000。然后x<<2的结果和x执行&按位与,11010000&00110100,即为00010000。最后~x的结果与00010000按位异或,11001011^00010000,为11011011,即为8进制0333。

这种题也就考考大学生,真写代码的时候写成这样肯定被同事打死。😈 ——Prof. 翁恺

  1. 定义各变量及初始化如下:int a = 2, b = 3, c = 1, d, x=10;

printf("%d", d = a > b); 输出( 0 )
⬆️赋值的优先级低于关系运算,这里会先算a > b输出0表示false,再把0赋值给d,整个表达式的值就是最后赋值表达式中赋的值,也就是0。
printf("%d", b–1 == a != c); 输出( 0 )
⬆️判断相等/不相等是从左到右运算的。先算b-1 == a,成立,输出1表示true;再算1 != c,不成立,输出0表示false。
printf("%d", 3 <= x <= 5); 输出( 1 )
⬆️关系运算从左往右运算。先算3 <= x,输出1表示true;再算1 <= 5,输出1表示true。

  1. 下述对C语言字符数组的描述中错误的是( C )。

A.字符数组可以存放字符串
B.字符数组中的字符串可以整体输入、输出
C.可以在赋值语句中通过赋值运算符”=”对字符数组整体赋值
D.不可以用关系运算符对字符数组中的字符串进行比较
我错选了B。
可以用puts( )和gets( )实现字符数组的整体输入输出,用printf(“%s”,str)整体输出。数组没法整体引用,每次只能用下标引用数组中的一个元素。
只能在刚定义字符数组时用=给字符数组“赋值”,形如char name[MAXN] = "Kaleo";,但这叫做“初始化”,和赋值的原理是不一样的。不能用=来整体赋值字符数组,要不然strcpy( )函数的存在还有什么意义呢?🤣
同样的,字符串比较不能用关系运算符,不然strcmp( )函数的存在还有什么意义呢?

  1. 有以下定义:char x[ ]="abcdefg"; char y[ ]={'a', 'b', 'c', 'd', 'e', 'f', 'g'};,则正确的叙述为( C )。

A.数组x和数组y等价
B.数组x和数组y的长度相同
C.数组x的长度大于数组y的长度
D.数组x的长度小于数组y的长度
计算字符串的长度用strlen( x ),它遇到'\0'才能停止,并且不会把'\0'计入字符总数;计算数组长度用sizeof( y ),会把'\0'计入数组长度。x最后比y多了一个'\0',所以x数组长度比y大。
这里y[ ]最后没有'\0',所以不是字符串,不能用strlen( y )算它长度,不然strlen( )函数一直没读到'\0',程序就会停不下来,最后崩溃。同样地,puts( )函数遇到'\0'才会停止,忘了在字符数组结尾放'\0'也会导致程序崩溃。

  1. int c[2][ ]={{1, 2}, {3, 4}};(❌)

这样定义矩阵是错的,超过一维的数组,只有第一个指标可以是空的。像这样写编译时会报错:[Error] declaration of ‘c’ as multidimensional array must have bounds for all dimensions except the first.

  1. int m, *p = &m; printf("%d",p);(✔️)

定义变量m的时候,就已经给它分配内存空间了,只不过这块内存空间没初始化,用指针访问这块内存空间完全是可行的。用%d格式输出指针也不会出问题,只不过它是用十进制表示了指针变量指向的内存空间地址。用%p格式当然更好,是用十六进制输出的,更加专业。
image.png

  1. 以下选项中,对基本类型相同的指针变量不能进行运算的运算符是 ( A )。

A. +
B. -
C. =
D. ==
B和D选项的用法如下:

  1. #include<stdio.h>
  2. int main()
  3. {
  4. char c[20] = "I am learning C.";
  5. char *p1 = &c[0], *p2 = &c[10];
  6. printf("两指针相隔的元素个数为%d\n", p2 - p1);
  7. if(p1 == p2)
  8. printf("两指针指向同一内存空间");
  9. else if(p1 != p2)
  10. printf("两指针不指向同一内存空间");
  11. return 0;
  12. }
  1. 一维数组定义的一般形式如下,其中的类型名指定数组变量的类型。(❌)

类型名 数组名[数组长度];
类型名是指数组元素的类型。很多同学以为“数组变量”是指数组元素,其实对于C而言,数组变量就是数组名,它的值等于数组第一个元素的内存地址。这个名字起得很奇怪,因为数组变量里有“变量”两个字,它的值却是不能改变的,是个常量。🤔

  1. 对于以下程序段,叙述正确的是( D )。

    1. char s[ ]="china";
    2. char *p;
    3. p = s;

    A. s和p完全相同
    B. 数组s中的内容和指针变量p中的内容相等
    C. 数组s的长度和p所指向的字符串长度相等
    D. *p与s[0]相等
    s和p的数据类型不一样,s是数组变量,p是字符指针变量。p被赋值为s,也就指向了字符串(结尾带'\0'的字符数组)的第一个字符,变量p中的内容是'c'。数组长度包括结尾的'\0',而字符串长度不包括。

  2. 下面程序段的运行结果是( B )。

A. LANGUAGE
B. ANGU
C. LANGU
D. LANG

  1. char s[ ] = "language", *p = s;
  2. while( *p++ != 'u')
  3. printf("%c", *p 'a' + 'A');

相同优先级的单目运算是从右到左结合的,所以这里*p++相当于*(p++)
p不会乖乖等一次循环走完再+1,而是在判断完循环条件是否成立之后直接就+1了,别错选D。🤪

  1. 下列程序的执行结果是( 4 6 )

    1. #include <stdio.h>
    2. int main(void){
    3. int a[10], b[10], *pa, *pb, i;
    4. pa = a;
    5. pb = b;
    6. for( i = 0; i < 3; i++, pa++, pb++){
    7. *pa = i;
    8. *pb = 2*i;
    9. }
    10. pa = &a[0];
    11. pb = &b[0];
    12. for ( i = 0; i < 3; i++, pa++, pb++){
    13. *pa = *pa + i;
    14. *pb = *pb + i;
    15. }
    16. printf("%d %d", *--pa, *--pb);
    17. return 0;
    18. }

    考试的时候做这种比较复杂的程序阅读题,建议在草稿纸上用表格实时记录各个变量的值的变化,把自己当成电脑按字面意思读程序。不要太相信自己作为人类的记忆力和理解力。😇

  2. 下面程序段的运行结果是( ef )

    1. char str[]= "abc\0def\0ghi", *p = str;
    2. printf("%s", p+5) ;

    注意'\n''\t''\0'这样的转义符算一个字符,而不是两个。printf( )函数按%s格式输出时遇到'\0'就直接结束输出。

  3. 下面程序段的运行结果是( bcdBCD )

    1. char s[20]= "abcd" ;
    2. char *sp = s ;
    3. puts(strcat(sp+1, "ABCD"+1)) ;

    字符串常量的输出值也是首字符的地址,"ABCD"+1输出的就是'B'的地址。strcat( )函数把输入的两个地址指向的字符串"bcd""BCD"连接起来作为输出值。

  4. 如下代码块能输出最后i的值。(❌)

    1. for(int i=0; i<n; i++)
    2. {
    3. //随便什么循环体
    4. }
    5. printf("%d", i);

    这个i是定义在for循环里的,离开for循环之后就没了。

    三、上机考

    1. 习题8-4 报数

    报数游戏是这样的:有n个人围成一圈,按顺序从1到n编好号。从第一个人开始报数,报到m(本题要求编写函数,给出每个人的退出顺序编号。

    1. void CountOff( int n, int m, int out[] );

    其中n是初始人数;m是游戏规定的退出位次(保证为小于n的正整数)。函数CountOff将每个人的退出顺序编号存在数组out[]中。因为C语言数组下标是从0开始的,所以第i个位置上的人是第out[i-1]个退出的。 ```c

    include

    define MAXN 20

void CountOff( int n, int m, int out[] );

int main() { int out[MAXN], n, m; int i;

  1. scanf("%d %d", &n, &m);
  2. CountOff( n, m, out );
  3. for ( i = 0; i < n; i++ )
  4. printf("%d ", out[i]);
  5. printf("\n");
  6. return 0;

}

/ 你的代码将被嵌在这里 /

  1. 输入样例:<br />`11 3`<br />输出样例:<br />`4 10 1 7 5 2 11 9 3 6 8`
  2. ```c
  3. void CountOff( int n, int m, int out[] )
  4. {
  5. int count = 0, all = 0, i; /*count记录每次数到了第几个人,all记录退出了几个人*/
  6. for( i=0; i<n; i++ ) out[i] = 0; /*预先把数组元素清零*/
  7. i = 0; /*从下标为0的元素开始数*/
  8. while( all < n ) /*循环到n个人都退出为止*/
  9. {
  10. if( !out[i] && count!=m-1 ) /*必须要!out[i]。如果out[i]不是0说明这个人已经退出了*/
  11. count++;
  12. else if( !out[i] && count==m-1 ) /*数到m个人了,让第m个人退出*/
  13. {
  14. out[i] = all + 1; /*在数组中注明这个人是第几个退出的人*/
  15. count = 0;
  16. all++;
  17. }
  18. i++;
  19. i %= n;
  20. }
  21. }

这一题是经典的“猴子选大王”问题的变形,对初学者来说比较复杂也比较重要。

2. 习题8-5 使用函数实现字符串部分复制

本题要求编写函数,将输入字符串t中从第m个字符开始的全部字符复制到字符串s中。

  1. void strmcpy( char *t, int m, char *s );

函数strmcpy将输入字符串char t中从第m个字符开始的全部字符复制到字符串char s中。若m超过输入字符串的长度,则结果字符串应为空串。

  1. #include <stdio.h>
  2. #define MAXN 20
  3. void strmcpy( char *t, int m, char *s );
  4. void ReadString( char s[] ); /* 由裁判实现,略去不表 */
  5. int main()
  6. {
  7. char t[MAXN], s[MAXN];
  8. int m;
  9. scanf("%d\n", &m);
  10. ReadString(t);
  11. strmcpy( t, m, s );
  12. printf("%s\n", s);
  13. return 0;
  14. }
  15. /* 你的代码将被嵌在这里 */

输入样例:
7
happy new year
输出样例:
new year

  1. void strmcpy( char *t, int m, char *s )
  2. {
  3. int i = 0;
  4. m--;
  5. while(t[m] != NULL) /*这样t中表示字符串结尾的'\0'就没复制到s里*/
  6. {
  7. s[i] = t[m];
  8. m++; i++;
  9. }
  10. }
  1. #include<string.h> /*可以在自己写的函数前面调用库文件简化代码*/
  2. void strmcpy( char *t, int m, char *s )
  3. {
  4. int n, i, j = 0;
  5. memset(s, 0, sizeof(s)); /*要初始化字符串s,因为主函数中没初始化*/
  6. n = strlen(t); /*计算字符串t的长度*/
  7. for( i=m-1; i<=n; i++, j++ ) s[j]=t[i];
  8. /*注意i<=n保证了t结尾的'\0'也被复制到s中。结尾没有'\0'的话主函数中以%s格式输出s会出错*/
  9. }

3. 习题2-6 求阶乘序列前N项和

本题要求编写程序,计算序列 1!+2!+3!+⋯ 的前N项之和。
输入格式:
输入在一行中给出一个不超过12的正整数N。
输出格式:
在一行中输出整数结果。
输入样例:
5
输出样例:
153

  1. #include<stdio.h>
  2. double fact();
  3. int main()
  4. {
  5. int i, n;
  6. double sum = 0;
  7. scanf("%d", &n);
  8. for( i=1; i<=n; i++ )
  9. sum += fact();
  10. printf("%d", sum); //这一行写错了!
  11. return 0;
  12. }
  13. double fact()
  14. {
  15. static double n = 1;
  16. static int i = 1;
  17. n *= i;
  18. i++;
  19. return n;
  20. }

这在我自己电脑上编译运行的结果是正确的,但PTA上运行出来都是WA。
原因是printf写错了。sum是double类型的变量,用int型格式输出double类型变量属于未定义行为。我自己电脑上的编译器会好心地帮我把double强制类型转换成int,而PTA的编译器没那么聪明,就强行按照int的格式输出,然后就出错了。
改成printf("%d", (int)sum);就通过了。

4. 习题10-11 有序表的增删改查操作

首先输入一个正整数N(1≤N≤1000)和一个无重复元素的、从小到大排列的、N个元素的有序表,然后在屏幕上显示以下菜单(编号和选项):

  1. [1] Insert
  2. [2] Delete
  3. [3] Update
  4. [4] Query
  5. [Other option] End

用户可以反复对该有序表进行插入、删除、修改和查找操作,也可以选择结束。当用户输入编号1~4和相关参数时,将分别对该有序表进行插入、删除、修改和查找操作,输入其他编号,则结束操作。本题要求实现4个函数,分别在有序表(数组)中插入、删除、修改、查找一个值。

  1. int insert(int a[ ], int value);
  2. int del(int a[ ], int value);
  3. int modify(int a[ ], int value1, int value2);
  4. int query(int a[ ], int value);

函数insert在有序数组a中插入一个值为value的元素,如果在数组a中已有值为value的元素,则返回-1。
函数del删除有序数组a中等于value的元素,如果在数组a中没有找到值为value的元素,则返回-1。
函数modify将有序数组a中等于value1的元素,替换为value2 ,如果在数组a中没有找到值为value1的元素或者value2已在数组a中存在,则返回-1。
函数query用二分法在有序数组a中查找元素value,如果找到,则返回相应的下标;如果没有找到,则返回-1。

  1. /* 有序表的增删改查操作 */
  2. #include<stdio.h>
  3. #define MAXN 10000 /* 定义符号常量表示数组a的长度 */
  4. int Count = 0; /* 用全局变量Count表示数组a中待处理的元素个数 */
  5. void select(int a[], int option, int value); /* 决定对有序数组a进行何种操作的控制函数 */
  6. int input_array(int a[ ]); /* 输入有序数组a的函数 */
  7. void print_array(int a[ ]); /* 输出有序数组a的函数 */
  8. int insert(int a[ ], int value); /* 在有序数组a中插入一个值为value的元素的函数 */
  9. int del(int a[ ], int value); /* 删除有序数组a中等于value的元素的函数 */
  10. int modify(int a[ ], int value1, int value2); /* 将有序数组a中等于value1的元素,替换为value2 */
  11. int query(int a[ ], int value); /* 用二分法在有序数组a中查找元素value的函数 */
  12. int main(void)
  13. {
  14. int option, value, a[MAXN];
  15. if(input_array(a) == -1){ /* 调用函数输入有序数组 a */
  16. printf("Error"); /* a不是有序数组,则输出相应的信息 */
  17. return 0;
  18. }
  19. printf("[1] Insert\n"); /* 以下4行显示菜单*/
  20. printf("[2] Delete\n");
  21. printf("[3] Update\n");
  22. printf("[4] Query\n");
  23. printf("[Other option] End\n");
  24. while (1) { /* 循环 */
  25. scanf("%d", &option); /* 接受用户输入的编号 */
  26. if (option < 1 || option > 4) { /* 如果输入1、2、3、4以外的编号,结束循环 */
  27. break;
  28. }
  29. scanf("%d", &value); /* 接受用户输入的参数value */
  30. select(a, option, value); /* 调用控制函数 */
  31. printf("\n");
  32. }
  33. printf("Thanks."); /* 结束操作 */
  34. return 0;
  35. }
  36. /* 控制函数 */
  37. void select(int a[ ], int option, int value)
  38. {
  39. int index, value2;
  40. switch (option) {
  41. case 1:
  42. index = insert(a, value); /* 调用插入函数在有序数组 a 中插入元素value */
  43. if(index == -1){ /* 插入数据已存在,则输出相应的信息 */
  44. printf("Error");
  45. }else{
  46. print_array(a); /* 调用输出函数,输出插入后的有序数组a */
  47. }
  48. break;
  49. case 2:
  50. index = del(a, value); /* 调用删除函数在有序数组 a 中删除元素value */
  51. if(index == -1){ /* 没找到value,则输出相应的信息 */
  52. printf("Deletion failed.");
  53. }else{
  54. print_array(a); /* 调用输出函数,输出删除后的有序数组a */
  55. }
  56. break;
  57. case 3:
  58. scanf("%d", &value2); /* 接受用户输入的参数value2 */
  59. index = modify(a, value, value2); /* 调用修改函数在有序数组 a 中修改元素value的值为value2 */
  60. if(index == -1){ /* 没找到value或者vaule2已存在,则输出相应的信息 */
  61. printf("Update failed.");
  62. }else{
  63. print_array(a); /* 调用输出函数,输出修改后的有序数组a */
  64. }
  65. break;
  66. case 4:
  67. index = query(a, value); /* 调用查询函数在有序数组 a 中查找元素value */
  68. if(index == -1){ /* 没找到value,则输出相应的信息 */
  69. printf( "Not found.");
  70. }else{ /* 找到,则输出对应的下标 */
  71. printf("%d", index);
  72. }
  73. break;
  74. }
  75. }
  76. /* 有序表输入函数 */
  77. int input_array(int a[ ])
  78. {
  79. scanf("%d", &Count);
  80. for (int i = 0; i < Count; i ++) {
  81. scanf("%d", &a[i]);
  82. if(i > 0 && a[i] <= a[i-1]){ /* a不是有序数组 */
  83. return -1;
  84. }
  85. }
  86. return 0;
  87. }
  88. /* 有序表输出函数 */
  89. void print_array(int a[ ])
  90. {
  91. for (int i = 0; i < Count; i ++) { /* 输出时相邻数字间用一个空格分开,行末无空格 */
  92. if(i == 0){
  93. printf("%d", a[i]);
  94. }else{
  95. printf(" %d", a[i]);
  96. }
  97. }
  98. }
  99. /* 请在这里填写答案 */
  1. 6 -2 3 7 9 101 400
  2. 1
  3. 96
  4. 0
  1. [1] Insert
  2. [2] Delete
  3. [3] Update
  4. [4] Query
  5. [Other option] End
  6. -2 3 7 9 96 101 400
  7. Thanks.
  1. 6 -2 3 7 9 101 400
  2. 2
  3. 400
  4. 0
  1. [1] Insert
  2. [2] Delete
  3. [3] Update
  4. [4] Query
  5. [Other option] End
  6. -2 3 7 9 101
  7. Thanks.
  1. 6 -2 3 7 9 101 400
  2. 3
  3. 7
  4. 10
  5. 0
  1. [1] Insert
  2. [2] Delete
  3. [3] Update
  4. [4] Query
  5. [Other option] End
  6. -2 3 9 10 101 400
  7. Thanks.
  1. 6 -2 3 7 9 101 400
  2. 4
  3. -2
  4. 0
  1. [1] Insert
  2. [2] Delete
  3. [3] Update
  4. [4] Query
  5. [Other option] End
  6. 0
  7. Thanks.
  1. 6 -2 3 7 9 101 400
  2. 2
  3. 101
  4. 1
  5. -10
  6. 4
  7. 101
  8. 3
  9. 400
  10. 444
  11. 0
  1. [1] Insert
  2. [2] Delete
  3. [3] Update
  4. [4] Query
  5. [Other option] End
  6. -2 3 7 9 400
  7. -10 -2 3 7 9 400
  8. Not found.
  9. -10 -2 3 7 9 444
  10. Thanks.
  1. int insert(int a[ ], int value)
  2. {
  3. int i = 0;
  4. while(a[i]<value && a[i]!=0) i++; //已经有Count了,没必要自己找数组有几个元素
  5. //而且a[i]!=0这个判断标准也不对,有序表中完全有可能出现值为0的元素
  6. if(a[i]==value) return -1;
  7. int j = i;
  8. while( a[j]!=0 ) j++;
  9. for(; j>i; j--)
  10. a[j] = a[j-1];
  11. a[i] = value;
  12. Count++;
  13. return 0;
  14. }
  15. int del(int a[ ], int value)
  16. {
  17. int i = query(a, value);
  18. if(i==-1) return -1;
  19. while(a[i]!= 0)
  20. {
  21. a[i] = a[i+1];
  22. i++;
  23. }
  24. Count--;
  25. return 0;
  26. }
  27. int modify(int a[ ], int value1, int value2)
  28. {
  29. int c = del(a, value1);
  30. int b = insert(a, value2);
  31. if(c==-1||b==-1) return -1; //应该在del和insert之后各检查一次是否返回了-1
  32. return 0;
  33. }
  34. int query(int a[ ], int value)
  35. {
  36. int right = 0;
  37. while(a[right]!=0) right++;
  38. right--;
  39. int left = 0, mid = (left+right)/2;
  40. while(left<=right)
  41. {
  42. if(a[mid]<value) left = mid+1;
  43. else if(a[mid]>value) right = mid-1;
  44. else return mid;
  45. mid = (left+right)/2;
  46. } //二分查找改错改了好久。这种有很多细节要注意的常用代码最好还是背下来
  47. return -1;
  48. }

这段代码有一个case是错的,PTA提示说是输入样例5的等价样例出错。奇怪的是,虽然运行出来是错的,但在PTA调试中运行结果样例5,输出却是对的。
![KTACTPDCCL]F(O__)(6[(M.png](https://cdn.nlark.com/yuque/0/2022/png/25775350/1649210866339-4f689392-3fe7-4b45-990e-552b0bfb97b4.png#clientId=ub1b0debf-f6a3-4&crop=0&crop=0.0055&crop=1&crop=1&from=paste&height=180&id=u2c76073f&margin=%5Bobject%20Object%5D&name=KTAC%60TPDCCL%5DF%28O__%29%286%5B%28M.png&originHeight=302&originWidth=420&originalType=binary&ratio=1&rotation=0&showTitle=false&size=42351&status=done&style=none&taskId=ua9c674b1-f0bb-4382-acc5-f6739881a78&title=&width=250)<br />这题里没用到野指针,也没有数组下标越界。后来发现是因为第4行用a[i]!=0判断是否结束循环,导致在测试样例的有序表中出现0时提前退出循环。而五个输入样例中都没出现0,所以我按照五个输入样例去试就发现不了错在哪。<br />![H}J1~KWGSVF`LMSDH$QR8H.png](https://cdn.nlark.com/yuque/0/2022/png/25775350/1649333564715-bedfcee7-c485-490a-8d67-2e2f4d0f66fb.png#clientId=u22296693-de05-4&crop=0&crop=0&crop=1&crop=0.8931&from=paste&height=559&id=uf814075f&margin=%5Bobject%20Object%5D&name=H%7DJ1~K%60WGSVF%60LMSDH%24QR8H.png&originHeight=956&originWidth=766&originalType=binary&ratio=1&rotation=0&showTitle=true&size=131179&status=done&style=none&taskId=u6450dea5-b99d-40b8-b230-9081b087095&title=%E2%80%9C%E6%B7%86%E4%B9%A0%E2%80%9D%E6%98%AF%E6%B2%B3%E5%8C%97%E8%AF%9D%E9%87%8C%E7%9A%84%E2%80%9C%E5%AD%A6%E4%B9%A0%E2%80%9D&width=448 ““淆习”是河北话里的“学习””)

  1. int insert(int a[ ], int value)
  2. {
  3. int i = 0;
  4. while( a[i]<value&&i<Count ) i++;
  5. if( a[i]==value ) return -1;
  6. int j = Count;
  7. for(; j>i; j-- )
  8. {
  9. a[j] = a[j-1];
  10. }
  11. a[i] = value;
  12. Count++;
  13. return 0;
  14. }
  15. int del(int a[ ], int value)
  16. {
  17. int i = query(a, value);
  18. if(i==-1) return -1;
  19. for(; i<Count; i++ )
  20. a[i] = a[i+1];
  21. Count--;
  22. return 0;
  23. }
  24. int modify(int a[ ], int value1, int value2)
  25. {
  26. int c = del(a, value1);
  27. if(c==-1) return -1;
  28. int b = insert(a, value2);
  29. if(b==-1) return -1;
  30. return 0;
  31. }
  32. int query(int a[ ], int value)
  33. {
  34. int right = Count-1;
  35. int left = 0, mid = (left+right)/2;
  36. while(left<=right)
  37. {
  38. if(a[mid]<value) left = mid+1;
  39. else if(a[mid]>value) right = mid-1;
  40. else return mid;
  41. mid = (left+right)/2;
  42. }
  43. return -1;
  44. }

5. 习题7-10 水仙花数

水仙花数是指一个N位正整数(N≥3),它的每个位上的数字的N次幂之和等于它本身。例如:153=13+53+33。 本题要求编写程序,计算所有N位水仙花数。
输入格式:
输入在一行中给出一个正整数N(3≤N≤7)。
输出格式:
按递增顺序输出所有N位水仙花数,每个数字占一行。
输入样例:
3
输出样例:
153
370
371
407
本题中lry同学遇到的困难是N=7的情况会运行超时。
思路1:用原来的慢程序提前算好N=7时的水仙花数,写进新程序里,当用户输入N=7时不用计算,直接输出预先算好的值。(这在工业界也是常规方法,大家别歧视这种“奇技淫巧”!🤪)
思路2:程序慢的原因是对每个七位数都调用了七次pow( )函数计算其各位数字的七次方,而pow()函数是非常费时的,不如提前用一个10元数组储存0N、1N、……、9N,这样就不需要每次都重新计算各位数字的N次方了。

  1. #include<stdio.h>
  2. #include<math.h>
  3. int main()
  4. {
  5. int N, i, j, sum;
  6. scanf("%d", &N);
  7. int a[10];
  8. for(i=0; i<10; i++)
  9. a[i] = pow(i, N); //用数组储存每个个位数的N次幂
  10. for(i=pow(10, N-1); i<pow(10, N); i++)
  11. {
  12. j = i; sum = 0;
  13. while(j>0)
  14. {
  15. sum += a[j % 10];
  16. j /= 10;
  17. }
  18. if(sum==i) printf("%d\n", i);
  19. }
  20. //不要给判断是否为水仙花数单独建个函数,因为每次调用函数也要花时间的
  21. return 0;
  22. }

6. 习题2-12 输出华氏-摄氏温度转换表

输入2个正整数lower和upper(lower≤upper≤100),请输出一张取值范围为[lower,upper]、且每次增加2华氏度的华氏-摄氏温度转换表。温度转换的计算公式:C=5×(F−32)/9,其中C表示摄氏温度,F表示华氏温度。
输入格式:
在一行中输入2个整数,分别表示lower和upper的值,中间用空格分开。
输出格式:
第一行输出:”fahr celsius”,接着每行输出一个华氏温度fahr(整型)与一个摄氏温度celsius(占据6个字符宽度,靠右对齐,保留1位小数)。若输入的范围不合法,则输出”Invalid.”。
输入样例1:
32 35
输出样例1:
fahr celsius
32 0.0
34 1.1
输入样例2:
40 30
输出样例2:
Invalid.

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int low, upp;
  5. scanf("%d%d", &low, &upp);
  6. if( low>upp||low>100 )
  7. printf("Invalid.");
  8. else
  9. {
  10. printf("fahr celsius\n");
  11. for(; low<=upp; low += 2)
  12. printf("%d%6.1f\n", low, 5.0*(low-32)/9);
  13. }
  14. }

这题主要是输出格式容易错,“占据6个字符宽度,靠右对齐,保留1位小数”就是%6.1f
可以参考这篇笔记来复习:C语言printf()输出格式大全

7. 习题3-2 高速公路超速处罚

按照规定,在高速公路上行使的机动车,达到或超出本车道限速的10%则处200元罚款;若达到或超出50%,就要吊销驾驶证。请编写程序根据车速和限速自动判别对该机动车的处理。
输入格式:
输入在一行中给出2个正整数,分别对应车速和限速,其间以空格分隔。
输出格式:
在一行中输出处理意见:若属于正常行驶,则输出“OK”;若应处罚款,则输出“Exceed x%. Ticket 200”;若应吊销驾驶证,则输出“Exceed x%. License Revoked”。其中x是超速的百分比,精确到整数。
输入样例1:
65 60
输出样例1:
OK
输入样例2:
110 100
输出样例2:
Exceed 10%. Ticket 200
输入样例3:
200 120
输出样例3:
Exceed 67%. License Revoked

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int v, lim;
  5. scanf("%d%d", &v, &lim);
  6. if(v < 1.1*lim) printf("OK");
  7. else if(v < 1.5*lim) printf("Exceed %d%%. Ticket 200", (int)(100.0*(v-lim)/lim));
  8. else printf("Exceed %d%%. License Revoked", (int)(100.0*(v-lim)/lim));
  9. return 0;
  10. }

这个解答的bug是:分段函数三段的临界值会出错,且输出的超速百分比有误差。这些都是计算机内浮点数运算自带的误差导致的。

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int v, lim;
  5. scanf("%d%d", &v, &lim);
  6. if(v < 1.1*lim) printf("OK");
  7. else if(v < 1.5*lim) printf("Exceed %.0f%%. Ticket 200", 100.0*(v-lim)/lim);
  8. else printf("Exceed %.0f%%. License Revoked", 100.0*(v-lim)/lim);
  9. return 0;
  10. }

改成这样,输出的百分比的误差就可容忍了。题目要求精确到整数,不一定要强制类型转换成int,也可以用浮点数运算,然后输出零位小数,这样算得更准。但是临界值处依然是错的,输入110 100会输出OK
SD62UA))I1B76VS6}NE_D$H.png986V1JAZ(2KVM}{CV($}~OP.png

8. 习题11-1 输出月份英文名

本题要求实现函数,可以返回一个给定月份的英文名称。
函数接口定义:
char *getmonth( int n );
函数getmonth应返回存储了n对应的月份英文名称的字符串头指针。如果传入的参数n不是一个代表月份的数字,则返回空指针NULL。

  1. #include <stdio.h>
  2. char *getmonth( int n );
  3. int main()
  4. {
  5. int n;
  6. char *s;
  7. scanf("%d", &n);
  8. s = getmonth(n);
  9. if ( s==NULL ) printf("wrong input!\n");
  10. else printf("%s\n", s);
  11. return 0;
  12. }
  13. /* 你的代码将被嵌在这里 */

输入样例1:
5
输出样例1:
May
输入样例2:
15
输出样例2:
wrong input!

  1. char *getmonth( int n )
  2. {
  3. char* a[12] = {
  4. &"January", &"February", &"March", &"April",
  5. &"May", &"June", &"July", &"August",
  6. &"September", &"October", &"November", &"December"
  7. };
  8. if(n>0&&n<13)
  9. return a[n-1];
  10. else
  11. return NULL;
  12. }

两个错误:

  1. 不需要&,字符串本身的返回值就是它首字符的地址。
  2. 字符串是在getmonth函数里定义的,回到主函数之后这些字符串的内存空间被回收,主函数顺着getmonth函数返回的指针去找字符串,可能此时字符串还没被清理掉,也可能被清理掉了。所以运行的效果是有时候正常,有时候找不到字符串。

解决方法:

  1. 多加了&是C语言的未定义行为(Undefined Behavior),聪明的编译器会明白程序员的意图,自动忽略&。所以虽然这样写是错的,但也能正常运行。
  2. char* a[12]前面加static,或者干脆把这个字符指针数组定义为全局变量。

    9. 习题4-11 兔子繁衍问题

    一对兔子,从出生后第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又生一对兔子。假如兔子都不死,请问第1个月出生的一对兔子,至少需要繁衍到第几个月时兔子总数才可以达到N对?
    输入格式:
    输入在一行中给出一个不超过10000的正整数N。
    输出格式:
    在一行中输出兔子总数达到N最少需要的月数。
    输入样例:
    30
    输出样例:
    9
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int n, month=1;
    5. int f_0, f_1=0, f_2=1;
    6. scanf("%d", &n);
    7. while(f_2<n)
    8. {
    9. f_0 = f_1;
    10. f_1 = f_2;
    11. f_2 += f_0;
    12. month++;
    13. }
    14. printf("%d", month);
    15. return 0;
    16. }
    其实这题的代码很好写,就是一般的斐波那契数列。但说实话我一开始没看出来这是斐波那契数列。第(n-1)个月已经出生了的兔子都能活到第n个月,而第(n-2)个月已经出生了的兔子到第n个月都能生小兔子,所以第n个月的兔子总数就是第(n-1)个月已出生的兔子数加上第(n-2)个月已出生的兔子数,是斐波那契数列。
    事实上,斐波那契数列也叫兔子数列,就是因为这个关于兔子的数学问题。

    10. 练习7-10 查找指定字符

    本题要求编写程序,从给定字符串中查找某指定的字符。
    输入格式:
    输入的第一行是一个待查找的字符。第二行是一个以回车结束的非空字符串(不超过80个字符)。
    输出格式:
    如果找到,在一行内按照格式“index = 下标”输出该字符在字符串中所对应的最大下标(下标从0开始);否则输出”Not Found”。
    输入样例1:
    m
    programming
    输出样例1:
    index = 7
    输入样例2:
    a
    1234
    输出样例2:
    Not Found
    1. #include<stdio.h>
    2. #define MAXN 80
    3. int main()
    4. {
    5. int index = -1, i = 0;
    6. char c, s[MAXN];
    7. scanf("%c", &c);
    8. scanf("%s", s);
    9. while(s[i])
    10. {
    11. if(s[i]==c)
    12. index = i;
    13. i++;
    14. }
    15. if(index==-1)
    16. printf("Not Found\n");
    17. else
    18. printf("index = %d\n", index);
    19. return 0;
    20. }
    有一个index = 79,字符串中有空格的case没通过,在scanf("%s", s);后面加入puts(s);发现输出的字符串只有输入字符串空格之前的部分,原因是scanf( )函数以%s格式读取字符串只读到空格、制表符为止,要把空格及空格以后的部分读入字符串只能用getchar( )。
    1. #include<stdio.h>
    2. #define MAXN 81
    3. int main()
    4. {
    5. int index = -1, i = 0;
    6. char c, ch, s[MAXN];
    7. scanf("%c", &c);
    8. while(((ch=getchar())!='\n')&&(i<MAXN-1))
    9. {
    10. s[i] = ch;
    11. i++;
    12. }
    13. s[i] = '\0';
    14. i = 0;
    15. while(s[i]!='\0'&&i<MAXN)
    16. {
    17. if(s[i]==c)
    18. index = i;
    19. i++;
    20. }
    21. if(index==-1)
    22. printf("Not Found\n");
    23. else
    24. printf("index = %d\n", index);
    25. return 0;
    26. }
    这一次不管怎样输出都是Not Found,在第一个while循环后用puts( )输出字符串,发现输出为空;再在第一个while循环里用printf( )输出每次读入的ch,没有输出,说明没有进入第一个while循环。原因是输入的第一行除了一个字符,还有行末的一个回车,会在第一个while循环处被getchar( )读入储存给ch。ch==’\n’,不满足循环条件,就直接退出了。
    1. #include<stdio.h>
    2. #define MAXN 81
    3. int main()
    4. {
    5. int index = -1, i = 0;
    6. char c, ch, s[MAXN];
    7. scanf("%c", &c);
    8. getchar(); //增加这一行代码,读走第一行结尾的\n即可
    9. while(((ch=getchar())!='\n')&&(i<MAXN-1))
    10. {
    11. s[i] = ch;
    12. i++;
    13. }
    14. s[i] = '\0';
    15. i = 0;
    16. while(s[i]!='\0'&&i<MAXN)
    17. {
    18. if(s[i]==c)
    19. index = i;
    20. i++;
    21. }
    22. if(index==-1)
    23. printf("Not Found\n");
    24. else
    25. printf("index = %d\n", index);
    26. return 0;
    27. }

    11. 习题9-1 时间换算

    本题要求编写程序,以hh:mm:ss的格式输出某给定时间再过n秒后的时间值(超过23:59:59就从0点开始计时)。
    输入格式:
    输入在第一行中以hh:mm:ss的格式给出起始时间,第二行给出整秒数n(<60)。
    输出格式:
    输出在一行中给出hh:mm:ss格式的结果时间。
    输入样例:
    11:59:40
    30
    输出样例:
    12:00:10
    1. #include<stdio.h>
    2. int main()
    3. {
    4. int hh, mm, ss, plus;
    5. scanf("%d%d%d", &hh, &mm, &ss); //这一行有问题,改成"%d:%d:%d"就对了
    6. scanf("%d", &plus);
    7. ss += plus;
    8. mm += ss / 60;
    9. ss %= 60;
    10. hh += mm / 60;
    11. mm %= 60;
    12. hh %= 24;
    13. printf("%02d:%02d:%02d\n", hh, mm, ss); //输出两位整数,不足两位左边补0
    14. return 0;
    15. }
    在scanf( )后面输出了hh、mm、ss,发现只有hh是正常的,原因是读入格式%d%d%d不对。平时这样写可以正常读入三个整数,那是因为scanf( )把空格和制表符当作正常的分隔符,会自动跳过它们去读数字;而冒号是scanf( )不认识的分隔符,要让scanf( )知道应该跳过冒号去找下一个数字,就得在读入格式中把冒号写出来。
    想深入了解的话,可以去google一下scanf( )的实现方式。