news 2026/10/17 12:17:29

循环三题拆解:素数判断、计算e与数字之和的边界与调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
循环三题拆解:素数判断、计算e与数字之和的边界与调试

如果你正在学循环,你大概率会在题库里撞见这三道题:素数判断、计算自然常数e、求各位数字之和,对应编号分别是第31题、第32题、第33题。它们排在一起不是巧合,而是把一个新手最容易迷糊的循环难点拆成了三个阶段:条件遍历、级数累加、数位剥离。别看这三道题单独拿出来都很基础,不少人写出的代码“能跑”,但边界一测就露馅;还有人把循环写成了死循环,或者一换成大一点的输入直接溢出。

这篇文章我就按这三道题的顺序,逐个拆解它们的循环结构,讲清楚每一步为什么这么写,再把我这么多年调试循环代码踩过的坑一并列出来。适合刚学完for和while、但面对题目还是有点发懵的同学,也适合准备期末考试想快速系统过一遍循环题的老读者。内容以C语言为例,但思路对Python、Java同样适用。

1. 三道题凑在一起,恰好覆盖循环的三种典型“变数”

很多人学循环时都会背“三要素”:初始化、条件、更新。但真到做题,卡住的地方往往是同一个问题——循环体里到底该写什么?为什么同样都是循环,素数题里变量一路加一,数字之和里变量一路变小,而计算e的题里变量几乎一直在被自己除?

1.1 循环不是在重复同一段代码

循环表面上是“把一段代码重复执行很多遍”,但真正有价值的循环,每一轮执行时数据都在变化。比如for (i = 2; i < n; i++),这段代码每一轮测试的i不同,所以判断结果也可能不同。循环体里写的不是“固定动作”,而是“对当前这个值做动作”。

理解这一点之后,三道题的本质就露出来了:

  • 素数题:重复的动作是“测试n能否被i整除”,其中i从2一路递增到某个边界。
  • 计算e题:重复的动作是“算出当前这一项,并把它累加到总和里”,其中项的值在不断变小。
  • 数字之和题:重复的动作是“取出当前数字的最后一位,累加起来,然后把数字缩小”,其中n本身在逐轮被改写。

这么一拆,你就能看出三道题分别对应三种不同的“变数”:递增的整数变量、递推的浮点项、不断缩减的输入值。把“什么在变”找清楚,循环条件基本就写对了一半。

1.2 循环体里真正变化的量,才是解题钥匙

我见过不少学员拿到题目就急着敲代码,结果写完一运行,要么死循环,要么结果差一位。他们的问题不是语法不熟,而是没先回答一个问题:每一轮循环结束之后,哪个变量的值会变?怎么变?

拿数字之和来说,最容易想到的写法是:

while (n > 0) { sum += n % 10; n = n / 10; }

这里的“变”体现在n身上:从1234变成123,再变成12、1,最后变成0。如果你写的时候忘记n = n / 10,循环就永远不会结束。这不是笑话,是我真的见过无数次的错误。

所以我的建议是:拿到任何一道循环题,先不要写代码,用一句话回答“每一轮我到底要对什么做操作,做完之后哪些值会变”。这三道题正好是三种完全不同的答案,做完它们,你对循环的理解会立体很多。

2. 素数(循环)第31题:试除边界的数学依据与三个隐蔽坑

素数题是循环入门里公认的“第一道坎”。题目本身一句话:输入一个整数,判断它是不是素数。但代码写出来,有人十几行,有人三十几行,效率差好几倍,还都有各自的坑。

2.1 素数的定义与第一版试除代码

素数定义是“大于1的自然数,除了1和它本身之外,不能被其他数整除”。翻译成循环,最直接的想法就是:从2开始,一直试到n-1,看有没有谁能整除n。

int isPrime(int n) { if (n < 2) { return 0; } int i; for (i = 2; i < n; i++) { if (n % i == 0) { return 0; } } return 1; }

这个版本逻辑没错,但对大数来说太慢了。比如判断1000000以内的素数,最坏情况要做将近一百万次取模运算。虽然基础题不要求极致性能,但从一开始就养成优化边界的习惯,后面写算法会省很多事。

2.2 为什么边界可以砍到sqrt(n)

如果你判断n是不是素数,其实不需要试到n-1。假设n = a × b,并且a和b都大于sqrt(n),那么a × b一定大于n,矛盾。所以一旦n是合数,它的因子里至少有一个不超过sqrt(n)。

换句话说:只要在2到sqrt(n)之间找不到因子,n就是素数。于是循环条件可以改成:

int isPrime(int n) { if (n < 2) { return 0; } int i; for (i = 2; i * i <= n; i++) { if (n % i == 0) { return 0; } } return 1; }

从i < n改成i * i <= n,时间一下子从O(n)降到O(sqrt(n))。这也是我认为基础题里最值得反复体会的一个优化点:它不是靠什么玄学技巧,而是用一条简单的数学性质,直接砍掉了一大半无效计算。

2.3 三个隐蔽坑:flag重置、i*i溢出、边界临界值

先看一个看起来很合理的错误写法:

int flag = 1; for (i = 2; i < n; i++) { if (n % i == 0) { flag = 0; } } return flag;

如果这段代码只调用一次,问题不大。但如果你在一个循环里反复调用isPrime,flag可能还残留上一次的结果,导致判断全部错乱。正确做法是每次进入函数都重新初始化,或者像我前面的代码一样,直接用return提前退出,根本不用flag。

第二个坑是i * i <= n的溢出问题。int类型最大能表示约21亿,当i超过46340时,i * i就超过21亿了,会变成负数,循环条件立刻出错。如果你用这个写法,又恰好有人输入了一个接近int上限的大数,程序可能误判或死循环。稳妥方案有两种:一是用long long类型保存乘法结果,二是先算好int limit = (int)sqrt(n);再循环,但要记得#include <math.h>。

第三个坑是边界临界值。n = 2时,i * i <= 2不成立,循环一次都不执行,直接返回1,这是对的。n = 1或负数时,如果忘了开头的if (n < 2)检查,程序会返回1,把1也当成素数,这就是经典的边界错误。我建议每个初学者都把n = 2, 3, 4, 9, 97这几个值当成“素数题固定测试集”。

2.4 一个能直接跑的完整示例

我把判断和调用放在一起,方便你直接复现:

#include <stdio.h> int isPrime(int n) { if (n < 2) { return 0; } int i; for (i = 2; i * i <= n; i++) { if (n % i == 0) { return 0; } } return 1; } int main() { int n; printf("请输入一个整数(输入0退出):"); while (scanf("%d", &n) == 1 && n != 0) { if (isPrime(n)) { printf("%d 是素数\n", n); } else { printf("%d 不是素数\n", n); } printf("请输入一个整数(输入0退出):"); } return 0; }

运行效果:输入7输出“7 是素数”,输入9输出“9 不是素数”。如果你想把这道题扩展一下,可以改成输出2到100之间的所有素数,注意每输出10个换一行,这又是一个锻炼循环和计数器的小练习。

3. 计算e(循环)第32题:级数求和如何用递推躲开阶乘溢出

计算自然常数e也是一道经典循环题。题目通常有两种问法:一种是输入项数n,计算前n项的和;另一种是不给项数,要求一直累加到某项小于某个精度为止。无论哪种,核心都是同一个级数公式。

3.1 e的级数定义与最笨的实现

自然常数e的定义是:

e = 1 + 1/1! + 1/2! + 1/3! + ... + 1/n! + ...

其中“!”是阶乘,0!约定为1。最容易想到的实现是每轮重新算一次阶乘:

double e = 0.0; for (int k = 0; k < n; k++) { double fact = 1.0; for (int j = 2; j <= k; j++) { fact *= j; } e += 1.0 / fact; }

这个写法有两个明显问题。第一,内层循环重复计算阶乘,每轮都要从头乘一遍,整体复杂度是O(n^2)。第二,阶乘增长极快,13!就已经超过62亿,int根本放不下,算到后面阶乘溢出成负数,累加结果全乱。

3.2 用t_k = t_{k-1} / k 一行递推

观察相邻两项的关系。令第k项为 t_k = 1/k!,那么 t_k = t_{k-1} / k。也就是说,你根本不需要每轮重新算阶乘,只要把上一轮的结果除以k,就得到了这一轮的结果。

代码可以写成:

double e = 1.0; // 第0项:1/0! = 1 double term = 1.0; // 上一项的值,初始是第0项 int k = 1; while (k <= n) { term = term / k; e = e + term; k++; }

验证一下:k=1时,term = 1/1 = 1,e = 1 + 1 = 2,对应前2项;k=2时,term = 1/2 = 0.5,e = 2 + 0.5 = 2.5,对应前3项。完全正确。

这个递推式的好处是:每次只做一次除法和一次加法,没有重复劳动;同时term的数值一直在变小,永远不会溢出。你想算多少项都行,只受double精度限制。

3.3 固定项数还是精度控制

如果老师要求“计算前n项”,上面用while(k <= n)就好。如果要求“计算e,直到新增项小于0.000001”,则改一下循环条件:

double e = 1.0; double term = 1.0; int k = 1; while (term > 1e-12) { term = term / k; e = e + term; k++; } printf("%.12f\n", e);

这里有几个容易搞错的细节。第一,term的初始值是第0项1.0,所以在进入循环前,e已经包含了第0项。第二,循环内的顺序必须是先更新term再累加,如果写反了,结果会差一项。第三,精度阈值不要设得太小,double大约只有15到16位有效数字,设成1e-15以下基本是无效循环,白算很多轮。

另外,如果你直接用printf("%f", e),输出默认只有6位小数,你只能看到2.718282,既看不出精度,也容易让人误以为计算错了。建议至少输出8到10位小数,例如printf("%.10f\n", e);。

3.4 顺序错误与其他浮点坑

我曾经见过一个学员把代码写成:

e += term; term = term / k; k++;

结果输出2.5之后怎么都不涨。原因就是第一轮累加了第0项,第二轮累加的却还是第0项的值,等于每一项都被推迟了一轮。虽然这个错误不致命,但会让人困惑很久。调试方法很简单:在循环体里临时加一句printf("k=%d, term=%.10f, e=%.10f\n", k, term, e);,立刻就能看出顺序对不对。

浮点计算还有一个经典陷阱:如果把k定义成int,term / k会自动转成double,没有问题;但如果有人写成1 / k,那就是整数除法,结果全是0。我建议在写级数题时,养成把“上一项”放到double变量里的习惯,比如last_term /= k的形式,既清楚又安全。

4. 数字之和(循环)第33题:拆位循环的本质是“缩减规模”

第三道题看起来比前两道简单:输入一个整数,把它的每一位数字加起来,比如1234输出10。但真正写起来,它考察的是你能不能想到“取个位,再去个位”这两个动作。

4.1 从“取个位、去个位”两个动作开始

对于一个十进制正整数n,n % 10能得到它的个位数字,n / 10则能把个位去掉。比如n=1234:

  • 第一轮:n % 10 = 4,累加4,然后n = 1234 / 10 = 123。
  • 第二轮:n % 10 = 3,累加3,然后n = 123 / 10 = 12。
  • 第三轮:n % 10 = 2,累加2,然后n = 12 / 10 = 1。
  • 第四轮:n % 10 = 1,累加1,然后n = 1 / 10 = 0。

当n变成0,循环结束。完整代码:

#include <stdio.h> int digitSum(int n) { int sum = 0; if (n < 0) { n = -n; } while (n != 0) { sum += n % 10; n = n / 10; } return sum; } int main() { int n; printf("请输入一个整数:"); scanf("%d", &n); printf("各位数字之和 = %d\n", digitSum(n)); return 0; }

4.2 为什么这个场景默认用while而不是for

很多人会问:能不能用for写?能,for (; n != 0; n /= 10)也能跑。但从可读性角度,while更自然,因为循环次数不由一个固定的计数变量决定,而是取决于n这个输入值本身会被“削减”成什么样。你要处理的不是一个从1数到100的计数器,而是一个不断缩小的数据源。

这也解释了为什么这题经常和“进制转换”绑定在一起出现。你把n % 10改成n % 2,把n / 10改成n / 2,它就变成了统计二进制中1的个数,原理一模一样。可以说这一题的循环结构是后续很多位运算和数位DP问题的最小原型。

4.3 负数、零和末尾带0的数字

题目如果只说“整数”,没有说“正整数”,那负数就要小心。C语言里负数取模结果是负数,比如-123 % 10是-3,直接累加会得到负数结果。简单解法是开头判断一次,if (n < 0) n = -n;。如果你不想改n本身,也可以每次累加abs(n % 10),但那样每轮都要调用一次绝对值函数,不如统一处理一次来得干净。

还有一个容易被忽略的边界是末尾带0的数字。比如输入1000,循环过程是:取出0,加0;变成100;取出0,加0;变成10;取出0,加0;变成1;取出1,加1;变成0。最终结果是1,正确的。因为加0不影响总和,所以这类数字往往不容易测出问题,但你能在纸上推演一遍,说明你真正理解了循环的每一步变化。

如果输入是0,while循环一次都不执行,sum保持0,输出0,这也是对的。所以这道题的边界测试集建议为:0、1、1234、1000、-123、99999。

4.4 一个常见变体:各位数字的乘积

练习循环的人很快会遇到类似的题:把各位数字改成“求积”。思路几乎一样,但初始值要从0变成1。如果写成sum初始为0然后累乘,结果永远是0;而初始为1,最后返回1,又会在n=0时产生歧义。这类小变体最适合用来检验你是不是真的懂了“累加器和累乘器初始化值的差异”,建议自己动手改一遍。

5. 三道题的调试串讲:从手工推演到边界清单

做题和写bug是两回事。我见过太多人代码写完了,运行结果不对,就开始盲目改循环条件,改来改去更乱。这里分享一套我个人最常用的循环调试流程,基础题阶段足够用了。

5.1 手工推演:在纸上列出每一轮的变化

遇到循环结果不对,先别急着上IDE,拿一张纸,把每一轮的循环变量变化写下来。以计算e为例,我让你写一个推演表:

k本轮term累加后e
11.02.0
20.52.5
30.166666...2.666666...
40.041666...2.708333...

只要执行三到四轮,你马上就能发现是顺序错了、初始值错了,还是循环条件错了。这个方法在素数题和数字之和题里同样有效,只是表格内容换成i和n%i,以及n和sum。

5.2 临时printf大法:在循环体里窥探变量

如果你觉得手工推演太慢,程序里埋点输出是最高效的调试方式。比如素数判断,你在循环体里写一句:

printf("i=%d, n%%i=%d\n", i, n % i);

运行之后,你能直接看到i和余数变化的全过程,哪一轮误判一目了然。注意一点:调试完记得把这些printf删掉,或者用//注释掉。有些初学者调对了之后就忘了删,结果提交作业时带着一堆调试输出,格式分直接扣没了。

5.3 教科书上不写但你必须会的边界测试清单

循环代码最常见的错误,几乎全部藏在边界值里。我把三道题的边界测试列成一张表,建议你每道题写完都过一遍:

题目测试输入期望输出能暴露什么问题
素数1不是素数漏掉n<2的判断
素数2是素数循环边界写错导致特判失败
素数4不是素数能否正确退出循环
素数97是素数边界优化是否有效
计算en=12.0漏掉第0项
计算en=22.5循环顺序是否正确
数字之和00while条件写错
数字之和123410基础拆位逻辑
数字之和10001末尾0是否处理正确
数字之和-1236负数处理

不要觉得这些输入“太简单”。我工作这些年,真正线上出问题的往往就是这类边界场景。能在基础题阶段养成测边界的习惯,后面写业务代码会少挨很多骂。

5.4 常见编译错误的快速自查

如果代码编译不过,先看这几类问题:math.h有没有加;循环条件里是用=还是==;循环体里有没有漏掉更新语句;变量是不是定义在使用之前;是不是在中文输入法状态下敲了全角分号或括号。尤其是全角符号,报错信息经常错位,新手容易排查很久。

6. 做完这三道题,值得继续做的三个方向

三道基础题不是终点。如果做完就扔,过两周基本忘光;如果能把它们串起来或者做点扩展,循环这一块就算是真正吃透了。我给几个亲测有效的方向。

6.1 组合题:求区间内所有“素数的数字之和”

把第31题和第33题组合:输入范围[m, n],求这个范围内每个素数的各位数字之和的总和。代码无非是先调用isPrime判断,再调用digitSum累加:

#include <stdio.h> int isPrime(int n) { if (n < 2) return 0; for (int i = 2; i * i <= n; i++) { if (n % i == 0) return 0; } return 1; } int digitSum(int n) { if (n < 0) n = -n; int sum = 0; while (n != 0) { sum += n % 10; n /= 10; } return sum; } int main() { int m, n, total = 0; scanf("%d %d", &m, &n); for (int i = m; i <= n; i++) { if (isPrime(i)) { total += digitSum(i); } } printf("%d\n", total); return 0; }

这道组合题能让你体会到“函数拆解”的好处:判断和拆位各干各的,主函数读起来像一句话。这也是我推荐初学者尽早养成写小函数的习惯的原因。

6.2 从朴素质数判断升级到埃氏筛

第31题只判断一个数,但如果要判断2到100000之间所有素数,逐个试除的效率就很一般了。埃氏筛的思想很简单:先把所有数都当成素数,然后从2开始,把每个素数的倍数全部标记为合数。代码只要十几行就能筛出很大范围,这也是很多后续算法题的原型。学有余力的人可以把它作为第31题的“进阶篇”来写。

6.3 把级数求e的套路迁移到其他数学常数

计算e的递推思路换到π上完全通用,比如π的莱布尼茨级数,或者sin(x)的泰勒展开,都是“上一项乘以一个分式得到下一项”的结构。你甚至可以把计算e的代码里term = term / k;改成其他递推式,然后观察收敛速度的变化。这样一道题就变成了一类题。

说句实在话,这三道题我当年也做了不止一遍。第一遍照着书上的答案抄,第二遍合上书自己写,第三遍是过了一周又回来重写,目的就是检验自己是不是真明白。现在回头想,真正让我受益的不是记住了“素数怎么判断”“e怎么算”,而是养成了拿到循环题就先问一句“每轮什么东西在变”的思维习惯。做项目debug到深夜的时候,最感谢的反而是这些基础题打下的底子。

最后分享一个小技巧:循环变量命名尽可能含义明确。i、k、n这种单字母不是不能用,但你心里要清楚它此刻代表谁。如果你发现一个循环里同时出现好几个含义不清的变量,先停下来给它改个名字,往往bug自己就现形了。祝刷题顺利。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 12:17:28

浮点数精度揭密:混合精度训练如何避免LLM崩溃

本来只是想复盘一次训练事故&#xff0c;结果越查越觉得&#xff0c;LLM 训练里的浮点数问题才是很多“莫名其妙”的崩溃背后真正的元凶。某开发者跑一个 1.5B 参数的语言模型&#xff0c;前 3000 步一切正常&#xff0c;之后 loss 每隔几百步就冲上 NaN&#xff0c;训练吞吐也…

作者头像 李华
网站建设 2026/10/17 12:17:19

批处理数据分区策略调优指南:从40分钟到6分钟的进阶之路

1. 分区这件事&#xff0c;为什么值得重新认真对待 先讲一个我自己踩过的场景。当时在某数据团队负责一个每天凌晨跑批的统计任务&#xff0c;处理的是近八千万行订单明细数据。某天告警群里弹消息&#xff1a;任务执行时间超过 40 分钟的阈值&#xff0c;眼看要耽误下游报表出…

作者头像 李华
网站建设 2026/10/17 12:16:10

从超级提示词到多Agent特工团队:任务拆解与结构化交接实战

从“一个超级提示词搞定所有事”到“一支特工团队分工协作”&#xff0c;这个转变我最近体会特别深。以前写复杂需求&#xff0c;我总是把角色、背景、格式要求、约束条件全部塞进一段提示词里&#xff0c;结果经常是前500字写得清楚&#xff0c;后500字就开始走样。直到我换了…

作者头像 李华
网站建设 2026/10/17 12:16:03

Model-Based RL实战:建模、规划与误差治理全链路

1. 这堂课讲的不是“用模型代替试错”&#xff0c;而是“如何让试错本身变得可计算” “Model-Based Reinforcement Learning”——当这个词第一次出现在某高校强化学习系列讲座的第四讲标题里时&#xff0c;我注意到不少听众在课前讨论区发问&#xff1a;“是不是有了模型就不…

作者头像 李华
网站建设 2026/10/17 12:16:02

上下文工程:Agent 表现不稳的根源与系统化实战方法

我见过不少团队&#xff0c;优化 Prompt 优化到怀疑人生&#xff1a;指令写得越来越长、越来越绕&#xff0c;例子上了一堆&#xff0c;few-shot 都塞到顶&#xff0c;模型该犯错还是犯错。尤其是一上 Agent&#xff0c;问题更明显——系统明明接了知识库、接了工具、接了多轮对…

作者头像 李华
网站建设 2026/10/17 12:15:13

Git入门本质:理解快照、三棵树与分布式设计

1. 这不是“学Git”&#xff0c;而是重建你和代码的关系很多人点开“GitHub Git版本管理工具入门”这个标题&#xff0c;第一反应是&#xff1a;“哦&#xff0c;又一个教命令行的教程。”然后顺手收藏&#xff0c;再也没打开过。我试过不下二十种入门路径——从官方文档啃到某…

作者头像 李华