很多刚接触Python的朋友,都会在练习册上碰到这一组题:用for循环打印直角三角形、九九乘法表、金字塔。我第一次看到时也不以为然,觉得不就是print几个星号嘛。真正动手写才发现,每一行要输出几个字符、什么时候换行、怎么对齐,全是循环边界和字符串拼接的细节。这套题的核心价值,不在于画图本身,而在于让你把“重复动作”拆成“行”和“列”两层去理解,为后续处理列表、矩阵、文本排版这类问题打基础。这篇文章按我自己当年练习的顺序来写,适合零基础、刚学完if和for的初学者,也适合想给新手讲清楚嵌套循环的教学场景。
1. 先把问题拆成“行”和“列”
1.1 嵌套循环到底在干什么
很多人第一次写嵌套循环时,容易把两层for想得很玄。其实它和现实中的排队很像:外层循环负责“换下一行”,内层循环负责“把当前这一行从头到尾填充完”。打印一排星号,相当于在一面墙上从左往右贴星星;打印多排星号,是先贴完一排,再回到最左边贴下一排。
用代码表达就是:
for i in range(1, 6): # 外层循环:控制行数 for j in range(i): # 内层循环:控制这一行打印几个 print('*', end='') print() # 一行结束,换行内层的range(i)决定了“这一行打几个字符”,外层的range(1, 6)决定了“一共打几行”。end=''是让每次print不要自动换行,这样多个星号才能连成一排;后面那个单独的print()才是真正换行的地方。如果漏掉最后的print(),所有星号会全部挤在一行里。
1.2 每一行其实都有一个规律
这三道图形题,表面是画图,本质是找“行号 i 和目标图案的对应关系”。直角三角形第 i 行有 i 个星号,金字塔第 i 行有 2*i-1 个星号,九九乘法表第 i 行有 i 个等式。只要把每行的数量规律找出来,内层循环的范围就有了。
以 n=5 的直角三角形为例:
- 第1行:打1个星号
- 第2行:打2个星号
- 第3行:打3个星号
- 第4行:打4个星号
- 第5行:打5个星号
所以内层循环次数就是range(i)。这里的i如果从1开始,range(i)正好产生0到i-1,长度为i。如果从0开始,就是range(i+1)。这个“差一位”很容易把人绕进去,我建议记住一个习惯:写成for i in range(1, n+1),内层直接range(i),思路最顺。
1.3 统一的三层结构
不管打印哪个图案,只要涉及多行、每行多个字符,结构基本都是三层:
for i in range(起点, 终点, 步长): # 第1步:打印行首空格(金字塔、右对齐三角需要) # 第2步:打印本行内容(星号或公式) # 第3步:print() 换行这个模板可以覆盖后面所有的变体。先考虑“空格数”,再考虑“内容数”,最后补一个换行。很多人写金字塔前总觉得难,其实拆成这3步就不难了。
2. 直角三角形:从固定行数到逐行变化
2.1 标准左下版本
最常见的直角三角形是左下角对齐,也就是星号从少到多,左边缘对齐。代码可以这样写:
n = 5 for i in range(1, n + 1): for j in range(i): print('*', end='') print()输出:
* ** *** **** *****我第一次写时,把内层写成range(i+1),结果第一行就打了2个星号。原因是用range(i+1)时,i=1 会生成 [0,1] 两个值,星号数比行数多1。所以这里一定要让内层范围和行号保持一致:第 i 行就循环 i 次。最稳妥的验证方法,是把 n 改成 2 和 3 各跑一次,看看每行星号数量是否等于行号。
2.2 四个方向的变体
“直角三角形”其实有四个朝向:左下、左上、右下、右上。很多人不知道,只要调换外层循环的起点/终点,以及是否打印空格,就能自由切换。
| 变体 | 外层循环 | 每行空格数 | 每行星号数 |
|---|---|---|---|
| 左下 | 从1到n | 0 | i |
| 右上 | 从1到n | n-i | i |
| 左上 | 从n到1 | 0 | i |
| 右上(另一种) | 从n到1 | n-i | i |
这里我把“右下”和“右上”的表头稍微区分一下,方便对应代码。最常用的两个变体是右下和左上:
右下三角形:星号仍然从少到多,但右边对齐,所以每行要先打印空格,再打印星号。
n = 5 for i in range(1, n + 1): for k in range(n - i): print(' ', end='') for j in range(i): print('*', end='') print()左上三角形:星号从多到少,左边缘对齐。外层循环改成range(n, 0, -1),内层仍然range(i):
n = 5 for i in range(n, 0, -1): for j in range(i): print('*', end='') print()如果你把左下、左上、右下、右上这四个都亲手写一遍,对range的步长和“先空格后内容”的理解会一下子提升不少。
2.3 实操细节:别小看print的end参数
很多人在这里踩的第一个坑,就是忘记end=''。默认情况下,print每打印一个字符就换行,结果输出被拉成一条竖直的星号列,完全不是三角形。第二个坑是缩进混乱。
Python用缩进区分代码块,内层循环里的print必须比外层for多缩进,换行用的print必须和外层for同级。如果缩进错了,程序要么报错,要么逻辑全乱。我习惯一律用4个空格缩进,并且不要混用Tab和空格。
第三个坑是星号和星号之间有没有空格。如果你写的是print('* ', end=''),输出会变成每个星号后面带一个空格,图形看着更大,但和标准直角三角形不同。题目要求的是连续星号,所以用'*'就好。
3. 九九乘法表:在嵌套循环里做格式化
3.1 从逐行公式推导循环结构
九九乘法表的本质,是把“第 i 行、第 j 列”的乘法公式打印出来。行数从1到9,列数从1到当前行号i。也就是说内层循环范围是range(1, i+1),外层是range(1, 10)。
先写一版最基础的:
for i in range(1, 10): for j in range(1, i + 1): print(f'{j}*{i}={i*j}', end=' ') print()输出会是这样:
1*1=1 1*2=2 2*2=4 1*3=3 2*3=6 3*3=9 ...注意公式里的写法:j在前,i在后,表示“较小的数乘较大的数”。如果写成f'{i}*{j}={i*j}',那么同一行会出现2*1=2、2*2=4这样的顺序,虽然结果没错,但不符合人们阅读乘法表的习惯。很多新手在这里忽略顺序,导致看起来乱。
3.2 对齐技巧:\t与格式化字符串的取舍
上面这个版本有个问题:当积是一位数或两位数时,公式长度不同,只用end=' '隔开,列会扭来扭去。要让九行对齐,核心是用制表符或指定宽度。
用end='\t'是最直白的办法:
for i in range(1, 10): for j in range(1, i + 1): print(f'{j}*{i}={i*j}', end='\t') print()这样每个公式后面跟一个制表位,终端解析时会把它们推到相对整齐的列。虽然不同终端宽度可能让效果略有差异,但对初学者来说足够。如果追求更稳定,可以给每个公式一个固定宽度,比如:<4表示左对齐、占4个字符宽度:
for i in range(1, 10): for j in range(1, i + 1): print(f'{j}*{i}={i*j:<4}', end='') print()这里的:<4是关键:把公式整体在4个字符宽度里左对齐。我看到很多人问为什么用{:<4}而不是{:>4},其实左对齐更符合乘法表的阅读习惯,每个公式从相同起点开始,积的位数变化不会导致错位。右对齐则会让数字个位对齐,风格不同。
3.3 上三角和完整矩形变体
九九乘法表不只有下三角这一种。把内层循环的终点固定成9,就能打印完整矩形:
for i in range(1, 10): for j in range(1, 10): print(f'{j}*{i}={i*j:<4}', end='') print()如果想打印上三角,也就是每一行从当前行号开始到9,可以这样:
for i in range(1, 10): for j in range(i, 10): print(f'{j}*{i}={i*j:<4}', end='') print()但这里有个细节:上三角通常希望右边缘对齐,也就是前面空出已经跳过的列。如果直接range(i, 10),每行开头直接是公式,整体是左对齐的上三角,不是居中的上三角。若想完全对齐,需要先打印空格占位。这个可以作为一个进阶练习,先打印空公式占位,再打印真实公式,效果更接近常见课本里的“右上三角”。
for i in range(1, 10): for k in range(1, i): print(' ', end='') # 每个空位占8个字符 for j in range(i, 10): print(f'{j}*{i}={i*j:<4}', end='') print()不过这种写法需要统一空位宽度和公式宽度,否则依然对不齐。不要被这层细节吓到,先能把标准下三角打印好,就掌握了核心思路。
4. 金字塔:空格和星号的数学关系
4.1 从半个金字塔到完整金字塔
金字塔比直角三角形多了“居中”这个要求。先看一个更简单的过渡:每行星号数按奇数递增,左对齐,像是半个金字塔:
n = 5 for i in range(1, n + 1): for j in range(2 * i - 1): print('*', end='') print()输出:
* *** ***** ******* *********这其实就是“金字塔”去掉左边空格后的样子。要让星号居中,必须让右边的每行都比上一行少一些:左边的空格数等于总行数减当前行号。总行数是 n,当前行号是 i,所以空格数是n - i。这是整个金字塔实现里我最想强调的公式,一旦想通,代码就水到渠成。
4.2 把规律翻译成代码
完整金字塔代码:
n = 5 for i in range(1, n + 1): for k in range(n - i): print(' ', end='') for j in range(2 * i - 1): print('*', end='') print()输出:
* *** ***** ******* *********用 n=5 来验证一下:
- 第1行:空格数 5-1=4,星号数 2*1-1=1
- 第2行:空格数 5-2=3,星号数 2*2-1=3
- 第3行:空格数 5-3=2,星号数 2*3-1=5
星号数比上一行多2,正好是奇数递增。空格逐渐减少1,整体看起来中心线对齐。很多教材会直接把空格循环放在星号循环前面,逻辑上就是“先往后推几格,再画星号”。
4.3 把星号换成数字或字母
理解了空格和星号的关系,金字塔就能炸出很多花样。比如打印数字金字塔,每行打印当前行号的数字:
n = 5 for i in range(1, n + 1): for k in range(n - i): print(' ', end='') for j in range(2 * i - 1): print(i, end='') print()输出:
1 222 33333 4444444 555555555如果想打印递增数字,比如第i行从1到i再倒回来,内层就要多一层判断或拼接字符串。这是金字塔练习的经典扩展,能让人更熟悉字符串和range的组合。初学者通常先把星号金字塔打出来,再试数字版,会有一种“原来如此”的通透感。
关于金字塔,我还有一个建议:不要只盯着星号。把星号换成字母“A”或者任意字符,代码结构完全一样,但会更容易看出空格是否算对。想要验证居中对齐,可以临时把空格改成.符号,比如print('.', end=''),输出会清晰地展示每一行前面有几个点。检查完后,再把点改回空格。
5. 常见问题与排查技巧:循环输出错乱怎么办
5.1 高频问题速查表
这些题目看起来简单,实际练习中我见过的问题五花八门。我把最典型的整理成一张表,方便随时对照。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 所有星号全在一行 | 缺少换行用的print() | 在内层循环结束后加一个独立print() |
| 每行星号比预期多1个 | range(i+1)误写成多一位 | 用range(i),并验证行号与数量一致 |
| 金字塔向左歪 | 没有打印空格,或空格数写错 | 空格数应为n - i,且放在星号循环之前 |
| 乘法表各列对不齐 | 只用end=' ',没有控制宽度 | 改用end='\t'或:<4格式化 |
| 内层循环用了全局变量j | 忘记定义或外层变量被覆盖 | 使用独立变量名,或在循环内初始化 |
| 外层循环从0开始,行号混乱 | 对range的区间理解不清 | 记住range(1, n+1)才是1到n |
这里有很多是“边界差一”问题,看起来是小错误,但会让整个图形错乱。最有效的排查办法,是拿一张纸把n=3时的期望输出写下来,然后对照程序的每一行输出。
5.2 打印中间变量:让循环过程显形
遇到循环结果和预期不符时,别光盯着最终输出。把循环变量打印出来,一步一个脚印地看它怎么走。
比如打印直角三角形时,临时改成:
for i in range(1, 6): for j in range(i): print(f'i={i}, j={j}', end=' | ') print()这样就能看到每一行有哪些j值,从而验证内层循环到底跑了几次。排查完再改回打印星号的版本。这个方法对任何嵌套循环都适用。
检查空格时,print(' ')肉眼看不清到底是一个空格还是两个空格,可以用repr():
print(repr(' ')) # 输出 ' '或者直接把空格换成点,比如:
for k in range(n - i): print('.', end='')这样输出就变成:
....* ...*** ..***** .******* *********数一下点的数量,马上就知道空格有没有算对。我至今还用这种“显形法”排查加宽类的排版问题。
5.3 练习时我会故意改参数
很多人写完一个正确版本就停下来了,我反而会劝你多“破坏”几次。怎么破坏?就是故意改边界、改步长、改打印内容,看会输出什么。
比如把金字塔的外层循环改成range(0, n),这个依赖行号公式的地方就会全乱;把乘法表里的i+1改成i,会发现最后一行少一列;把三角形的end=''去掉,会看到竖线变成竖列。这些错误见一次,印象就深一层,后面面试或考试时遇到类似题目就不会慌。
我自己带过不少新手,发现一个通病:只追求“代码能跑”,不回头想“为什么能跑”。图形题是少有的、能够直观看到代码效果的练习,特别适合用来体会循环执行顺序。建议多花时间在“改参数、看变化”上,这比抄十遍代码更有用。
5.4 把n设为变量,养成不写死数字的习惯
我在所有示例里都用n或range(1, 10)控制范围,而不直接写range(1, 6)。因为图案的行数一旦写死,想改成10行就要改好几处。用变量 n 统一管理,改一个地方就能控制全部。
尤其是金字塔,空格、星号、外层行数都用同一个 n,代码可读性会好很多:
n = 5 for i in range(1, n + 1): for _ in range(n - i): print(' ', end='') for _ in range(2 * i - 1): print('*', end='') print()循环变量用_表示“这里不关心具体值”,能让代码更清爽。这个写法在很多算法题里都很常见,建议提前习惯。
这几道打印题,我前前后后陪过不少朋友练习,自己也重写过很多次。坦白说,九九乘法表和金字塔的对齐,是最容易让新手卡住的地方。我后来总结了一个笨办法:遇到复杂图形,先用'.'把空格占位打出来,确认空格数没算错,再换成真实内容;遇到嵌套循环搞不清走向,就把循环变量打印出来看。这个习惯到现在写稍微复杂一点的列表处理时还在用。图形题本身不难,但它逼着你把循环、边界、格式化这些基础功练扎实,后面学列表推导式、二维矩阵遍历时,会非常自然。如果练完还有余力,试试把金字塔改成菱形,或者把乘法表改成右上三角版本,一通百通。