首页 / 资讯中心 / 文章详情

循环边界总出错?8.6章习题复盘与边界思维训练

循环边界总出错?8.6章习题复盘与边界思维训练 ★ FEATURED ARTICLE
1. 为什么循环边界是编程新手的“第一道鬼门关”1.1 从一道习题看循环边界的杀伤力编程基础课学到循环这一章大部分人都会经历一个相同的困惑期语法看懂了例子也能跟着敲出来但一到自己独立写习题程序要么少跑一次要么多跑一次要么干脆死循环。这不是你笨而是循环边界本身就是编程里最容易出错的地方之一连工作多年的老手在写二分查找时都可能栽在边界条件上。我当年带过几个刚入门的朋友他们做8.6章习题时最常卡住的就三类题累加求和、图形打印、以及带条件的循环控制。这三类题表面上看是不同题型实际上考的是同一个核心能力——你能不能准确判断循环该从哪里开始、到哪里结束、每次变化多少。把这三个问题想清楚了循环题基本就稳了。这一章习题的价值不在于题目本身有多难而在于它逼着你把“边界”这件事想明白。想明白之后后面学数组、学递归、学算法都会轻松很多。因为数组越界、递归终止条件这些坑本质上和循环边界是同一类问题。1.2 循环三要素初值、条件、步长任何循环都可以拆成三个要素我习惯叫它们“循环三要素”初值循环变量从哪个数开始条件满足什么条件时继续循环步长每次循环变量怎么变化这三者共同决定了循环体执行的次数。很多新手写循环时只关注循环体里干什么却忽略了这三个要素的配合结果就是“逻辑看着对结果就是不对”。举个最典型的例子求1到100的和。有人写成total 0 i 1 while i 100: total i i 1 print(total)这段代码的结果是4950而不是5050因为条件写成了i 100漏掉了100。正确的应该是i 100。就这么一个符号的差别结果差了100。这就是边界的威力。我个人的经验是写循环之前先在纸上或者注释里把三要素写清楚尤其是“循环结束时循环变量应该等于多少”这个问题想清楚了再动手能省掉大量调试时间。1.3 边界错误为什么这么难自查边界错误难查是因为它往往不会报错。程序能跑结果也“看起来差不多”只是差了一点点。这种错误比语法错误更可怕因为语法错误编译器会直接告诉你而边界错误需要你自己发现。我见过一个朋友写打印九九乘法表的程序打印出来是8行而不是9行他盯着代码看了半小时没看出来问题最后发现是外层循环写成了for i in range(1, 9)而Python的range是左闭右开的range(1, 9)只到8。这个坑几乎每个Python初学者都会踩一次。所以复盘这一章习题时重点不是把答案抄一遍而是要把每道题的边界条件单独拎出来分析搞清楚为什么是这个边界换成别的边界会怎样。这种“边界敏感度”一旦建立起来受益终身。2. 累加累乘类习题的边界处理与常见陷阱2.1 求和题的三种写法与边界对比求和是循环习题里最基础的一类但基础不代表简单。以“求1到n的和”为例至少有三种常见写法每种写法的边界处理都不一样。第一种是while循环def sum_while(n): total 0 i 1 while i n: total i i 1 return total第二种是for循环配rangedef sum_for(n): total 0 for i in range(1, n 1): total i return total第三种是直接用数学公式def sum_formula(n): return n * (n 1) // 2这三种写法里前两种的边界最容易出错。while版本的关键是i n这个条件如果写成i n就漏了n。for版本的关键是range(1, n 1)如果写成range(1, n)同样漏了n。而公式法没有循环也就没有边界问题但需要你记得公式。提示初学阶段建议先用循环写理解过程之后再了解公式法。不要一上来就用公式那样练不到边界思维。2.2 累乘题的溢出与初值陷阱累乘和累加结构相似但有两个额外的坑。第一个是初值累加的初值是0累乘的初值必须是1。如果累乘也写total 0那不管循环多少次结果都是0因为0乘任何数都是0。这个错误新手犯得特别多而且因为程序不报错很难发现。第二个坑是溢出。在C、Java这类语言里阶乘增长极快算到13的阶乘就可能超出int范围算到21的阶乘会超出long范围。Python虽然自动处理大整数不会溢出但如果你在学C或Java做累乘题时一定要考虑数据类型的范围。我建议做累乘题时养成一个习惯先估算结果的最大值再决定用什么数据类型。比如算20的阶乘结果约2.4×10^18刚好在64位整数范围内但21的阶乘就超了。这种估算能力在实际开发中很重要。2.3 带条件的累加筛选逻辑与边界叠加稍微进阶一点的题目是“求1到100之间所有偶数的和”或者“求1到100之间能被3整除的数的和”。这类题在累加的基础上加了一个筛选条件边界问题会叠加。以“求1到100之间所有偶数的和”为例有两种写法。第一种是遍历所有数用if筛选total 0 for i in range(1, 101): if i % 2 0: total i第二种是直接遍历偶数total 0 for i in range(2, 101, 2): total i第一种写法的边界是range(1, 101)第二种是range(2, 101, 2)。第二种写法效率更高但边界更容易错。range(2, 101, 2)的最后一个数是100因为101是开区间取不到而步长2保证了所有数都是偶数。如果写成range(2, 100, 2)最后一个数就是98漏了100。我个人的偏好是筛选条件简单时用第一种写法可读性好不容易错筛选条件复杂或者性能要求高时用第二种。但不管用哪种写完都要手动验证一下首尾两个数是否被正确包含。3. 图形打印类习题嵌套循环的边界配合3.1 直角三角形打印的行列关系图形打印是循环习题里最能锻炼边界思维的一类。以打印一个n行的直角三角形为例第1行1个星号第2行2个星号以此类推。这道题需要嵌套循环外层控制行数内层控制每行的星号数。n 5 for i in range(1, n 1): for j in range(1, i 1): print(*, end) print()这里的关键是内层循环的边界range(1, i 1)它依赖于外层变量i。i等于1时内层跑1次i等于5时内层跑5次。如果内层写成range(1, i)那第i行就只有i-1个星号整个图形就变形了。这类题的核心思路是找到每一行的数量与行号之间的函数关系。直角三角形的关系是“第i行有i个”所以内层边界是i。把这个关系想清楚代码自然就出来了。3.2 等腰三角形的空格与星号双重边界等腰三角形比直角三角形难一个档次因为它每行既有空格又有星号而且两者的数量都随行号变化。以n5为例第1行4个空格1个星号第2行3个空格3个星号第3行2个空格5个星号。规律是第i行的空格数是n - i星号数是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()这道题的边界有三个地方要注意外层range(1, n 1)保证n行空格循环range(n - i)i1时有n-1个空格星号循环range(2 * i - 1)i1时有1个星号。三个边界任何一个错了图形都会歪。我当年做这道题时空格数算错了好几次后来发现是因为我习惯性地把空格循环写成range(n - i 1)多了一个空格。这种错误只能通过实际打印出来看效果才能发现所以做图形题一定要运行看结果不能光靠脑补。3.3 打印图形时的换行与格式细节图形打印还有一个容易被忽略的细节换行。内层循环负责打印一行内的字符外层循环每结束一轮要换行。如果换行位置放错所有字符会挤在一行。在Python里print()默认换行print(x, end)不换行。所以内层循环用end外层循环末尾单独调用print()换行。这个模式要记牢。另外有些题目要求打印的图形有特定格式比如数字金字塔、菱形等。这些题目的思路是一样的先找每行的空格数和字符数与行号的函数关系再用嵌套循环实现。难点不在代码而在找规律。我建议做这类题时先在纸上画出n5的情况标出每行的空格数和字符数规律一目了然。4. 循环控制语句的边界break与continue的正确用法4.1 break的终止时机与常见误用break用于提前终止循环但它的终止时机很容易搞错。看这道题在1到100中找第一个能被7整除的数。for i in range(1, 101): if i % 7 0: print(i) break结果是7正确。但如果把break写在if外面for i in range(1, 101): if i % 7 0: print(i) break那结果还是7但原因是循环第一次就break了只检查了i1。这种错误很隐蔽因为结果“碰巧”对了但逻辑完全错了。如果题目改成“找第一个能被13整除的数”第二种写法就会输出1明显错误。break的位置必须紧跟在找到目标之后不能放在循环体的其他位置。我个人的习惯是写完带break的循环后一定用一个“找不到”的案例测试一下看看循环是否能正常跑完。4.2 continue跳过与边界更新的坑continue用于跳过本次循环剩余部分直接进入下一次。它最常见的坑是如果循环变量的更新写在continue之后那更新语句会被跳过导致死循环。i 0 while i 10: if i % 2 0: continue print(i) i 1这段代码会死循环因为i0时满足偶数条件continue跳过了i 1i永远是0。正确的写法是把i 1放在continue之前或者改用for循环。注意用while配continue时一定要确保循环变量的更新在continue之前执行否则必死循环。这是新手最容易踩的坑之一。4.3 循环嵌套中break只跳一层的陷阱在嵌套循环里break只能跳出它所在的那一层不能一次跳出所有层。这是很多人不知道的细节。for i in range(3): for j in range(3): if j 1: break print(i, j)这段代码里break只跳出内层循环外层循环继续。输出是(0,0) (1,0) (2,0)。如果你想一次跳出两层需要用一个标志变量或者函数返回。found False for i in range(3): for j in range(3): if j 1: found True break if found: break这种写法虽然啰嗦但逻辑清晰。在Python里也可以用for...else结构但那个语法比较绕新手容易搞混我一般建议用标志变量。5. 经典习题复盘从错误中提炼边界思维5.1 素数判断循环上界的数学依据素数判断是循环习题里的经典题。判断n是否为素数需要检查2到n-1之间有没有能整除n的数。但更高效的做法是只检查到√n。import math def is_prime(n): if n 2: return False for i in range(2, int(math.sqrt(n)) 1): if n % i 0: return False return True这里的边界int(math.sqrt(n)) 1是关键。为什么要加1因为range是左闭右开如果不加1当n是完全平方数时平方根那个因子会被漏掉。比如n9math.sqrt(9)是3.0int之后是3range(2, 3)只检查2漏了3结果会把9误判为素数。为什么只需要检查到√n因为如果n有一个大于√n的因子那必然有一个小于√n的对应因子。比如n100因子对是(2,50)(4,25)(5,20)(10,10)所有大于10的因子都对应一个小于10的因子。所以检查到√n就够了。这道题把边界和数学原理结合得很好是训练边界思维的好素材。5.2 水仙花数位数提取与循环范围水仙花数是指一个三位数其各位数字的立方和等于它本身。比如153 1³ 5³ 3³。这道题的循环范围是100到999边界很明确。for num in range(100, 1000): a num // 100 b (num // 10) % 10 c num % 10 if a**3 b**3 c**3 num: print(num)这道题的坑在于位数提取。num // 100取百位(num // 10) % 10取十位num % 10取个位。这三个公式要记牢后面做类似题目会反复用到。循环范围range(100, 1000)的边界是100到999因为水仙花数定义就是三位数。如果写成range(100, 999)就漏了999虽然999不是水仙花数但边界写错说明思维不严谨。5.3 斐波那契数列循环与递推的边界衔接斐波那契数列是1, 1, 2, 3, 5, 8...从第三项开始每项等于前两项之和。用循环实现时边界处理有讲究。def fib(n): if n 0: return 0 if n 1 or n 2: return 1 a, b 1, 1 for i in range(3, n 1): a, b b, a b return b这里的边界有两处前两项单独处理循环从3开始到n。为什么从3开始因为第1、2项已知从第3项才开始递推。循环条件range(3, n 1)保证第n项被算出来。这道题的难点在于理解“循环变量i代表的是第几项”。i3时算第3项in时算第n项。如果写成range(3, n)那第n项就算不出来。这种“循环变量与业务含义对应”的思维是边界思维的高级形态。6. 循环边界自查清单与调试技巧6.1 写完循环必做的三个检查根据我多年的经验写完任何循环后做这三个检查能发现90%的边界错误首项检查循环第一次执行时循环变量是多少这个值符合预期吗末项检查循环最后一次执行时循环变量是多少有没有漏掉或多算零次检查如果循环条件一开始就不满足循环体不执行这个结果合理吗以for i in range(1, n 1)为例首项是1末项是n如果n0则一次都不执行。这三个检查做完边界基本就稳了。我个人的习惯是写完循环后在代码旁边用注释标出首项和末项的值比如# i: 1 - n。这样一眼就能看出边界对不对比在脑子里想要可靠得多。6.2 用打印语句追踪循环变量调试循环最有效的方法是在循环体里打印循环变量的值。虽然简单但极其管用。for i in range(1, 6): print(fi {i}) # 循环体逻辑运行后看输出的i值序列是不是1,2,3,4,5。如果是1,2,3,4那就是边界少了如果是1,2,3,4,5,6那就是多了。这种方法比盯着代码看快得多。对于嵌套循环可以在内层循环打印(i, j)的组合看看所有组合是否都符合预期。图形打印题尤其适合这种方法把每行的空格数和星号数打印出来和纸上画的对比问题一目了然。6.3 常见边界错误速查表错误现象常见原因排查方法结果少一个条件用了而非或range右边界少1检查末项是否被包含结果多一个条件用了而非或range右边界多1检查首项是否从0开始死循环while条件永远为真或continue跳过了变量更新打印循环变量看是否变化累乘结果为0初值写成了0而非1检查累乘变量初值嵌套循环只跳一层break只能跳出当前层用标志变量或函数返回图形错位空格数或字符数的边界算错打印每行的空格数和字符数这张表是我自己踩坑总结出来的基本上循环题的错误都能对上号。做习题时如果卡住了先对照这张表排查比盲目改代码效率高得多。7. 从习题到实战边界思维的迁移价值7.1 数组越界与循环边界的同源性学完循环再学数组你会发现数组越界和循环边界是同一类问题。数组下标从0开始长度是n的数组有效下标是0到n-1。遍历数组时循环条件应该是i n而不是i n否则就会越界。arr [1, 2, 3, 4, 5] n len(arr) for i in range(n): # 正确0到4 print(arr[i]) for i in range(n 1): # 错误0到5arr[5]越界 print(arr[i])这个坑和循环边界是同一个思维模式搞清楚有效范围的首尾。循环题练好了数组题自然就不会错。反过来如果循环边界总是搞不清数组越界会一直困扰你。7.2 二分查找边界思维的终极考验二分查找是边界思维的最高考验因为它的边界条件极其微妙。写错一个符号结果就可能完全错误而且不一定报错。def binary_search(arr, target): left, right 0, len(arr) - 1 while left right: mid (left right) // 2 if arr[mid] target: return mid elif arr[mid] target: left mid 1 else: right mid - 1 return -1这段代码里有四个边界right初值是len(arr) - 1循环条件是left right更新时left mid 1和right mid - 1。任何一个写错都会导致错误。比如循环条件写成left right当数组只有一个元素时就会漏查。我当年学二分查找时反复写错了五六次才彻底搞明白。后来发现只要把“搜索区间”这个概念想清楚——是左闭右闭还是左闭右开——边界就自然确定了。这个思维方式正是从循环习题里练出来的。7.3 把边界检查变成肌肉记忆边界思维的最高境界是变成肌肉记忆写循环时不用刻意想手自然就写对了。这需要大量的练习和复盘。我的建议是每做完一道循环题都问自己三个问题这道题的边界是什么为什么是这个边界换成别的边界会怎样把这三个问题回答清楚比做十道题都管用。另外养成“先写边界再写循环体”的习惯。先把for i in range(?, ?)或者while ?的框架搭好确认边界无误再往里填逻辑。这样能把边界问题和逻辑问题分开处理降低出错概率。循环边界这件事说难不难说简单也不简单。它不需要高深的数学但需要严谨的思维和大量的练习。8.6章这些习题表面上是练循环语法实际上是在训练一种“精确控制”的思维方式。这种思维方式一旦建立后面学什么都会事半功倍。我自己带新人的经验是循环边界过关的人后面学算法、学数据结构都会顺很多循环边界总是含糊的人后面会一直卡在各种越界和死循环上。所以这一章值得多花时间把每道题的边界都抠清楚。
阅读完成 · 觉得有帮助?
咨询建站