ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

循环结构三大经典场景:素数判断、计算e、数字拆分实战解析

循环结构三大经典场景:素数判断、计算e、数字拆分实战解析 如果你正在刷一套编号整齐的循环基础题恰好做到“素数循环-基础题31th 计算e循环-基础题32th 数字之和循环-基础题33th”这一组我建议你停下来把这三道题当成一个整体来看。它们虽然都是循环结构但对应的是循环最常见的三种应用场景穷举试探、累加逼近、迭代拆分。理解了这三副面孔循环这一关基本就稳了。我每年带新人练循环都会让他们把这几个题连着做完。不是为了刷数量而是因为它们恰好覆盖了循环从“会写”到“写好”的关键跨越什么时候用 for、什么时候用 while、哪些边界值会坑人、中间结果怎么查。这篇文章就围绕这31th、32th、33th三道题把我实际讲解时用的思路和踩过的坑完整整理出来适合刚学完循环语法、正在刷基础题找感觉的读者。1. 三个题放在一起看循环的三种典型场景1.1 循环解决问题本质上只有三种姿势很多新手做循环题是一个个孤立地去背套路看到素数就想到“取余”看到求和就想到“累加器”看到数字拆位就想到“整除”。背多了会发现题目稍微改一下就懵因为没看透循环背后的共性。循环解决实际问题其实只有三种最原始的姿势第一种叫穷举试探。也就是我不知道答案具体是多少但我知道答案藏在一个范围里一个个试过去总能找到。素数判断就是这样一个数 n 是不是素数得看它能不能被 2 到 n-1优化后是 2 到 sqrt(n)之间的某个整数整除。这个“逐个检查”的动作天然就是循环。第二种叫累加逼近。我需要计算一个不断累加才能得到的结果每一项由前一项递推而来。计算自然常数 e 的级数展开就是典型e 等于一系列 1 除以阶乘之和每一项都有规律循环一次加一项慢慢逼近真实值。第三种叫迭代拆分。输入的数据本身是一个整体比如一个多位数我需要把它拆成一个个小单元来处理。数字之和就是不断取个位、去掉个位直到把数字拆完。循环在这里是“处理一个单元 缩小问题规模”的重复过程。这三种姿势没有哪个更高级但它们的循环写法、终止条件、变量设计都不一样。把三道题放一起练就是为了让你能清楚区分它们。1.2 刷这组题的正确姿势先想后写再改我遇到过不少同学看到题目马上打开编辑器敲代码敲完发现不对又从头看一遍来回折腾好几轮。其实刷这种基础题效率最高的顺序是三步走先写伪代码再填正确语法最后主动试边界。伪代码不需要规范只要能帮你想清楚“循环从哪里开始、到哪里停、每次变什么”就行。比如素数题你在纸上写“从2开始一直试到n的平方根如果中间有任意一个数能整除n就说明不是素数”这就是循环逻辑。伪代码过关了再用 Python 或 C 一句句翻译成代码基本不会跑偏。三个题都做完之后还要改一改。把素数的范围从 n 改成 n/2 试试把计算 e 的迭代次数从 10 改成 100 看看精度怎么变化把数字之和从十进制改成二进制想想怎么做。改的过程才是真正理解的过程。这也是我为什么把这组题叫“基础题”因为它们短小但可塑性极强。2. 素数循环-基础题31th试除法的尺度与边界2.1 素数的定义直接翻译成代码就是试除法什么叫素数教科书上的定义是大于 1 的自然数中只能被 1 和它本身整除的数。这个定义翻译成代码最直接的做法就是“试除”——从 2 开始挨个去除 n只要有任何一次余数为 0就说明除了 1 和 n 之外还有别的因子那 n 就是合数。用 Python 写最基础的版本是这样def is_prime(n): if n 2: return False for i in range(2, n): if n % i 0: return False return True这个版本没错但循环到 n 显然做了很多无效劳动。比如判断 101 是不是素数试到 2、3、5……实际上在试到 sqrt(101)约等于 10的时候因子有没有就已经能确认了。试到 100 纯属多跑了一圈。很多初学者会问多跑一圈也无所谓啊反正数字小感觉不出来。确实刷题时 n 小可能无所谓但素数判断是要被其他算法反复调用的每次多啃一点性能积累起来就是灾难。而且理解“循环到哪就够了”这件事本身就是循环题的核心素养。2.2 为什么试到 sqrt(n) 就足够这可不是性能优化的小技巧要理解 sqrt(n) 这个界限得从因子的成对性说起。如果 n 能被某个整数 a 整除商是 b那么 a * b na 和 b 是成对出现的因子。比如 36 的因子对不对1 和 36、2 和 18、3 和 12、4 和 9、6 和 6。仔细观察这些成对因子每一对里总有一个较小、一个较大相等时就是平方根那一对。较小的那个必然小于等于 sqrt(n)。所以只要 2 到 sqrt(n) 这个范围里没有因子那大于 sqrt(n) 的范围里也必然没有因子。你不需要把 36 试到 35试到 6 就够了。这个推理很多人上课听了就忘做题时还是习惯写range(2, n)。我建议你把它彻底想明白因为它不只用在素数题里。将来做因子分解、判断完全平方数、优化枚举类题目都会用上“对称区间只查一半”的思路。循环的次数从 n-2 次降到 sqrt(n)-2 次这是从 O(n) 到 O(sqrt(n)) 的跨越在算法题里属于质的区别。正确的优化版本是这样def is_prime(n): if n 2: return False i 2 while i * i n: if n % i 0: return False i 1 return True这里我故意用了 while 写法因为很多人在 for 里写range(2, int(n**0.5) 1)时会因为浮点数转整数的舍入问题感到困惑。用i * i n直接避免开根号的精度损失。实测下来这个写法在边界值上更稳。2.3 从素数题里带走的两个通用习惯第一素数题要单独处理 n 2 的情况。1 不是素数0 和负数更不是。如果不做这个前置判断很多代码会在 n 1 或 n 0 时给出错误结果。这属于边界值意识刷基础题最该养成的就是这个。第二写好循环后一定要手动过三个测试用例最小的素数 2、典型的合数 4、边界上的完全平方数 9。我遇到过不少人写的代码n 2 时直接在循环里被2 % 2 0判成合数就是因为忘了 2 本身是素数循环根本不应该执行。这种问题只有跑边界用例才看得出来。素数这道题还有一个很实用的变体判断 n 是不是素数同时输出 n 的所有因子。做法就是在循环里把余数为 0 的 i 都收集起来。你会发现调换一下判断条件和输出时机同一段循环就能完成完全不同的任务。这就是基础题的价值——它看起来简单但每个点都能往外长出新的东西。3. 计算e循环-基础题32th级数求和里的累乘器与浮点账3.1 自然常数e的级数形式怎么变成一个循环计算自然常数 e 通常用它的级数定义e 1 1/1! 1/2! 1/3! ...一共加到第 N 项。每一项的分母是阶乘分子恒为 1。用循环累加时正儿八经的做法是设置一个总和变量 e然后循环 N 次每次算出一项加进去。新手第一反应通常是每次循环里调用现成的阶乘函数比如先算出 fact i!再累加1 / fact。这样写没问题但效率很差因为阶乘计算本身是个循环套在里面就是循环嵌套做了大量重复计算。计算 1/5! 时4! 已经在算上一项时算过了重新算一遍纯属浪费。更优雅的方案是让每一项依赖前一项。用递推式表达就是第 i 项 第 i-1 项 ÷ i。因为在级数里1/(i1)! 和 1/i! 之间只差一个因子 1/(i1)。这样整个循环就是线性的不需要嵌套。3.2 一个累乘器变量把阶乘计算省到底我给学生推荐的写法是用一个 term 变量存储当前项每轮循环先累加到 e 上再更新 termdef calc_e(n): e 0.0 term 1.0 # 第一项是 1/0!也就是 1 for i in range(1, n 1): e term term / i return e这里 term 就相当于一个累乘器。第一轮循环e 加上了 1即 1/0!然后 term 变成 1/1。第二轮e 加上 1/1即 1/1!term 变成 1/2。第三轮e 加上 1/2即 1/2!……每一步 term 都正好是当前需要的下一项。循环 N 次e 就是 1/0! 加到 1/(N-1)! 的和。如果你写成fact * i; e 1 / fact其实也行但要注意那个 fact 变量从 1 开始乘循环要从 1 开始。两种写法没有对错之分但我更推荐 term 写法因为它把“这一项怎么计算”的关注点从阶乘本身转移到了项与项的递推关系上这才是级数求和的精髓。将来学泰勒展开、牛顿迭代你会反复看到这种“维护一个递推项”的套路。3.3 浮点数精度循环次数不是越多越准计算 e 的循环次数设置多少合适很多同学觉得 100 次、1000 次越多越好实测下来发现到后面根本停不下来。原因是阶乘增长极其迅猛1/10! 已经小于 0.000000271/20! 大约是 4.1e-19早就低于双精度浮点数能表示的精度范围。在 Python 里用 float双精度计算一般循环到 20 到 30 项e 就已经收敛到 2.718281828459045 附近再往后加的都是比浮点精度还小的数字加上去等于没加。我用 Python 实测n 20 和 n 100 的输出在小数点后十几位完全一致。这里顺带提一下浮点数的经典坑1 / 3在二进制浮点数里是一个无限循环小数存进 float 以后会有舍入误差。计算 e 的时候每一项也都是不精确的但误差很小累加之后仍然能保证前十几位的精度。如果你用 C 语言的 float 类型而不是 double精度会明显下降迭代到 15 项左右就开始不稳定。所以做科学计算类的循环题优先选 double别在精度上抠门。还有一个细节不同语言的整数除法规则不一样。如果你用 C 系列语言1 / fact在 fact 是整数时会直接得到 0必须写成1.0 / fact才能触发浮点除法。这个坑很隐蔽我见过不少人在 C 语言里算 e 算出来 1找半天找不到原因。Python 3 的/就没有这个问题但如果是 Python 2同样会中招。4. 数字之和循环-基础题33th整除与取模的拆解搭档4.1 数字拆分的核心操作就靠 % 和 //数字之和这个题输入是一个整数比如 12345要输出 15各位数字相加。这里的难点在于“怎么把一个整数拆成单个数字”。核心操作只有两个取模% 10拿到个位整除// 10去掉个位。两者配合就能从右往左逐个取出每一位。def digit_sum(n): total 0 while n 0: total n % 10 n // 10 return total拿 12345 手动走一遍循环先12345 % 10得 5加上去再把 n 变成 1234下一轮得 4再下一轮得 3……直到 n 变成 0循环结束total 就是 15。这个流程里最关键的是n // 10这一句。它既是“缩小问题规模”的操作也是让循环最终能退出的条件更新。我见过很多新手把取模和整除写反或者忘记写整除导致循环永远在同一个 n 上打转变成死循环。这种问题特别基础但出错的频率高得吓人。4.2 while循环和for循环这道题给出了答案前面素数题我用的是 for或等效的 while计算 e 我用的是 for数字之和我偏偏用了 while。这不是随手写的而是因为它们循环的判断条件不同。for 循环适合“确定循环次数”的场景。素数题我知道从 2 试到 sqrt(n) 一共多少个候选数计算 e 我知道迭代 N 次。但数字之和不同我不确定一个数有几位只能判断“n 还没被拆完”这个状态而这种“不知道次数、只知结束条件”的情况正是 while 循环的主场。一句话总结循环次数固定用 for循环结束条件复杂、次数不固定用 while。这个判断标准虽然朴素但覆盖了绝大多数基础场景。4.3 把基础题改出花来二进制、负数、逆序输出数字之和这道题做完我强烈建议你立刻做几个变形因为变形能验证你是真懂还是背代码。第一个变形把十进制改成二进制求二进制下各位之和。做法只需要把% 10和// 10改成% 2和// 2其他逻辑原封不动。你会发现循环的骨架完全没变变的只是模数和除数。这就是“数字拆分”模型的通用性。第二个变形输入可能有负数怎么办在 Python 里-123 % 10的结果是 7不是 3这和数学里的取模定义有关。如果你题目保证输入非负那不需要处理如果不保证最好先取绝对值abs(n)再拆。我平时会加一句n abs(n)省得后面被边界用例打脸。第三个变形要求逆序输出这个数的每一位。思路一模一样只是把累加换成输出。你会发现这些看起来花样百出的题拆穿了都是同一段%和//的循环。这就是基础题反复练的意义——你见多了以后遇到任何一个“处理数字的题”脑子第一反应就是“先拆位”。5. 循环综合练习三个题暴露出的共性问题与排查清单5.1 死循环的根源更新语句放错位置三道题里最容易引发死循环的是数字之和原因就是上面说的忘记写n // 10或者把整除写到了%的前面导致每一轮处理的 n 根本没变。判断一个循环会不会死最有效的办法是问自己每循环一次循环条件里用到的那些变量值到底变没变如果没变那这个循环就永远出不来。排查死循环时我的做法是先不要瞎猜直接在循环第一行打印主要变量的当前值。比如数字之和里打印 n 和 total一眼就能看出 n 是不是在变小。如果 n 一直卡在同一个值问题基本就锁定在更新语句上。这类错误属于逻辑层编译器不会报警运行时会一直卡住所以只能靠“观察变量变化”来定位。5.2 边界值最容易翻车三个题各有各的坑素数题里n 2 是素数但循环体写错会判成合数n 1 不是素数忘了前置判断会判成素数。计算 e 里i 0 或 n 0 时有些写法会出现除零错误有些写法会把第一项漏掉。数字之和里n 0 时 while 循环一次都不进total 返回 0这个结果对吗按题意通常是对的但你要是想用 for 循环配合字符串转换来写就得考虑 0 的特殊处理。处理边界值有个笨办法但很可靠把 n 取 2、1、0、负数、以及刚好是某个数的平方这三种情况各测一遍。测试用例不用多但每个都要有目的性。像素数题里 9 这种完全平方数就是专门拿来检验循环边界会不会多算一位的。5.3 肉眼调试法中间结果打印比断点更快入门很多新手到这一步还不会用调试器没关系用 print 打中间结果就足够了。计算 e 的时候我习惯把每一轮的 i、term、e 都打出来看它们是否符合递推关系。如果发现 term 从某一轮开始不再变化大概率是浮点数精度到头了而不是代码写错。这个习惯要一直保留到以后写更复杂的循环。我见过不少人在循环题里遇到“结果差一点点”的问题比如 e 算出来是 2.7182818284590459 而不是期望的 2.718281828459045就开始怀疑算法错了。实际上这只是浮点数表示误差打印出每一项你就知道循环逻辑本身完全没问题。有时候打印不是为了找错误而是为了确认“对在哪里”。5.4 做完一组题之后怎么复盘才有含金量三道题都做完之后我建议你腾出十分钟画一张表把它们的循环要素列出来循环变量是什么、初始值是多少、终止条件是什么、每轮怎么更新、循环体里发生了什么。画完这张表你再看这三道题会发现它们本质上是同一个骨架换了几层皮。以我自己带练的经验来说这组题的正确打开方式是素数的试除范围要学会用 sqrt 收紧计算 e 的递推项要养成“用前一项算后一项”的习惯数字之和的%与//组合要形成肌肉记忆。这三个点看着散其实都是循环设计里的核心基本功——一个是“循环区间怎么定义”一个是“循环体内部怎么递推”一个是“数据怎么在循环中迭代变形”。最后分享一个我的小习惯每做完一道基础题我都会把它改成至少两个变体再收工。素数改成输出因子e 改成计算某个函数在某点的近似值数字之和改成二进制版本。基础题本身不是终点它们是用来安装“循环思维”这套脚手架的。等脚手架搭稳了后面遇到排序、查找、递归那些更难的东西你至少不用再为“循环条件为什么写错”这种问题卡壳。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进