
1. 从一道上机题看Python分支与循环的真实门槛很多人拿到《Python程序设计与算法基础教程第二版微课版》第三章的上机实践题第一反应是这不就是if-else和for循环吗能有多难。我带过几届刚入门的学生也帮不少转行的朋友看过代码发现一个很普遍的现象语法都认识一上机就卡壳。第三章恰恰是Python从能看懂到能写出来的分水岭——分支结构、循环结构、循环嵌套、break与continue的控制流这些概念单独拎出来都不复杂但一旦组合成上机题尤其是涉及边界条件、累加累乘、图形打印、素数判断这类经典题型问题就集中爆发了。这篇文章不打算给你一份干巴巴的答案清单那种东西网上到处都是抄完就忘。我想做的是把第三章上机实践里最典型的几类题目拆开讲清楚每道题背后的设计意图、常见错误、调试思路以及我在实际教学和代码review中总结出来的经验。你如果是正在学这门课的学生可以把它当成一份解题思路避坑手册如果你是自学者或者需要辅导别人的人这里面的分析框架同样适用。核心关键词就几个分支结构、循环控制、循环嵌套、边界条件、算法基础围绕它们展开。第三章的上机题通常有这么几个特征题目描述简短但隐藏条件多看起来能跑通但测试用例一多就露馅很多题有数学味比如求最大公约数、判断素数、打印乘法表、计算数列和。这些题不是随便出的它们对应着算法思维的基本功。我下面按题型分类来讲每一类都会给出完整代码、逐行解释、常见错误对照以及我踩过的坑。2. 分支结构上机题为什么你的if-else总是漏掉一种情况2.1 分段函数与多条件判断的典型陷阱第三章分支部分最常见的上机题是分段函数求值比如根据输入的x值按照不同区间计算y。题目本身不难但错误率高得惊人。我统计过一批学生的提交超过六成的人第一次写出来的代码在边界点上出错。问题出在哪出在区间开闭的判断上。举个例子题目要求x0时y-1x0时y0x0时y1。很多人写成x float(input(请输入x: )) if x 0: y -1 elif x 0: y 1 else: y 0 print(y)这个写法是对的因为else兜住了x0的情况。但换成更复杂的分段比如x0、0x10、10x20、x20四段很多人就会写成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结果x20的时候因为前面的条件都用了严格小于反而漏掉了。我的建议是分段函数要么用累进式写法只写上界靠顺序兜底要么把每个区间的开闭都写清楚但绝不要混用。提示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(f1到{n}的和是{total})while循环版本n int(input(请输入n: )) total 0 i 1 while i n: total i i 1 print(f1到{n}的和是{total})两个版本功能一样但有几个细节值得说。第一range(1, n1)的右边界是开区间所以必须写n1这是新手最容易搞错的地方写成range(1, n)会少加一个n。第二while循环里i 1的位置很关键如果写在total i前面就会从2开始加如果忘了写就是死循环。我建议初学阶段优先用for循环因为循环变量的更新由range自动管理少一个出错点。还有一个进阶写法是用内置函数sum(range(1, n1))一行搞定。但上机题通常要求你用循环实现目的是训练循环思维不要偷懒。不过知道有这种写法没坏处实际工作中能用内置函数就用内置函数性能和可读性都更好。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。第二n0时阶乘定义为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(i1)就会多一个写成range(1, i)就会少一个。我的经验是先用小数据手推一遍。n3时i1打印1个i2打印2个i3打印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, i1)保证第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公式一样。这个题的关键是找到空格数和星号数与行号的数学关系。我建议在纸上画一个n3的菱形数一数每行的空格和星号然后归纳公式。这种观察-归纳-验证的过程就是算法思维的核心训练。很多人卡在菱形题上不是因为代码写不出来而是因为没耐心去推导数学关系。直接看答案当然快但下次换个图形又不会了。我的建议是图形打印题一定要自己动手画、动手数、动手推推出来的公式才是你自己的。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}不是素数)这段代码有几个关键点。第一n2要单独处理因为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当输入有负数时就会出问题。虽然题目通常默认输入正整数但养成考虑边界条件的习惯很重要。另外如果输入a0或b0辗转相除法也能正确处理gcd(0, b) b但前提是循环条件写对。最小公倍数可以用a * b // gcd(a, b)求得注意用整除//而不是普通除法/否则结果是浮点数。6. 上机调试的实战心得那些教材不会告诉你的经验6.1 用print大法定位循环问题初学阶段最有效的调试手段就是在关键位置插入print。比如循环嵌套出问题时在外层循环开始处打印当前行号在内层循环里打印当前列号一眼就能看出哪一层出了问题。我见过太多学生对着代码干瞪眼其实加两行print问题立刻现形。具体做法在循环体开头加print(fi{i}, j{j})运行后看输出是否符合预期。如果i和j的变化规律和你想象的不一样说明循环边界或步长写错了。这个方法虽然原始但在没有调试器或者不熟悉调试器的情况下是最快的手段。6.2 边界值测试0、1、负数、大数一个都不能少上机题最容易在边界值上翻车。我总结了一个边界测试清单测试类型测试值常见问题零值0循环不执行、除零错误最小值1循环边界差一负数-1条件判断遗漏大数1000000性能问题、溢出空输入直接回车类型转换异常每次写完代码至少用这五类值各测一遍。比如素数判断n0、n1、n2、n3、n4都要测。n2是唯一的偶素数特别容易出错。这个习惯一旦养成代码质量会有质的提升。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的偶数和把打印直角三角形改成打印等腰三角形把判断素数改成判断完全数。改题的过程就是举一反三的过程能帮你真正掌握知识点而不是死记代码。第三章的题目数量有限但通过改题可以衍生出无限多的练习这才是上机实践的正确打开方式。