第三章 · 结构化程序设计¶
本页最后更新于 ,内容可能过时
《C语言程序设计(第3版)》第 3 章课后习题,共 24 题。
1 · 统计 0~9 各数字出现次数¶
编写一个程序,输入一个整数,输出 0~9 各个数字在该整数中出现的次数。
答案
解析
思路:开一个长度为 10 的数组当计数器,下标就是数字本身。n % 10 取出最低位、n /= 10 把它丢掉,循环到 n 变成 0 为止。
关键点:
- 下标即数字。
cnt[n % 10]++一行就完成了"取位 + 计数",不用switch分十种情况写。 - 负号要先处理掉,否则
-1007会一直被% 10取到负的下标。题干说的是"输入一个整数",没有限定正数,这一步必须加。 n本身就是0时循环一次都不执行,0这个数字要单独记一次。- 数组要写初值
{0}。函数里定义的局部数组不会自动清零,漏了初值计数器里就是随机值。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
1220340,输出0出现 2 次、1出现 1 次、2出现 2 次、3出现 1 次、4出现 1 次,5到9各 0 次;输入-1007得0两次、1一次、7一次;输入0得0出现 1 次、其余全 0。易错:
- 数组没写初值
- 忘了负号,
n % 10取出负下标- 输入
0时输出全0(循环一次没跑)
2 · 英文字符与 ASCII 码值表¶
编写一个程序,输出所有英文字符及它们的 ASCII 码值,其中代码值分别用八进制形式、十六进制形式和十进制形式输出。
答案
解析
思路:英文字符就是 26 个大写加 26 个小写,两段连着的区间;每段一个循环,循环变量直接当字符用。
关键点:
char参与运算时会自动提升成int,所以同一个c可以喂给%c、%o、%x、%d四个转换说明符,实参写四遍c就行。- 大写区间是
'A'~'Z'(65~90),小写区间是'a'~'z'(97~122)。'A'到'Z'在 ASCII 里是连续的,所以for (c = 'A'; c <= 'Z'; c++)成立。 - 不要用
for (c = 65; c <= 90; c++)这种码值循环。写'A'、'Z'一眼能看出是字符,而且换字符集也还对。 - 用
%o打八进制,%x打十六进制(小写字母)。十六进制里10~15会打成a~f。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行共 52 行,每行格式为"字符 + 两个空格 + 八进制 + 两个空格 + 十六进制 + 两个空格 + 十进制"。首行
A 101 41 65,大写段末行Z 132 5a 90,小写段首行a 141 61 97,末行z 172 7a 122。易错:
- 只输出大写或只输出小写(题干说"所有英文字符",52 个)
- 十六进制用了
%X,打出来是大写5A(本题没说大小写,两种都对,但要和格式串一致)- 把
%d当成"另一种写法"而漏掉真正的十进制输出
3 · 两种循环执行后的变量值¶
设有整型变量 x 和 y 的值分别为 5 和 110。试指出执行完以下循环语句后,变量 x 和 y 的值分别是多少?
(1)while(x<=y)x*=2;
(2)do{x=y/x;y=y-x;}while(y>=1);
答案
| 序号 | 答案 |
|---|---|
| (1) | x = 160,y = 110 |
| (2) | x = 18,y = 0 |
解析
两小题都从 x = 5、y = 110 出发,区别只在循环的形态。
逐题推演:
- (1)
while (x <= y) x *= 2;:循环体只有一条自乘语句,y从头到尾没被碰过,所以答案里y一直是110。x的取值依次是5、10、20、40、80、160;到160时160 <= 110不成立,循环退出。共执行 5 次。 - (2)
do { x = y / x; y = y - x; } while (y >= 1);:do-while的循环体至少执行一次,而且循环体里两句是有先后的——先算出新的x,再拿这个新的x去更新y。y / x是整型除法,除不尽的小数部分直接丢掉。逐轮如下:
| 轮次 | 进入本轮时的 x、y |
x = y / x 之后 |
y = y - x 之后 |
y >= 1 |
|---|---|---|---|---|
| 1 | 5, 110 | 22 | 88 | 真 |
| 2 | 22, 88 | 4 | 84 | 真 |
| 3 | 4, 84 | 21 | 63 | 真 |
| 4 | 21, 63 | 3 | 60 | 真 |
| 5 | 3, 60 | 20 | 40 | 真 |
| 6 | 20, 40 | 2 | 38 | 真 |
| 7 | 2, 38 | 19 | 19 | 真 |
| 8 | 19, 19 | 1 | 18 | 真 |
| 9 | 1, 18 | 18 | 0 | 假 |
第 9 轮结束时 y 变成 0,0 >= 1 为假,循环退出,此时 x = 18。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):两段代码各跑一遍,输出
(1) x=160 y=110和(2) x=18 y=0,与上表逐轮推出的一致。
第(2)小题里 x 的赋值顺序不能调换
循环体写成 y = y - x; x = y / x; 就是另一回事了:那样每轮先用旧的 x 减掉、再用新的 y 去除,答案会变成 x = 1、y = 0。题干给的顺序是 x = y / x 在前,y = y - x 在后,y 用的是刚更新过的 x。
易错:
- (2)用
while去读,漏掉"循环体至少执行一次",少数一轮- (2)里把
y - x的x当成每轮开头那个值(它已经是y / x算完的新值)- (1)把
x *= 2的执行次数数成 6 次(160那一次进不去循环体了)
4 · 求小于 999 的水仙花数¶
水仙花数是一个 n(n≥3)位数字的数,它等于每个数字的 n 次幂之和。
例如,153 是一个水仙花数,153=1³+5³+3³。试编写一个程序求小于 999 的所有水仙花数。
答案
解析
思路:n 至少 3 位、又要小于 999,那就只可能是三位数,枚举范围直接取 100~998。三位数拆位用整除和取余两下就能拿到,不用数组、不用循环。
关键点:
- 拆位:
n / 100是百位,n / 10 % 10是十位,n % 10是个位。先除以10把个位丢掉,再对10取余,剩下的就是十位。 - 按题干,
n就是位数。这题里n恒为3,所以每个数字都是三次幂。如果题目改成"求1000以内的水仙花数",四位数要用四次幂,位数得现算,那时才需要另想办法求位数。 - 循环上界写
n < 999,对应题干"小于999"。写成n <= 999也不会多打出什么(999不满足条件),但那是碰巧,口径该跟题干对齐。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,打出的四位是
153、370、371、407,分别对应153 = 1^3 + 5^3 + 3^3、370 = 3^3 + 7^3 + 0^3、371 = 3^3 + 7^3 + 1^3、407 = 4^3 + 0^3 + 7^3。易错:
- 循环从
1开始(1 位、2 位数不满足题干的n≥3,1 = 1³、153之外的两位数都凑不出来,但范围本身就错了)- 十位写成
n % 100 / 10之外的样式记混,或直接写n / 10(那拿到的是前两位)- 三段幂和忘了乘第三下,写成
a * a + b * b + c * c
5 · 百鸡问题¶
编写程序解百鸡问题:鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一。百钱买百鸡。问鸡翁、鸡母和鸡雏各几何?
答案
解析
思路:三个未知量只有两个方程,解不唯一,只能枚举。把"一共 100 只"这个条件用来消掉一个未知量,循环就从三层降到两层。
关键点:
- 鸡雏的只数由
c = 100 - w - m直接减出来,不用为它再套一层循环。 - 枚举范围按钱数卡:鸡翁最多
20只(20×5 = 100),鸡母最多33只(33×3 = 99),鸡雏的只数由前两者决定。 - 鸡雏三只一钱,所以
c必须是3的倍数,条件里先写c % 3 == 0。漏掉这一条,c / 3的整型除法会把零头丢掉,凑出钱数其实不对的解。 - 钱数判据写成整数等式
5 * w + 3 * m + c / 3 == 100。不要写成== 100.0那种浮点比较,整数解本来就能精确判。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,共打出四组解:鸡翁
0、鸡母25、鸡雏75;鸡翁4、鸡母18、鸡雏78;鸡翁8、鸡母11、鸡雏81;鸡翁12、鸡母4、鸡雏84。四组都是100只、100钱。易错:
- 忘了判
c % 3 == 0,把不能整钱买鸡雏的情况也算成解- 鸡雏那层循环也从
0数到100,三层循环白跑- 用
c / 3.0凑钱数,再拿浮点数判相等
6 · 逐位输出整数的十进制数值¶
编写一个程序,输入一个整数,逐位地输出整数的十进制数值,要求位与位之间有一个空格符分隔。
答案
解析
思路:题目要求从左到右逐位输出,而取余只能先拿到最低位。用递归把顺序倒过来:先让剩下的高位递归打完,再打自己这一位。
关键点:
- 直接写
while (n > 0) { printf("%d ", n % 10); n /= 10; }打出的是逆序,12345会变成5 4 3 2 1。要么像上面这样递归,要么先把各位存进数组再倒着打。 - 空格加在"两个数字之间"。
if (n >= 10)里先递归、再补空格、最后打自己的个位;这样最高位前面和最低位后面都没有多余空格。 - 负号先单独打掉,剩下的按正数处理。
-708的输出是-7 0 8。 n是0时递归只走一层,打出一个0。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
12345输出1 2 3 4 5;输入-708输出-7 0 8。
负数的取余结果是负的
-708 % 10 在 C 里是 -8 而不是 8(C 的整除向零取整,余数跟着被除数走)。如果不先把负号摘掉,拆出来的每一位都带负号,输出会变成 -7 -0 -8 这种样子。这一条和第二章题 14 讲的整型除法是同一个规则。
易错:
- 用取余循环直接打,得到逆序
- 末尾多出一个空格(先打数字再判断要不要打空格)
- 负号没摘掉,
% 10出来是负数
7 · 1~10 的平方与立方表¶
编写程序,列表输出整数 1~10 的平方和立方值。
答案
解析
思路:一个循环从 1 数到 10,三个量(本身、平方、立方)按固定宽度打成一列一列的表。
关键点:
- 表格的整齐靠字段宽度,格式串里三列写成
%4d%8d%10d。宽度取多少看到底最大的数是几位:n最多 2 位,平方最多 3 位(100),立方最多 4 位(1000),所以4/8/10宽绰有余。 - 表头也用同样的宽度写(
%4s%8s%10s),表头和数据才对齐。表头里不要塞中文再指定宽度——%8s数的是字节数,一个汉字占 3 字节,屏幕上却是 2 列宽,算出来的对齐是错的。 - 平方、立方用连乘写。
i * i和i * i * i都是整型,值不会溢出也不会有浮点误差。
输出:
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,输出上面这 11 行,与预期逐字相同。
易错:
- 三列宽度随手写成
%d,表看着是歪的- 指数用
pow(i, 2),那样算出来的是double,配%d打就错了- 循环写成
i < 10,漏掉10那一行
8 · 无限循环与有符号溢出¶
编写一个程序,用循环不停地输出 2 的倍数,观察最终会发生什么情况。
答案
解析
思路:循环条件写恒真的 1,循环体每次把 i 加 2,输出的就是 2、4、6、8……这一串 2 的倍数。
关键点:
while (1)的条件下永远为真,程序自己不会停。要停下来只能从外面动手:按 Ctrl+C,或者像核实这一步一样用timeout命令限时。i每次加2,会一直加到超过int能表示的上限2147483647。取值序列是2、4、……、2147483646,下一个数2147483648就装不进int了——这就是有符号整型溢出。- 有符号整型溢出在 C 标准里是未定义行为,标准并没有规定"会翻成负数"。本机怎么走要跑了才算数。
本机的实际现象:
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):先用
timeout 3限时跑一次,3 秒里打出了 52988463 行、474 MB,最后一行停在105976926,进程被 timeout 杀掉(退出码124)——3 秒连溢出点都没到。改成一直跑、只在输出里截第一个负数,第一条负数是-2147483648,也就是第 1073741824 个数。此后i继续每次加2,输出的是-2147483648、-2147483646、……这样一串负偶数,程序仍然不停。
跑出来的数不是标准保证的结果
有符号整型溢出是未定义行为,标准只说"程序的行为没有定义",不说 i 一定变成 -2147483648。上面那个负数是本机跑出来的值,不能当标准结论。核实过程中还看到一件事:给探针程序加一句"i 变负就停下"的判断,在 -O2 下 gcc 默认有符号数不溢出,直接把它当成永假删掉了,探针于是围着 2147483640~2147483646 这一段一圈一圈地转,始终没停。同一段逻辑在 -O0 下就正常触发了。这就是未定义行为的典型表现——优化档不同,程序跑出来的东西可以完全不一样。
易错:
- 以为
i到2147483646之后会自己停下来(循环条件里根本没有上限判断)- 把
i定义成unsigned再照抄一遍——无符号溢出是标准定义好的行为,会规规矩矩绕回0,那就不叫"观察未定义行为"了- 直接照着"溢出就变负数"写结论,不把跑出来的数写出来
9 · 三角形判定与面积¶
编写一个程序,输入 3 个实数,判断这些值是否能作为一个三角形的三条边的长,如果能构成三角形,要求输出三角形的面积。
答案
#include <stdio.h>
#include <math.h>
int main(void)
{
double a, b, c, s, area;
printf("请输入三条边长: ");
if (scanf("%lf%lf%lf", &a, &b, &c) != 3)
return 0;
if (a + b > c && a + c > b && b + c > a) {
s = (a + b + c) / 2;
area = sqrt(s * (s - a) * (s - b) * (s - c));
printf("能构成三角形,面积 = %.6f\n", area);
} else {
printf("不能构成三角形\n");
}
return 0;
}
解析
思路:先按"任意两边之和大于第三边"判能不能围成三角形,能围的再用海伦公式算面积。
关键点:
- 三个不等式都要写全:
a + b > c、a + c > b、b + c > a。只写一个,换个边长顺序就判错了。 - 海伦公式:先求半周长
s = (a + b + c) / 2,面积是sqrt(s * (s - a) * (s - b) * (s - c))。 - 三个变量都用
double,scanf里配%lf。(a + b + c) / 2里除号一边是double,做的是浮点除法,不会像两个整数相除那样截断。 - 用了
math.h里的sqrt就要链数学库,命令行里补一个-lm。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
3 4 5,输出能构成三角形,面积 = 6.000000;输入3 4 6,输出面积5.332682;输入6 8 10,输出面积24.000000;输入1 2 5和1 2 3,都输出不能构成三角形。
判据用的是严格大于号
三边满足"两边之和大于第三边"才算三角形。1 2 3 这种两边之和恰好等于第三边的情况是三点共线,围不出三角形,上面那次运行里它被判为不能构成。写成 >= 就把这一列也算进去了。
易错:
- 三个不等式只写一个
- 半周长写成
(a + b + c) / 2.0之外还写int接收,把小数部分丢了- 漏掉
-lm,链接时报undefined reference to sqrt
10 · 三点构成三角形的判定与特征¶
编写程序,输入 3 对实数,若每对实数分别作为平面坐标系中点的坐标,判断这 3 个点是否能构成一个三角形。
若能构成三角形,判断这个三角形的特征:是直角三角形、等边三角形、等腰三角形还是普通三角形等。
答案
#include <stdio.h>
#include <math.h>
static double dist(double x1, double y1, double x2, double y2)
{
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
static int eq(double a, double b)
{
return fabs(a - b) < 1e-9;
}
int main(void)
{
double x1, y1, x2, y2, x3, y3, a, b, c, s, area;
printf("请输入三对坐标: ");
if (scanf("%lf%lf%lf%lf%lf%lf", &x1, &y1, &x2, &y2, &x3, &y3) != 6)
return 0;
a = dist(x2, y2, x3, y3);
b = dist(x1, y1, x3, y3);
c = dist(x1, y1, x2, y2);
if (eq((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1), 0.0)) {
printf("三点共线,不能构成三角形\n");
return 0;
}
s = (a + b + c) / 2;
area = sqrt(s * (s - a) * (s - b) * (s - c));
printf("能构成三角形,面积 = %.6f\n", area);
if (eq(a, b) && eq(b, c))
printf("特征: 等边三角形\n");
else if (eq(a, b) || eq(b, c) || eq(a, c))
printf("特征: 等腰三角形\n");
else if (eq(a * a + b * b, c * c) || eq(a * a + c * c, b * b) ||
eq(b * b + c * c, a * a))
printf("特征: 直角三角形\n");
else
printf("特征: 普通三角形\n");
return 0;
}
解析
思路:先按距离公式把三对坐标化成三条边长,后面就回到上一题的做法。能不能构成三角形用"三点不共线"来判,特征按从特殊到一般的顺序分层判。
关键点:
- 三点共线的判据是叉积为零:
(x2-x1)*(y3-y1) - (y2-y1)*(x3-x1)。它只用加减乘,比"两边之和大于第三边"稳——三点本来就共线时,浮点误差很容易让边长比较的结果摇摆。 - 特征判定有先后:先等边(三边全等)、再等腰(有两边相等)、再直角、最后普通。等腰直角三角形两类都占,题干把四类并列,按这个顺序它会归到等腰那一类,不会两个都打。
- 直角判定要把三组勾股关系都试一遍,因为不知道哪条边是斜边。
- 浮点数不能用
==直接比。距离里有开方,等边的三条边可能差出最后几位,所以统一走fabs(a - b) < 1e-9这种带容差的比较。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
0 0 4 0 0 3(三边3、4、5)输出面积6.000000、特征直角三角形;输入0 0 4 0 2 3.464101615(三边4、4、4)输出面积6.928203、特征等边三角形;输入0 0 4 0 1 1输出面积2.000000、特征普通三角形;输入0 0 4 0 2 0输出三点共线,不能构成三角形。易错:
- 用
a == b判等腰,开方之后的浮点误差让本该判等的两条边判成不等- 判定顺序倒过来,等腰直角三角形的两个特征都打
- 直角判定只试
a*a + b*b == c*c一组,换个输入顺序就漏
11 · 星号直角三角形图案¶
输入正整数 n,输出由 n 行 n 列星号字符组成的三角形图案。右边是 n 等于 4 的图案。
答案
#include <stdio.h>
int main(void)
{
int n, i, j;
printf("请输入行数 n: ");
if (scanf("%d", &n) != 1 || n <= 0)
return 0;
for (i = 1; i <= n; i++) {
for (j = 1; j <= n - i; j++)
printf(" ");
for (j = 1; j <= i; j++)
printf(j == 1 ? "*" : " *");
printf("\n");
}
return 0;
}
n 等于 4 时的输出:
解析
思路:第 i 行由两段拼成——左边一段缩进,右边 i 个星号。星号之间隔一个空格,所以每往下一行,缩进就少 2 个空格。
关键点:
- 第
i行的缩进是2 * (n - i)个空格。乘2是因为星号之间隔了一个空格,一个星号在屏幕上占 2 列。 - 打星号那层用
j == 1 ? "*" : " *":第一个星号前面不加空格,后面的都带上自己的前导空格。一行打完正好是* * * *这个样子,行尾不留空白。 - 读入
n之后顺手判一下n <= 0。缩进量是拿n - i算的,n是0或负数时循环根本进不去,什么也打不出来。 - 题干样图里最上面几行的行首空格是图案的一部分,不是排版缩进,比对的时候不能压掉。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
4,输出的 4 行与题干样图逐行相同(按判题同款口径normalize()比较,行首空格保留、行尾空白剥掉);输入5得到 5 行,最上面一行是*,往下每行左移 2 格。易错:
- 缩进写成
n - i个空格,图案的斜率变成 1 个字符,三角形被压瘦一半- 每个星号后面都补一个空格,行尾多出空白
- 把题干样图的行首空格当成 markdown 的缩进压掉了,比对时被判成图案不同
12 · 空心三角形图案¶
输入正整数 n,输出 n 行 2×n-1 列的空心三角形图案。右边是 n 等于 4 的图案。
答案
#include <stdio.h>
int main(void)
{
int n, i, j;
printf("请输入行数 n: ");
if (scanf("%d", &n) != 1 || n <= 0)
return 0;
for (i = 1; i <= n; i++) {
for (j = 1; j <= 2 * (i - 1); j++)
putchar(' ');
if (i == 1) {
for (j = 1; j <= 2 * n - 1; j++)
printf(j == 1 ? "*" : " *");
} else if (i == n) {
putchar('*');
} else {
putchar('*');
for (j = 1; j <= 4 * (n - i) - 1; j++)
putchar(' ');
putchar('*');
}
putchar('\n');
}
return 0;
}
n 等于 4 时的输出:
解析
思路:题干说这个图案是 n 行、2n-1 列,而且星号之间还空一格,所以一个"星号位"实际占 2 列,一行最宽 2×(2n-1)-1 个字符。
第 i 行的左边界在第 2*(i-1) 列、右边界在第 2*(2n-i-1) 列,两条边每往下一行各收 2 列。
关键点:
- 左星号的位置直接就是
2 * (i - 1)列,也就是每行的缩进量——这和上一题一样。 - 右星号在第
2 * (2n - i - 1)列,减去左星号的位置再减去左星号本身占的 1 列,中间的空格数是4 * (n - i) - 1。四个待填的中间行分别是 7、3 个空格,正好对上样图。 - 顶边(
i = 1)是实心的,2n-1个星号之间照样隔一个空格;底边(i = n)只有一个星号;中间各行只有左右两个星号。 - 中间行打完右边那个星号就换行,不要为了凑宽度再补空格。行尾有没有空白,比对时是要剥掉的。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
4,输出的 4 行与题干样图逐行相同(按判题同款口径normalize()比较,行首空格保留、行尾空白剥掉);输入5得到 5 行,顶行 9 个星号,往下每行左右两侧各收 2 列,末行是缩进 8 格的一个星号。易错:
- 中间行的空格数按
2 * (n - i) - 1算(那是"星号位"的个数,每格 2 列,少算了一半)- 顶边把
2n-1个星号连着打,星号之间漏了空格- 底边的星号忘了缩进,跑到行首去了
- 中间行右侧的星号忘了打,只剩左边一条边
13 · 读程序写输出:差一错误¶
读程序,试指出程序的输出结果。
#include<stdio.h>
void main()
{ int k,m,j;
k=15;
for(m=2;m<=k;m++)
{ for(j=2;j*j<=m;j++)
if(m%j==0) break;
if(m%j!=0) printf("%4d",m);
}
printf("\n");
}
解析
这段程序想打出 2 到 15 之间的素数:外层 m 逐个走,内层用试除法找 m 的最小约数。判断"是素数"的那一句是 if(m%j!=0),而它判的并不是"内层有没有找到约数",是"退出内层时 m % j 是不是 0"。
差一错误就出在这里。
内层的 for(j=2;j*j<=m;j++) 有两种退出方式:
- 撞上
break:j就是找到的那个约数,此时m % j == 0。 - 循环条件不成立、自然退出:
j是第一个满足j*j > m的数。
作者想要的是第二种(自然退出就说明是素数)。写成 m % j != 0 恰好也能认出自然退出——自然退出时 j*j > m,而 m 是素数,j 取不到 m 这个值,所以 m % j 必然非 0。
这个巧合在 m = 2 时断掉了。逐轮把 m、退出时的 j 和 m % j 打出来看:
m |
退出内层时的 j |
j*j |
m % j |
是否打印 |
|---|---|---|---|---|
| 2 | 2 | 4 | 0 | 跳过 |
| 3 | 2 | 4 | 1 | 打印 |
| 4 | 2 | 4 | 0 | 跳过 |
| 5 | 3 | 9 | 2 | 打印 |
| 6 | 2 | 4 | 0 | 跳过 |
| 7 | 3 | 9 | 1 | 打印 |
| 8 | 2 | 4 | 0 | 跳过 |
| 9 | 3 | 9 | 0 | 跳过 |
| 10 | 2 | 4 | 0 | 跳过 |
| 11 | 4 | 16 | 3 | 打印 |
| 12 | 2 | 4 | 0 | 跳过 |
| 13 | 4 | 16 | 1 | 打印 |
| 14 | 2 | 4 | 0 | 跳过 |
| 15 | 3 | 9 | 0 | 跳过 |
m = 2 那一行就是问题所在:j 从 2 起步,循环条件 j*j <= m 是 4 <= 2,一开始就不成立,循环体一次都没执行,j 停在初值 2。而 j 这时正好等于 m,于是 2 % 2 == 0,if(m%j!=0) 判为假,2 这个素数被当成合数跳过了。
其余的素数(3、5、7、11、13)内层都能跑到 j*j > m 才退出,j 比 m 小,m % j 不为 0,正常打印。
合数全都在某个约数上 break 出来,退出时 m % j == 0,也都正确跳过。
正确的判据应该直接问"内层是不是自然退出的",也就是 j*j > m:自然退出意味着 2 到 √m 之间的数都试过了、一个约数都没找到。把 if(m%j!=0) 换成 if(j*j>m),输出就变成 2 3 5 7 11 13。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,输出就是
3 5 7 11 13,2不在里面。每个数占 4 列(%4d右对齐),一位数前面 3 个空格,两位数前面 2 个空格。
题干里的 void main()
题干写的是 void main()。标准从来只认 int main,gcc 15.2.0 在 -Wmain 下会给一条警告 warning: return type of 'main' is not 'int' [-Wmain](只是警告,不是硬错误)。核实输出时只做了两处最小修改:
void main()改成int main(void)- 末尾补一句
return 0;
其余一字未动,改完那份的输出与按原样编译的那份逐字节相同。
易错:
- 按"素数就是除 1 和自身外没有别的约数"心算,把
2也算进去,答成2 3 5 7 11 13- 以为
if(m%j!=0)判的是"内层没找到约数"- 忘了
%4d会占满 4 列,把答案写成3 5 7 11 13
14 · 用级数计算自然对数底 e¶
编写程序,按下面的公式计算自然对数底 e 的值。
e=1+1/1!+1/2!+1/3!+1/4!+⋯
答案
解析
思路:级数每一项都是前一项再除以一个递增的整数——1/i! = (1/(i-1)!) / i。照这个递推关系写,循环体里只有一句除法。
关键点:
- 不要每次重新算阶乘。
term = 1.0 / fact(i)这种写法既慢,fact(i)还会在i稍大时整型溢出。term /= i一步到位,而且每一项都是浮点数。 i是int、term是double,term /= i做的是浮点除法,不会像两个整数相除那样截断。- 累加从
e = 1.0起(级数的第一项就是常数1),term也从1.0起,这样第一轮循环算出来的正好是1/1!。 - 算多少项够?
1/20!已经是4×10⁻¹⁹量级,而double在2.718附近能分辨的精度只到约10⁻¹⁶,再多加几项也不会让结果动一位。20 项足够打印 10 位小数。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,输出
e = 2.7182818285。
别用 float 存
float 只有大约 7 位有效十进制数字,用它累加这个级数,打到第 8 位小数就开始是乱的。这类累加型的计算统一用 double。
易错:
- 每次循环重新求阶乘,
int到13!就溢出了- 累加从
0或从term起,把第一项常数1漏掉或算重- 用
float存结果,%.10f打出来的后几位不可信
15 · 用级数计算 e 的 x 次幂¶
编写程序,按下面的公式计算 eˣ 的值。
eˣ=1+x/1!+x²/2!+x³/3!+x⁴/4!+⋯
答案
解析
思路:和上一题同一个做法,只是每一项再多乘一个 x——第 i 项是 x^i / i!,也就是前一项乘 x 再除以 i。
关键点:
- 递推关系
term *= x / i。x / i里一边是double,做的是浮点除法。 - 上一题固定跑 20 次就够,这一题不行:
x越大收敛越慢,x = 20时前几项还在猛涨。所以改用"当前项的绝对值小于1e-12就收尾",同时把循环上限写成100,防止某项一直不衰减时死循环。 - 终止判据必须带
fabs。x是负数时各项正负交替,只看term < 1e-12会在第一个负数项上立刻判停,结果是错的。这一条容易漏。 - 打印时
x用%g,0.5打成0.5、2打成2,不会拖出一串没用的0;结果用%.10f。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
1输出e^1 = 2.7182818285;输入2输出e^2 = 7.3890560989;输入-1输出e^-1 = 0.3678794412;输入0.5输出e^0.5 = 1.6487212707。易错:
- 终止判据漏掉
fabs,x为负数时第一项就判停- 只在循环上限上写死次数,
x大时项数不够- 递推写成
term *= x / i之外还额外乘了x,变成x^(i+1)/i!
16 · 莱布尼茨级数计算圆周率¶
编写程序,用如下公式计算圆周率 π 的近似值。
π=4 - 4/3 + 4/5 - 4/7 + 4/9 - 4/11+⋯
回答程序要计算多少项才能得到数值 3.14、3.141、3.141 5、3.141 59。
答案
| 目标值 | 需要的项数 |
|---|---|
3.14 |
152 |
3.141 |
916 |
3.1415 |
7010 |
3.14159 |
130658 |
#include <stdio.h>
#include <math.h>
int main(void)
{
double target[4] = {3.14, 3.141, 3.1415, 3.14159};
int digits[4] = {2, 3, 4, 5};
int done[4] = {0};
double pi = 0.0, term, scale;
int n, i, k;
for (n = 1; n <= 200000; n++) {
term = 4.0 / (2 * n - 1);
pi += (n % 2 == 1) ? term : -term;
for (k = 0; k < 4; k++) {
if (done[k])
continue;
scale = 1.0;
for (i = 0; i < digits[k]; i++)
scale *= 10.0;
if (fabs(round(pi * scale) / scale - target[k]) < 1e-12) {
printf("达到 %.5f 需要 %d 项\n", target[k], n);
done[k] = 1;
}
}
}
return 0;
}
解析
思路:第 n 项是 4/(2n-1),符号正负交替;边累加边拿部分和去和四个目标值比,谁先对上就把项数记下来。done 数组用来标记已经命中的目标,避免后面每一轮重复打。
关键点:
- 第
n项的分母是2n-1(1、3、5、7……),奇数项加、偶数项减,写成(n % 2 == 1) ? term : -term。 - "得到数值
3.14"按打印两位小数的口径来,也就是把部分和四舍五入到 2 位小数后等于3.14。题干的3.141 5、3.141 59就是3.1415、3.14159,分别对应 4 位和 5 位小数。 - 这个级数收敛极慢:误差大致按
1/n衰减,所以要到3.14159就得算 13 万多项。不是程序写得慢,是这个公式本身慢。 - 循环上限写成
200000是因为已经知道最大那个目标要 13 万项;写小了会提前退出去,后一个目标永远打不出来。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,依次输出
达到 3.14000 需要 152 项、达到 3.14100 需要 916 项、达到 3.14150 需要 7010 项、达到 3.14159 需要 130658 项。
两种理解口径差得很远
题干没写清"得到数值 3.14"是打印两位小数(四舍五入)还是截断到两位。上面按 printf("%.2f") 的口径给,四个答案是 152、916、7010、130658;按截断口径算,则是 119、1688、10794、136121。
差这么多是因为部分和沿着 π 两侧来回摆:奇数项从上面靠近、偶数项从下面靠近。第 119 项的部分和是 3.149 995 87,擦着 3.15 的下沿落进了"截断成 3.14"的范围,却因为超过 3.145 而四舍五入成 3.15;一直要等到第 152 项,部分和 3.135 013 78 才第一次落进四舍五入的 3.14 区间。
两种口径都说得通,关键是程序里的判据和说法要对上。
易错:
- 用"部分和与
π的差小于某个ε"当判据,却把ε取成0.005、0.0005这种一半的值(那对应的是另一种口径)- 拿浮点数直接写
pi == 3.14,几乎永远不成立- 循环上限写得太小,后面几个目标根本轮不到
17 · 用循环改写 goto 代码¶
用结构化的代码改写以下代码:
答案
补全成一个能编译的程序:
解析
思路:题干这三行是拿 goto 手写的一个循环,逐句换掉就成结构化的代码。
loop:是循环的入口标号。if(x<y) goto next;是"条件不满足就跳出循环",对应while的条件要取反,写成x >= y。x -= y;在两个标号之间,是循环体。goto loop;是回到入口,这一句在改成while后自然消失。next:之后的printf是循环后面的第一条语句,照原样接在while后面,缩进退回到顶层。
关键点:
- 条件必须取反。
goto版的写法是"满足某条件就跳出去",while版要写成它的反面。 printf不能进循环体。它在next:标号之后,是"循环结束才执行一次",塞进循环会每轮打一行。- 题干的片段能看出
x是double(打的是%lf),y的类型从x >= y的比较和x -= y推断也是double,补全时两个都定义成double。 - 循环条件用的是
>=而不是>:原码在x == y时还会再减一次(x < y才跳出),x -= y之后x正好等于0。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):补全的程序送
-fsyntax-only通过、无警告;跑一遍,输入17.5 5输出x=2.500000(减 3 次:17.5→12.5→7.5→2.5),输入3 5输出x=3.000000(一开始3 >= 5就不成立,一次都没进循环)。易错:
- 条件忘了取反,写成
while (x < y)(那样循环体成了"不满足才减",逻辑整个反过来)- 把
printf也放进循环体- 只改掉
goto loop;,留下goto next;或那个跳出用的if
18 · 两种方法求 Fibonacci 数的差异¶
以下给出计算 Fibonacci 数列的两种不同方法:
(1)递推计算:f₀=0,f₁=1;fᵢ=fᵢ₋₁+fᵢ₋₂(i>1)。
(2)用公式计算:
fᵢ=(int)(cⁱ/sqrt(5)+0.5) /0.5 是补偿计算误差/
其中,c=(1.0+sqrt(5))/2.0。试编写程序找出以上两种计算方法求得的 fᵢ 不相等的 i。
答案
#include <stdio.h>
#include <math.h>
int main(void)
{
long f[93];
double c = (1.0 + sqrt(5)) / 2.0;
int i;
f[0] = 0;
f[1] = 1;
for (i = 2; i <= 92; i++)
f[i] = f[i - 1] + f[i - 2];
for (i = 0; i <= 92; i++) {
long g = (long)(pow(c, i) / sqrt(5) + 0.5);
if (g != f[i])
printf("i=%d: 递推=%ld 公式=%ld\n", i, f[i], g);
}
return 0;
}
i 从 71 开始两种方法不再相等,并且此后一路不相等。
解析
思路:递推法按定义一项一项加起来,结果是精确的整数;公式法用 double 做幂运算,只能算近似值。把两个序列逐项对一遍,第一个对不上位置就是答案。
关键点:
c = (1+√5)/2和sqrt(5)都是无理数,double只能存近似值。pow(c, i)每算一次,c的相对误差就被放大i倍,乘上c^i这个量级之后变成绝对误差。- 公式里的
+0.5是把"四舍五入"用截断实现的,它能吸收的误差上限就是0.5。误差一旦涨到1以上,这个补偿就完全失效。 i = 71时f₇₁ = 308061521170129,约3×10¹⁴。double在这个量级上相邻两个可表示数之间已经隔着0.03左右,再加上c本身的误差被放大了71倍,累计误差刚好越过1。- 数组开
93是因为 64 位long的上限是9223372036854775807,f₉₂ = 7540113804746346429还在范围内,f₉₃就超了。再往后递推法自己也在溢出,两边都没意义,比较到此为止。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,
i在0~70之间两种方法的结果完全一致;从i = 71起不相等(递推308061521170129、公式308061521170130),此后一路不相等,到92为止共 22 个。
71 这个数取决于浮点精度,不是数学结论
double 的位数由 IEEE 754 定死,所以 71 在常见平台上是稳定的。但它是精度的结果,不是 Fibonacci 数列的性质:把 c、sqrt、pow 换成 long double 版本(x86 上尾数 64 位),第一个不相等的 i 就推迟到 90。题目要的是"把不相等的找出来",程序写对就行,具体从哪一项开始取决于用的是什么浮点类型。
易错:
- 只比前几项就下结论(前 70 项全都相等)
- 公式里忘了
+0.5,从i = 3起就对不上(截断比四舍五入小1)- 数组开到几百,后面那些项两边都在溢出,比出来的差异没有意义
19 · 求平方不超过 x 的最大整数¶
编写输入正实数 x,求平方不超过 x 的最大整数 n,并输出。
答案
解析
思路:要求的 n 就是 √x 向下取整。不开方也行——从 0 开始,一个一个试下一个整数的平方有没有超过 x,没超过就把 n 往前推一格。
关键点:
- 判据
(n + 1) * (n + 1) <= x里,乘法是整数乘法,再把结果转成double和x比。整数不超过2⁵³时double能精确表示,所以这个比较不会踩浮点误差,"不超过"就是真的不超过。 - 直接用
(int)sqrt(x)要留神开方函数往哪边偏。sqrt的返回值是舍入过的,遇到完全平方数时可能比真值小一点点,(int)一截就少1。用循环试乘法就没这个问题。 - 题干说的是"正实数",
x不必是整数,也不必大于1:x = 2.25时答案是1,x = 0.5时答案是0(0 * 0 = 0 <= 0.5,但1 * 1 = 1 > 0.5)。 - 循环次数是
√x量级。x特别大时这个写法会慢,那时改用二分查找;本题的规模不必。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
10得n = 3;输入16得n = 4(完全平方数,答案正好是4);输入15.9得n = 3;输入2.25得n = 1;输入0.5得n = 0。易错:
- 写成
(int)sqrt(x),遇到完全平方数偶发少1- 比较用
< x而不是<= x,x = 16时会得3n定义成double(它要当整数用,%d打不出来)
20 · 回文整数的判断¶
回文整数是指正读和反读相同的整数,编写一个程序,输入一个整数,判断它是否是回文整数。
答案
解析
思路:把整数倒过来写一遍,再和原数比。倒写的过程和上一章逐位输出是同一套拆位动作,只是这次把取出的数字一位一位接到结果的末尾。
关键点:
- 拆位要用副本
m。直接在n上拆,循环结束时n已经变成0,就没东西可比了。 rev = rev * 10 + m % 10分三步看:m % 10取出最低位,rev * 10把已有结果整体左移一位,加号把它放到新的个位。- 负数的
m > 0一开始就不成立,rev保持0,会被判成不是回文。题干说"输入一个整数",通常按正整数理解;真要为负号补充处理,也得先想清楚-121算不算回文。 - 末尾是
0的数(比如120)翻转后是021,也就是21,不等于原数,会被正确判为不是回文。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
12321输出12321 是回文整数;输入12345输出12345 不是回文整数;输入1000000001输出1000000001 是回文整数。
翻转过程中会溢出
rev = rev * 10 + ... 每一步都在放大,int 最多装 10 位数。输入 1999999999 时翻转到最后一步要得到 9999999991,早就超出了 int 的上限 2147483647,这一步是有符号整型溢出(未定义行为)。这次运行最终打出"不是回文整数",结论碰巧是对的,但过程不能依赖。真要处理这么大的输入,把 rev 定义成 long long,或者翻到一半位数就停下来比较。
易错:
- 在
n上直接拆位,最后n变成 0,跟谁比都不相等(所有数都判成不是回文)- 翻转时写成
rev = rev * 10 + m / 10(取的是高位不是低位)- 用字符串逆序当回文判据,还得额外处理前导零和负号
21 · 1~256 的三种进制数值表¶
编写一个程序,输出 1~256 十进制数等价的二进制、八进制和十六进制数值表。
答案
解析
思路:一行一个十进制数,后面跟它的三种进制。八进制和十六进制直接交给 %o、%X;二进制 printf 没有对应的转换说明符,得自己一位一位拼出来。
关键点:
- 取二进制的某一位用"右移 + 按位与":
i >> k & 1把第k位挪到最低位,再用& 1把它单独留下来。k从15数到0,打出来就是从高位到低位。 putchar('0' + 该位)把数字0/1变成字符'0'/'1'。字符'0'的码值是48,加0还是'0',加1就是'1'。- 二进制统一按 16 位打(左边补
0)。表里最大的是256,二进制是 9 位,16 位的宽度留给它足够余量,各行左边缘也能对齐。只打有效位数也行,那样每行长短不一,表就散了。 %o是小写八进制、%x是小写十六进制,%X换成大写字母。上面用%X,所以254那一行打的是FE。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,输出共 257 行(表头 1 行加
1~256各一行)。第 1 行是1 0000000000000001 1 1,末尾三行是254 0000000011111110 376 FE、255 0000000011111111 377 FF、256 0000000100000000 400 100。
表格的首尾几行长这样:
十进制 二进制 八进制 十六进制
1 0000000000000001 1 1
2 0000000000000010 2 2
3 0000000000000011 3 3
...
254 0000000011111110 376 FE
255 0000000011111111 377 FF
256 0000000100000000 400 100
易错:
- 只用
printf,找不到打二进制的格式符,卡在那里(必须自己拼)- 取位写成
i >> k & 1之外的i & (1 << k)时忘了归一化,打出来是 0 或 256 而不是 0 或 1- 十进制那列用
%d而不用%6d,位数一变表就歪
22 · 猴子吃野果的倒推¶
草地上有一堆野果,有一只猴子每天去吃掉这堆野果的一半又一个,5 天后刚好吃完这堆野果。
求这堆野果原来共有多少个?猴子每天吃多少个野果?
答案
#include <stdio.h>
int main(void)
{
int x = 0, day, before[6];
for (day = 5; day >= 1; day--) {
x = 2 * (x + 1);
before[day] = x;
}
printf("原来共有 %d 个野果\n", before[1]);
for (day = 1; day <= 5; day++) {
int eaten = before[day] / 2 + 1;
printf("第 %d 天吃掉 %2d 个,剩 %2d 个\n", day, eaten, before[day] - eaten);
}
return 0;
}
原来共有 62 个;第 1 天到第 5 天依次吃掉 32、16、8、4、2 个。
解析
思路:正着算不知道起点是多少,倒着推更直接。第 5 天刚好吃完,那就是第 5 天开始时有 2 个;把"吃掉一半又一个"反过来,每天开始时的数量就是"当天剩下的数量加 1 再乘 2"。
关键点:
- 倒推公式的来历:设某天开始有
x个,吃掉x / 2 + 1个,剩下x - x / 2 - 1 = x / 2 - 1个。把剩下的记作r,就有x = 2 * (r + 1)。 - 倒推的起点是第 5 天结束时的
0个,往前推 5 步。把每天开始时的数量存进数组,正着打一遍每天的吃法就成了。 - "一半"在这里永远是整数:倒推出来的数量都是偶数(
2、6、14、30、62),正着吃的时候不会出现半个野果,整数除法不会丢东西。 - 正着验算一遍:
62吃32剩30;30吃16剩14;14吃8剩6;6吃4剩2;2吃2剩0。五天正好吃完。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,输出
原来共有 62 个野果,随后五行依次是第 1 天吃掉 32 个,剩 30 个、第 2 天吃掉 16 个,剩 14 个、第 3 天吃掉 8 个,剩 6 个、第 4 天吃掉 4 个,剩 2 个、第 5 天吃掉 2 个,剩 0 个。易错:
- 正着穷举起点,没想到倒推一步就够
- 倒推公式写成
x = 2 * r + 1(应该是2 * (r + 1),多加的那个1是当天吃掉的多余一个)- 只算出总数
62,忘了题干还问"猴子每天吃多少个"
23 · 质因子分解式¶
输入自然数 n(n>1),输出该数的质因子分解式。例如,n=30,程序输出
30=2*3*5
答案
#include <stdio.h>
int main(void)
{
int n, m, p, first = 1;
printf("请输入自然数 n(n>1): ");
if (scanf("%d", &n) != 1 || n <= 1)
return 0;
printf("%d=", n);
m = n;
for (p = 2; p * p <= m; p++)
while (m % p == 0) {
printf(first ? "%d" : "*%d", p);
first = 0;
m /= p;
}
if (m > 1)
printf(first ? "%d" : "*%d", m);
printf("\n");
return 0;
}
解析
思路:拿 p 从 2 开始逐个试除。试到一个能整除的 p 就反复除,直到除不动为止,再换下一个 p。这样找到的因子天然都是质数——合数因子早就被它的小质因子除干净了。
关键点:
- 内层必须是
while不是if。360 = 2×2×2×3×3×5里有三个2,只除一次的话剩下的45再也没机会被2整除,最后会打出360=2*45这种没分解完的式子。 - 试除的上界跟着
m走:条件是p * p <= m。每除掉一个因子m就变小,能省掉大量无用的试除。 - 循环结束后若
m > 1,剩下这个m就是最后一个质因子。它可能大于原数的平方根,试除循环够不到它,必须单独补一句打出来——97这类素数就是靠这一句打出97=97的。 - 星号要放在因子之间,第一个因子前面不打。用
first标志控制,就不会出现30=*2*3*5。 - 打等号左边用的是原数
n,不是循环里一直在变的m。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入
30输出30=2*3*5;输入360输出360=2*2*2*3*3*5;输入1024输出1024=2*2*2*2*2*2*2*2*2*2;输入97和999983(都是素数)分别输出97=97、999983=999983。易错:
- 内层用
if,重复的质因子只打一次- 忘了循环结束后补打
m,素数的输出只剩下一个97=- 星号一律打在因子前面,输出成了
30=*2*3*5- 等号左边写成
m,那已经是除完的1了
24 · 二分法求方程的根¶
已知自变量 x 在区间 [0,3] 上,函数 f(x)=x³-x²-1 有一个实根,试用二分法求该函数的根。
提示:二分法求根方法是一个非常实用的求根方法,二分法求根的基本思想是,设连续函数 f(x) 在区间 [low,high] 上有定义,且 f(low) 与 f(high) 的值的符号不同。
循环做以下的工作,测试区间中点 m(m=(low+high)/2)的值 f(m),并用 f(m) 值的符号与 f(low) 和 f(high) 的符号比较,如果 f(m) 的符号与 f(low) 的符号相同,则用 m 更新 low,否则用 m 更新 high。
循环直至 | high-low | 小于指定的精度要求。
答案
#include <stdio.h>
static double f(double x)
{
return x * x * x - x * x - 1;
}
int main(void)
{
double low = 0.0, high = 3.0, mid, eps = 1e-6;
if (f(low) * f(high) >= 0) {
printf("区间端点同号,二分法不适用\n");
return 0;
}
while (high - low >= eps) {
mid = (low + high) / 2;
if (f(mid) * f(low) > 0)
low = mid;
else
high = mid;
}
printf("方程的根 ≈ %.6f\n", (low + high) / 2);
return 0;
}
解析
思路:就按题干给的算法写。区间 [low, high] 两端函数值异号,根一定落在里面;每轮取中点,看中点和哪个端点同号,就把那个端点搬过来,区间每次缩短一半。
关键点:
- 先验端点异号。
f(0) = -1、f(3) = 27 - 9 - 1 = 17,乘积为负,可以用二分。这一句顺手也把"区间给错了"挡在外面。 - 更新规则和题干提示一一对应:
f(mid)与f(low)同号(乘积大于 0)说明两端同号的那一半不含根,根在[mid, high]里,于是low = mid;否则根在[low, mid]里,high = mid。 - 判同号用乘积的符号,不要用
f(mid) * f(low) > 0之外的花哨写法。乘积写法很直白,本题的数值范围也不会溢出。 - 终止条件是
high - low < eps。区间每轮减半,从初始长度3缩到1e-6大约要 22 轮。 - 最后取
(low + high) / 2当结果。区间两端到根的距离都不超过eps,取中点误差最小。
真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):程序原样编译运行,输出
方程的根 ≈ 1.465572,迭代了 22 轮。把精度换成1e-12再跑,迭代 42 轮,得1.465571231877。
精度不是越高越准
double 只有约 16 位有效数字,精度一路收紧到 1e-15 以下时,low 和 high 的差值已经落在浮点分辨率以下,high - low 可能永远降不到 eps,循环就停不下来。本题取 1e-6 到 1e-12 都合适。
易错:
- 更新方向搞反(
f(mid)与f(low)同号时去改high),区间会朝着没有根的那一半收缩,结果是错的- 判同号时把
f(low)写成了f(high)- 循环条件写成
high - low > 0,浮点数永远等不到相等,死循环- 忘了
f(x)里的x * x * x,把x³写成x * 3
习题自测登记:完成本章习题后在此登记