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循环配range:
def 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 等腰三角形的空格与星号双重边界
等腰三角形比直角三角形难一个档次,因为它每行既有空格又有星号,而且两者的数量都随行号变化。以n=5为例,第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),i=1时有n-1个空格;星号循环range(2 * i - 1),i=1时有1个星号。三个边界任何一个错了,图形都会歪。
我当年做这道题时,空格数算错了好几次,后来发现是因为我习惯性地把空格循环写成range(n - i + 1),多了一个空格。这种错误只能通过实际打印出来看效果才能发现,所以做图形题一定要运行看结果,不能光靠脑补。
3.3 打印图形时的换行与格式细节
图形打印还有一个容易被忽略的细节:换行。内层循环负责打印一行内的字符,外层循环每结束一轮要换行。如果换行位置放错,所有字符会挤在一行。
在Python里,print()默认换行,print(x, end="")不换行。所以内层循环用end="",外层循环末尾单独调用print()换行。这个模式要记牢。
另外,有些题目要求打印的图形有特定格式,比如数字金字塔、菱形等。这些题目的思路是一样的:先找每行的空格数和字符数与行号的函数关系,再用嵌套循环实现。难点不在代码,而在找规律。我建议做这类题时先在纸上画出n=5的情况,标出每行的空格数和字符数,规律一目了然。
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了,只检查了i=1。这种错误很隐蔽,因为结果“碰巧”对了,但逻辑完全错了。如果题目改成“找第一个能被13整除的数”,第二种写法就会输出1,明显错误。
break的位置必须紧跟在找到目标之后,不能放在循环体的其他位置。我个人的习惯是,写完带break的循环后,一定用一个“找不到”的案例测试一下,看看循环是否能正常跑完。
4.2 continue跳过与边界更新的坑
continue用于跳过本次循环剩余部分,直接进入下一次。它最常见的坑是:如果循环变量的更新写在continue之后,那更新语句会被跳过,导致死循环。
i = 0 while i < 10: if i % 2 == 0: continue print(i) i += 1这段代码会死循环,因为i=0时满足偶数条件,continue跳过了i += 1,i永远是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是完全平方数时,平方根那个因子会被漏掉。比如n=9,math.sqrt(9)是3.0,int之后是3,range(2, 3)只检查2,漏了3,结果会把9误判为素数。
为什么只需要检查到√n?因为如果n有一个大于√n的因子,那必然有一个小于√n的对应因子。比如n=100,因子对是(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代表的是第几项”。i=3时算第3项,i=n时算第n项。如果写成range(3, n),那第n项就算不出来。这种“循环变量与业务含义对应”的思维,是边界思维的高级形态。
6. 循环边界自查清单与调试技巧
6.1 写完循环必做的三个检查
根据我多年的经验,写完任何循环后,做这三个检查能发现90%的边界错误:
- 首项检查:循环第一次执行时,循环变量是多少?这个值符合预期吗?
- 末项检查:循环最后一次执行时,循环变量是多少?有没有漏掉或多算?
- 零次检查:如果循环条件一开始就不满足,循环体不执行,这个结果合理吗?
以for i in range(1, n + 1)为例,首项是1,末项是n,如果n=0则一次都不执行。这三个检查做完,边界基本就稳了。
我个人的习惯是,写完循环后在代码旁边用注释标出首项和末项的值,比如# i: 1 -> n。这样一眼就能看出边界对不对,比在脑子里想要可靠得多。
6.2 用打印语句追踪循环变量
调试循环最有效的方法是在循环体里打印循环变量的值。虽然简单,但极其管用。
for i in range(1, 6): print(f"i = {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到5,arr[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章这些习题,表面上是练循环语法,实际上是在训练一种“精确控制”的思维方式。这种思维方式一旦建立,后面学什么都会事半功倍。我自己带新人的经验是,循环边界过关的人,后面学算法、学数据结构都会顺很多;循环边界总是含糊的人,后面会一直卡在各种越界和死循环上。所以这一章值得多花时间,把每道题的边界都抠清楚。