
“奇妙的比值”“T的倍数N”“三角形”——单看这三个题目你可能会以为这是一份数学练习卷但它们其实是入门编程课里非常经典的“循环”基础题编号分别是16th、17th、18th。三道题放在一起很有意思都要求用循环语句完成但每一道的循环用法都不一样。第16题要用循环做辗转相除求两个数的最大公约数然后推到最小公倍数和最简整数比第17题要用循环遍历一个区间把T的倍数筛出来第18题要用两层循环配合在终端里打印出三角形图案。换句话说这是对for、while、循环嵌套的一次组合式训练。如果你刚学完循环语法写单层for循环还磕磕绊绊或者看完答案能懂、合上书就写不出来那这篇内容应该能帮你把逻辑理顺。我会从算法思路开始拆给出可运行的Python和C两种代码再讲几个只有自己踩过坑才会知道的细节。题目不复杂但把这三道题吃透循环这一关基本就稳了。1. 三道题为什么都围绕“循环”展开1.1 循环的本质重复、变化、收集很多初学者把循环理解成“把一段代码重复执行几遍”这个理解不够完整。真正的循环题考的是三件事按规律重复、在重复中让某个量发生变化、在重复中收集结果。举个例子打印1到10的整数i每次都加1这是“变化”每打印一个数都在屏幕上留下输出这是“收集”整个过程重复10次这是“重复”。第16题的辗转相除每次循环都在算余数并交换两个数直到余数为0这也是一个标准的循环过程只是它的循环次数不固定完全由输入决定。所以判断一个场景能不能用循环解决就看它是否满足“同样的操作参数在变结果在积累”。凡是满足这三条的问题基本都能套上for或while。1.2 三道题的递进关系与考察点三道题是逐步加深的。第16题是条件型循环循环次数事先不知道靠循环条件决定何时停下来第17题是范围型循环遍历一个已知的区间在循环体里做条件筛选第18题是嵌套型循环外层管行、内层管列内外层还要配合好。我在下面这张表里把三题的考察点做了一个对照题号题目循环形态核心考点实际生活中的影子16th奇妙的比值while循环次数不固定辗转相除、取模、变量交换数据规约、求最大公约数17thT的倍数Nfor循环遍历范围取模判断、边界控制、计数累加数据筛选、批量任务处理18th三角形for嵌套循环行列建模、公式推导、输出格式表格遍历、图形输出、矩阵操作这个递进关系是刻意的。第16题考“循环到底什么时候结束”第17题考“循环体内部怎么根据条件取舍”第18题考“两层循环如何协同”三关闯完循环的三种典型用法基本都见过了。2. 第16题“奇妙的比值”用循环求最简整数比2.1 题目解析与算法思路“奇妙的比值”听起来抽象其实是求两个正整数的最大公约数和最小公倍数然后输出它们的最简整数比。比如输入12和18最大公约数是6最小公倍数是36最简整数比是2:3。为什么说比值“奇妙”因为任意两个整数只要同时除以它们的最大公约数得到的比例就和原来的比例完全等价但数字变小了这个性质在化简分数时天天用。题目用“循环”作为后缀意思是这个最大公约数必须用循环来实现而不是直接调用现成的数学函数。求最大公约数最经典的方法是欧几里得算法也叫辗转相除法。它的核心规则是gcd(m, n) gcd(n, m % n)一直算到余数为0最后一个非零余数就是最大公约数。这个算法用循环表达特别自然因为循环体里反复做“求余、赋值”两个动作。2.2 Python与C关键代码实现先看Python版本清晰而且贴近伪代码m int(input(请输入第一个数 m: )) n int(input(请输入第二个数 n: )) a, b m, n while b ! 0: r a % b a b b r g a l m // g * n print(最大公约数 g , g) print(最小公倍数 l , l) print(最简整数比{} : {}.format(m // g, n // g))这里a和b是工作变量不能直接修改m和n因为后面还要用原始值计算最小公倍数和比值。g a是循环结束时保存结果l m // g * n用的是最大公约数 × 最小公倍数 m × n这个性质。再看C语言版本很多学校的C语言课都会让把这道题写一遍#include stdio.h int main(void) { int m 0, n 0, a 0, b 0, r 0; scanf(%d%d, m, n); a m; b n; while (b ! 0) { r a % b; a b; b r; } printf(gcd %d\n, a); printf(lcm %d\n, m / a * n); printf(ratio %d:%d\n, m / a, n / a); return 0; }2.3 这个例题里的两个容易忽略的细节第一个细节是m * n / a和m / a * n的区别。数学上两式一样但在C语言里如果m和n比较大m * n可能先溢出结果就不对了。推荐先除后乘也就是m / a * n可以在多数情况下避免整数溢出。这是一个很实用的工程经验面试里考过很多次。第二个细节是输入的两个数大小顺序。欧几里得算法并不要求m n因为如果m n第一轮求余就会自动得到r m然后两个数交换位置。你可以试试输入18 12结果和12 18完全一致。这个特性是好心给了你写代码的方便但也容易被忽略导致有人专门写个判断去排序属于多余的步骤。3. 第17题“T的倍数N”用循环做区间筛选3.1 基础方案遍历1到N逐个判断这道题的要求是给定一个整数N和一个正整数T找出从1到N范围内所有T的倍数统计个数并求和。最直观的思路是从1循环到N对每一个数判断能否被T整除。判断的数学工具是取模运算i % T 0如果成立说明i是T的倍数。n int(input(请输入范围上界 N: )) t int(input(请输入倍数因子 T: )) count 0 total 0 for i in range(1, n 1): if i % t 0: print(i, end ) count 1 total i print() print(倍数的个数:, count) print(倍数的总和:, total)比如输入N20、T3程序输出3 6 9 12 15 18个数是6总和是63。整个过程一共循环20次每次只做一次取模和一次判断逻辑非常直白。这类题目是“遍历加筛选”的典型代表也是循环里最常见的模式。以后写代码时你会经常遇到“从一个集合里挑出符合某种条件的元素”的场景比如从日志里筛出报错行、从用户列表里找出活跃用户思路都是同一个循环遍历条件判断收集结果。3.2 步进方案循环次数更少如果你观察一下会发现T的倍数本身是有规律的它们构成一个等差数列T, 2T, 3T...。所以完全不用逐个遍历可以直接从T开始每次加T。n int(input(请输入范围上界 N: )) t int(input(请输入倍数因子 T: )) for i in range(t, n 1, t): print(i, end )这段代码里range(t, n 1, t)的第三个参数就是步长。当T很大时比如N100、T99第一种方案要循环100次第二种方案只循环1次效率差距非常明显。但要注意这两种方案在教学中各有价值。第一种方案把“倍数”这个数学判断和取模运算绑定在一起更有利于理解倍数的定义第二种方案更贴近数学本质也更高效。实际写代码时选哪种我的习惯是如果你只需要倍数本身用步进如果你还需要对非倍数做别的处理用取模判断。3.3 分隔符处理最后一个数不输出多余符号这个问题很不起眼但踩坑的人特别多。如果用print(i, end )这种方式输出的数字之间都会有空格但最后一个数字后面也会多出一个空格。在终端看不太出来但如果把输出结果当作数据传给别的地方这个多余的空格就可能造成问题。更常见的需求是用逗号分隔3, 6, 9, 12, 15, 18最后一个数后面不能有逗号。处理思路有两种一种是把结果先收集成列表再统一打印n int(input(N )) t int(input(T )) res [] for i in range(1, n 1): if i % t 0: res.append(i) for idx, v in enumerate(res): if idx len(res) - 1: print(v) else: print(v, end, )另一种思路是用一个布尔变量记住“是不是第一个”只有从第二个元素开始才在输出前加逗号。这种first标记法在C语言里非常常见int first 1; for (i 1; i n; i) { if (i % t 0) { if (!first) { printf(,); } printf(%d, i); first 0; } }这个技巧的通用性很强。前端做动态列表、脚本输出CSV、日志格式化都会遇到“最后一个元素别加分隔符”的问题。这个知识点虽然小但特别能体现工程感。4. 第18题“三角形”嵌套循环输出图形4.1 直角三角形从“第几行”推“几个星号”前面两道题都只用一层循环到了第18题开始上嵌套了。先看最简单的直角三角形输出效果是* ** *** **** *****观察规律第1行有1个星号第2行有2个第3行有3个。也就是说第i行的星号数量正好等于i。这就是外层循环和内层循环之间的关系外层循环变量负责控制行号内层循环的循环次数由外层变量决定。n 5 for i in range(1, n 1): for j in range(i): print(*, end) print()每一行内层循环结束后用一次print()换行。这个换行也是循环的一部分不能省略。C语言版本for (int i 1; i n; i) { for (int j 0; j i; j) { printf(*); } printf(\n); }4.2 金字塔三角形先推空格数再推星号数比直角三角形稍微难一层的是金字塔形等腰三角形。目标输出* *** ***** ******* *********这里的关键是每一行不仅有星号还有前置空格。想要让所有行的星号在中间对齐就要先输出空格再输出星号。先看规律。对于n5第1行4个空格1个星号第2行3个空格3个星号第3行2个空格5个星号所以第i行i从1开始的空格数是n - i星号数是2*i - 1。空格数为什么是n-i因为要保证最后一行没有前置空格而前面的行逐层缩进。星号数为什么是2*i-1因为每行比上一行多出两个星号首行1个对应的通项公式就是2*i-1。n 5 for i in range(1, n 1): for j in range(n - i): print( , end) for k in range(2 * i - 1): print(*, end) print()内层两个循环一个输出空格、一个输出星号顺序不能反。因为一旦先输出星号再输出空格图形就会整体错位。C语言版本for (int i 1; i n; i) { for (int j 0; j n - i; j) { printf( ); } for (int k 0; k 2 * i - 1; k) { printf(*); } printf(\n); }4.3 扩展图形倒三角与菱形三角形题目最常见的变体是倒三角形和菱形。倒三角形只需要把外层循环倒过来从n往下数到1n 5 for i in range(n, 0, -1): for j in range(n - i): print( , end) for k in range(2 * i - 1): print(*, end) print()菱形则是先输出上半部分的正金字塔再输出下半部分的倒金字塔去掉中间重复的一行。这类变体刷过一遍之后嵌套循环基本就通了。这里强调的关键不是记住某个图形的代码而是学会“先观察规律再写出公式最后翻译成循环”的解题流程。5. 循环题的常见问题与排查技巧实录5.1 边界错误少一个元素或多一个元素这是循环题最高频的问题。比如打印1到N你写的范围是for i in range(1, n)输出就少了n或者C语言里写成i n同样会少最后一项。反过来有时候又会多循环一次。排查思路很简单取最小的边界值N1做手工推演。让代码在脑子里“跑一遍”i从1开始满足条件吗循环结束后i的值是多少如果输出结果和预期不符那多半就是边界条件写错了。记住这个口诀含端点就用和range(1, n1)不含端点就用和range(1, n)。5.2 死循环循环变量“卡住”了死循环最常见的原因是循环体里没有更新循环变量。比如i 1 while i n: print(i) # 忘了 i 1这种错误看起来低级但写成复杂业务逻辑后非常隐蔽。排查时可以看循环体最后几行确认有没有修改条件里出现的变量。死循环不止出现在入门题里。比如硬件开发中ESP8266等设备用AT指令做串口通信时代码里等待设备返回“OK”字符串如果循环里没有设置超时上限设备状态异常就会一直等下去表现就是“卡死”。这和基础题里的死循环本质一样只是换到了真实场景。我的经验是所有等待型的while循环都要加一个“最大尝试次数”或“超时”出口这是工程级代码的底线要求。5.3 输出格式多空格、少换行、分隔符多余打印循环题经常栽在格式上。常见的三种问题第一多了一个前置空格。如果内层循环输出空格时多算了一个图形就会整体偏移。第二换行不对内层循环结束时忘了打印print()所有星号就会挤在一行。第三分隔符问题我在第3.3节里讲过尾部多出逗号或空格用first标记法就能解决。有一个通用的调试技巧在循环体里临时打印出循环变量比如print(i, i, j, j)跑一遍就能看到变量是怎么变化的。用完再删掉就行这种“打印调试法”比干瞪眼强十倍。5.4 从三道基础题看循环在现实场景中的影子这三道题虽然简单但循环的思想会在后续很多地方出现。链表遍历时访问节点后要移动到下一个节点典型的循环结构while (p ! NULL) { p p-next; }。如果是循环单链表就把结束条件换成p ! head。这和辗转相除法里的while循环非常像循环次数都由条件决定。循环队列中元素的移动常用公式rear (rear 1) % capacity。这里的取模运算和判断“T的倍数”用的i % t 0是同一个操作只是用途不同。语言层面也一样。shell脚本里的for循环写起来可能是for i in $(seq 1 5); do mkdir dir$i; doneJavaScript里提前结束循环用break这些语法细节不同但循环的逻辑骨架完全一致。理解了循环的本质换个语言不过是换个关键字。循环神经网络RNN则是把同一个结构在时间轴上重复执行多次每一层接收前一层的输出作为当前输入这和“在循环中反复使用同一个处理逻辑”的心智模型也是相通的。基础题里练的循环往上可以一直抽象到模型设计。我自己带学生时有个习惯这三道题做完一定要让他们把第16题的while循环改成for循环、把第17题的for循环改成while循环、把第18题的金字塔改成菱形。同样的逻辑用两种循环各写一遍很多理解不到位的地方就暴露出来了。代码不光是写出来能跑还要能解释清楚每一步为什么这么做这才算真正过关。