
如果你正在学循环你大概率会在题库里撞见这三道题素数判断、计算自然常数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;如果这段代码只调用一次问题不大。但如果你在一个循环里反复调用isPrimeflag可能还残留上一次的结果导致判断全部错乱。正确做法是每次进入函数都重新初始化或者像我前面的代码一样直接用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; }验证一下k1时term 1/1 1e 1 1 2对应前2项k2时term 1/2 0.5e 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定义成intterm / k会自动转成double没有问题但如果有人写成1 / k那就是整数除法结果全是0。我建议在写级数题时养成把“上一项”放到double变量里的习惯比如last_term / k的形式既清楚又安全。4. 数字之和循环第33题拆位循环的本质是“缩减规模”第三道题看起来比前两道简单输入一个整数把它的每一位数字加起来比如1234输出10。但真正写起来它考察的是你能不能想到“取个位再去个位”这两个动作。4.1 从“取个位、去个位”两个动作开始对于一个十进制正整数nn % 10能得到它的个位数字n / 10则能把个位去掉。比如n1234第一轮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不影响总和所以这类数字往往不容易测出问题但你能在纸上推演一遍说明你真正理解了循环的每一步变化。如果输入是0while循环一次都不执行sum保持0输出0这也是对的。所以这道题的边界测试集建议为0、1、1234、1000、-123、99999。4.4 一个常见变体各位数字的乘积练习循环的人很快会遇到类似的题把各位数字改成“求积”。思路几乎一样但初始值要从0变成1。如果写成sum初始为0然后累乘结果永远是0而初始为1最后返回1又会在n0时产生歧义。这类小变体最适合用来检验你是不是真的懂了“累加器和累乘器初始化值的差异”建议自己动手改一遍。5. 三道题的调试串讲从手工推演到边界清单做题和写bug是两回事。我见过太多人代码写完了运行结果不对就开始盲目改循环条件改来改去更乱。这里分享一套我个人最常用的循环调试流程基础题阶段足够用了。5.1 手工推演在纸上列出每一轮的变化遇到循环结果不对先别急着上IDE拿一张纸把每一轮的循环变量变化写下来。以计算e为例我让你写一个推演表k本轮term累加后e11.02.020.52.530.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不是素数漏掉n2的判断素数2是素数循环边界写错导致特判失败素数4不是素数能否正确退出循环素数97是素数边界优化是否有效计算en12.0漏掉第0项计算en22.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自己就现形了。祝刷题顺利。