1. 从一道上机题看Python分支与循环的真实门槛
很多人拿到《Python程序设计与算法基础教程(第二版)微课版》第三章的上机实践题,第一反应是"这不就是if-else和for循环吗,能有多难"。我带过几届刚入门的学生,也帮不少转行的朋友看过代码,发现一个很普遍的现象:语法都认识,一上机就卡壳。第三章恰恰是Python从"能看懂"到"能写出来"的分水岭——分支结构、循环结构、循环嵌套、break与continue的控制流,这些概念单独拎出来都不复杂,但一旦组合成上机题,尤其是涉及边界条件、累加累乘、图形打印、素数判断这类经典题型,问题就集中爆发了。
这篇文章不打算给你一份干巴巴的答案清单,那种东西网上到处都是,抄完就忘。我想做的是把第三章上机实践里最典型的几类题目拆开,讲清楚每道题背后的设计意图、常见错误、调试思路,以及我在实际教学和代码review中总结出来的经验。你如果是正在学这门课的学生,可以把它当成一份"解题思路+避坑手册";如果你是自学者或者需要辅导别人的人,这里面的分析框架同样适用。核心关键词就几个:分支结构、循环控制、循环嵌套、边界条件、算法基础,围绕它们展开。
第三章的上机题通常有这么几个特征:题目描述简短,但隐藏条件多;看起来能跑通,但测试用例一多就露馅;很多题有"数学味",比如求最大公约数、判断素数、打印乘法表、计算数列和。这些题不是随便出的,它们对应着算法思维的基本功。我下面按题型分类来讲,每一类都会给出完整代码、逐行解释、常见错误对照,以及我踩过的坑。
2. 分支结构上机题:为什么你的if-else总是漏掉一种情况
2.1 分段函数与多条件判断的典型陷阱
第三章分支部分最常见的上机题是分段函数求值,比如根据输入的x值,按照不同区间计算y。题目本身不难,但错误率高得惊人。我统计过一批学生的提交,超过六成的人第一次写出来的代码在边界点上出错。
问题出在哪?出在区间开闭的判断上。举个例子,题目要求:x<0时y=-1,x=0时y=0,x>0时y=1。很多人写成:
x = float(input("请输入x: ")) if x < 0: y = -1 elif x > 0: y = 1 else: y = 0 print(y)这个写法是对的,因为else兜住了x=0的情况。但换成更复杂的分段,比如x<0、0<=x<10、10<=x<20、x>=20四段,很多人就会写成:
if x < 0: y = 1 elif x < 10: y = 2 elif x < 20: y = 3 elif x >= 20: y = 4这个逻辑其实没问题,因为elif是顺序判断的,前面的条件不满足才会走到后面。但有人会画蛇添足写成elif 0 <= x < 10,然后最后一个写成else,结果x=20的时候因为前面的条件都用了严格小于,反而漏掉了。我的建议是:分段函数要么用"累进式"写法(只写上界,靠顺序兜底),要么把每个区间的开闭都写清楚,但绝不要混用。
提示:Python支持链式比较
0 <= x < 10,这是很多语言没有的特性,用好了能让分段逻辑非常清晰,但要注意不要和elif的顺序判断逻辑冲突。
2.2 闰年判断:一个被讲烂但依然有人错的经典
闰年判断几乎是每本教材分支章节的保留题目。规则是:能被4整除但不能被100整除,或者能被400整除。写成代码:
year = int(input("请输入年份: ")) if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): print(f"{year}是闰年") else: print(f"{year}不是闰年")看起来简单,但我见过有人写成year % 4 == 0 and year % 100 != 0 or year % 400 == 0,然后纠结运算符优先级。Python里and优先级高于or,所以这个写法其实和加括号的结果一样,但我强烈建议加括号,因为可读性远比省几个字符重要。另外,运算符优先级这种东西,不同语言规则不同,写代码时依赖记忆去判断优先级是给自己挖坑,加括号是最稳妥的做法。
还有一个隐藏坑:输入验证。如果用户输入的不是整数,int()会直接抛异常。上机题通常不要求处理这个,但如果你想让程序更健壮,可以用try-except包一层。这个在第三章可能还没学到,但提前知道没坏处。
2.3 三数排序:分支嵌套的思维训练
输入三个数,按从小到大输出。这道题是训练分支嵌套和变量交换的经典题。最直观的写法是用一系列if比较:
a = float(input("a: ")) b = float(input("b: ")) c = float(input("c: ")) if a > b: a, b = b, a if a > c: a, c = c, a if b > c: b, c = c, b print(a, b, c)这个写法的精妙之处在于:第一步保证a是a和b中较小的,第二步保证a是a和c中较小的(此时a已经是三者最小),第三步保证b是b和c中较小的(此时b是中间值,c是最大值)。三步下来排序完成。很多人第一次看到这个解法会觉得"怎么这么简洁",其实这就是算法思维的体现——用最少的比较次数完成排序。
对比另一种写法:把所有六种大小关系都列出来,用if-elif穷举。代码长,容易漏,而且扩展性差。如果题目改成四个数排序,穷举法直接崩溃,而上面的思路稍作扩展就能用。这就是为什么教材喜欢出这类题——它在训练你用通用逻辑而不是穷举逻辑解决问题。
3. 循环结构上机题:累加、累乘与数列计算的实战拆解
3.1 累加求和:从1到n的多种写法与性能差异
循环章节最基础的题是求1到n的和。教材通常要求用for循环和while循环各写一遍。for循环版本:
n = int(input("请输入n: ")) total = 0 for i in range(1, n + 1): total += i print(f"1到{n}的和是{total}")while循环版本:
n = int(input("请输入n: ")) total = 0 i = 1 while i <= n: total += i i += 1 print(f"1到{n}的和是{total}")两个版本功能一样,但有几个细节值得说。第一,range(1, n+1)的右边界是开区间,所以必须写n+1,这是新手最容易搞错的地方,写成range(1, n)会少加一个n。第二,while循环里i += 1的位置很关键,如果写在total += i前面,就会从2开始加;如果忘了写,就是死循环。我建议初学阶段优先用for循环,因为循环变量的更新由range自动管理,少一个出错点。
还有一个进阶写法是用内置函数sum(range(1, n+1)),一行搞定。但上机题通常要求你用循环实现,目的是训练循环思维,不要偷懒。不过知道有这种写法没坏处,实际工作中能用内置函数就用内置函数,性能和可读性都更好。
3.2 阶乘计算:累乘的边界与溢出问题
求n的阶乘是累乘的典型题:
n = int(input("请输入n: ")) result = 1 for i in range(1, n + 1): result *= i print(f"{n}的阶乘是{result}")这里有两个关键点。第一,result必须初始化为1而不是0,因为0乘任何数都是0。这是累乘和累加的核心区别,累加初始化为0,累乘初始化为1。第二,n=0时阶乘定义为1,上面的代码因为range(1, 1)为空,循环不执行,result保持1,正好正确。这就是为什么初始化值选对了,边界情况自动就对了。
Python的整数是任意精度的,所以算100的阶乘也不会溢出,只是数字会非常长。但如果你用C或Java写同样的题,就要考虑溢出了。这也是Python适合做算法入门的一个原因——你可以专注于逻辑,不用操心底层数据类型的限制。
3.3 斐波那契数列:循环与递推的思维转换
输出斐波那契数列前n项,这道题在第三章出现,是为了让你体会用循环代替递归的思路:
n = int(input("请输入项数: ")) a, b = 1, 1 for i in range(n): print(a, end=" ") a, b = b, a + b这段代码的核心是a, b = b, a + b这一行。Python的元组赋值会先计算右边,再同时赋给左边,所以不需要临时变量。如果用其他语言,得写成:
temp = a a = b b = temp + b斐波那契的递推关系是F(n) = F(n-1) + F(n-2),循环写法从前往后推,时间复杂度O(n),而递归写法是指数级的。第三章还没学递归,但教材放这道题就是埋个伏笔——等你学到递归那一章,回头对比就会发现循环的威力。
注意:
a, b = b, a + b这种写法在Python里很优雅,但初学阶段如果理解不了,就老老实实用临时变量,别为了炫技写出自己都看不懂的代码。
4. 循环嵌套:图形打印与乘法表的完整调试链路
4.1 打印三角形:外层控制行,内层控制列
循环嵌套是第三章的难点,图形打印是必考题型。以打印一个直角三角形为例:
n = int(input("请输入行数: ")) for i in range(1, n + 1): for j in range(i): print("*", end="") print()这段代码的逻辑是:外层循环i控制行数,从1到n;内层循环j控制每行打印的星号数,第i行打印i个。print()在每行结束后换行,end=""让print不自动换行。
新手最常见的错误是忘记内层循环的range边界。比如想打印第i行i个星号,写成range(i+1)就会多一个,写成range(1, i)就会少一个。我的经验是:先用小数据手推一遍。n=3时,i=1打印1个,i=2打印2个,i=3打印3个,验证range(i)是对的。这个手推过程看起来笨,但能帮你建立对循环边界的直觉。
如果要打印倒三角形:
n = int(input("请输入行数: ")) for i in range(n, 0, -1): for j in range(i): print("*", end="") print()这里外层循环用range(n, 0, -1)实现倒序,步长-1。很多人不习惯写步长,其实range的三个参数start、stop、step要记牢:start包含,stop不包含,step默认1。
4.2 九九乘法表:格式对齐与循环变量的配合
九九乘法表是循环嵌套的经典题:
for i in range(1, 10): for j in range(1, i + 1): print(f"{j}*{i}={i*j}", end="\t") print()这里有几个细节。第一,内层range(1, i+1)保证第i行打印i个算式,形成下三角。第二,end="\t"用制表符对齐,比空格整齐。第三,f"{j}*{i}={i*j}"里j在前i在后,符合乘法表的习惯写法。
如果想让输出更整齐,可以用格式化字符串控制宽度:
print(f"{j}*{i}={i*j:2d}", end=" "){i*j:2d}表示结果占2个字符宽度,右对齐。这样个位数和两位数就能对齐了。这种格式化技巧在实际输出报表时非常有用,值得掌握。
4.3 打印菱形:循环嵌套的进阶挑战
打印菱形是三角形题目的升级版,需要把菱形拆成上半部分和下半部分:
n = int(input("请输入菱形的半径: ")) for i in range(1, n + 1): print(" " * (n - i) + "*" * (2 * i - 1)) for i in range(n - 1, 0, -1): print(" " * (n - i) + "*" * (2 * i - 1))上半部分从1到n行,第i行的空格数是n-i,星号数是2i-1。下半部分从n-1倒着到1,公式一样。这个题的关键是找到空格数和星号数与行号的数学关系。我建议在纸上画一个n=3的菱形,数一数每行的空格和星号,然后归纳公式。这种"观察-归纳-验证"的过程,就是算法思维的核心训练。
很多人卡在菱形题上,不是因为代码写不出来,而是因为没耐心去推导数学关系。直接看答案当然快,但下次换个图形又不会了。我的建议是:图形打印题一定要自己动手画、动手数、动手推,推出来的公式才是你自己的。
5. 素数判断与最大公约数:算法入门的两个硬骨头
5.1 素数判断:从暴力法到平方根优化
判断一个数是否为素数是第三章循环部分的综合题:
n = int(input("请输入一个正整数: ")) if n < 2: print(f"{n}不是素数") else: is_prime = True for i in range(2, int(n ** 0.5) + 1): if n % i == 0: is_prime = False break if is_prime: print(f"{n}是素数") else: print(f"{n}不是素数")这段代码有几个关键点。第一,n<2要单独处理,因为1不是素数,0和负数也不是。第二,循环上界用int(n**0.5)+1而不是n,这是数学优化——如果一个数有因子,必然有一个因子小于等于它的平方根。第三,找到因子后用break提前退出,避免无谓的循环。
我见过很多学生写成for i in range(2, n),功能是对的,但n很大时效率极低。教材出这道题,表面考循环,实际考的是算法优化意识。平方根优化这个知识点,在后续学算法复杂度时还会反复出现,现在打好基础很重要。
提示:
n ** 0.5是开平方,也可以用math.sqrt(n),但需要import math。两种写法都行,**0.5不需要额外导入,更简洁。
5.2 最大公约数:辗转相除法的循环实现
求两个数的最大公约数,教材通常要求用辗转相除法(欧几里得算法):
a = int(input("请输入第一个数: ")) b = int(input("请输入第二个数: ")) m, n = a, b while n != 0: m, n = n, m % n print(f"{a}和{b}的最大公约数是{m}")辗转相除法的原理是:gcd(a, b) = gcd(b, a mod b),直到b为0,此时a就是最大公约数。用m, n = n, m % n这一行同时更新两个变量,是Python的优雅写法。
这道题的坑在于循环终止条件。必须是n != 0,如果写成n > 0,当输入有负数时就会出问题。虽然题目通常默认输入正整数,但养成考虑边界条件的习惯很重要。另外,如果输入a=0或b=0,辗转相除法也能正确处理(gcd(0, b) = b),但前提是循环条件写对。
最小公倍数可以用a * b // gcd(a, b)求得,注意用整除//而不是普通除法/,否则结果是浮点数。
6. 上机调试的实战心得:那些教材不会告诉你的经验
6.1 用print大法定位循环问题
初学阶段最有效的调试手段就是在关键位置插入print。比如循环嵌套出问题时,在外层循环开始处打印当前行号,在内层循环里打印当前列号,一眼就能看出哪一层出了问题。我见过太多学生对着代码干瞪眼,其实加两行print,问题立刻现形。
具体做法:在循环体开头加print(f"i={i}, j={j}"),运行后看输出是否符合预期。如果i和j的变化规律和你想象的不一样,说明循环边界或步长写错了。这个方法虽然原始,但在没有调试器或者不熟悉调试器的情况下,是最快的手段。
6.2 边界值测试:0、1、负数、大数一个都不能少
上机题最容易在边界值上翻车。我总结了一个边界测试清单:
| 测试类型 | 测试值 | 常见问题 |
|---|---|---|
| 零值 | 0 | 循环不执行、除零错误 |
| 最小值 | 1 | 循环边界差一 |
| 负数 | -1 | 条件判断遗漏 |
| 大数 | 1000000 | 性能问题、溢出 |
| 空输入 | 直接回车 | 类型转换异常 |
每次写完代码,至少用这五类值各测一遍。比如素数判断,n=0、n=1、n=2、n=3、n=4都要测。n=2是唯一的偶素数,特别容易出错。这个习惯一旦养成,代码质量会有质的提升。
6.3 循环变量的作用域与命名
Python的循环变量在循环结束后依然存在,这点和其他语言不同。比如:
for i in range(5): pass print(i) # 输出4循环结束后i的值是4,不是未定义。这个特性有时候会导致隐蔽的bug,比如你在循环外又用了i,但忘了它已经被循环修改过。我的建议是:循环变量用有意义的短名字,比如i、j用于嵌套循环,n用于计数,ch用于字符。不要用x、y这种容易和数学变量混淆的名字。
另外,如果循环变量在循环外还要用,最好在循环前先初始化,或者用不同的变量名。这种细节在写复杂程序时能帮你省很多调试时间。
6.4 从"能跑"到"跑对":测试用例的设计思路
很多学生写完代码,输入一个样例,输出对了,就认为完成了。但上机题的评分往往有多个测试用例,隐藏用例专门针对边界情况。我的经验是:自己设计测试用例时,要覆盖正常值、边界值、特殊值三类。
以阶乘为例,正常值测5,边界值测0和1,特殊值测10(验证大数计算)。以素数判断为例,正常值测7和8,边界值测1和2,特殊值测9(平方数)和25(平方数)。这种测试思路,其实就是软件测试里的等价类划分和边界值分析,提前掌握对后续学习很有帮助。
7. 从第三章上机题看Python学习的节奏感
第三章的上机实践题,表面上是练分支和循环,实际上是在训练三种能力:逻辑分解能力(把问题拆成条件判断和重复步骤)、边界意识(考虑所有可能的输入情况)、调试能力(出错了知道怎么找原因)。这三种能力,比记住几个语法点重要得多。
我个人的体会是,第三章的题不要贪快。每道题自己先写一遍,跑通了再对照参考答案,看看别人的写法有没有值得借鉴的地方。特别是循环嵌套和素数判断这类题,一题多解很常见,对比不同解法能加深理解。比如素数判断,除了平方根优化,还有埃拉托斯特尼筛法,虽然第三章用不上,但知道有更高效的算法,对建立算法视野有好处。
最后分享一个我常用的练习方法:改题。把教材上的题目改一改,比如把求1到n的和改成求1到n的偶数和,把打印直角三角形改成打印等腰三角形,把判断素数改成判断完全数。改题的过程就是举一反三的过程,能帮你真正掌握知识点,而不是死记代码。第三章的题目数量有限,但通过改题可以衍生出无限多的练习,这才是上机实践的正确打开方式。