尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C语言循环求和实战:边界、取模、溢出与自测
这道题我在给初学的朋友讲循环的时候几乎每次都会拿出来不是因为它难恰恰是因为它太简单了简单到大部分人写完就交根本没意识到自己在里面埋了多少雷。C语言里求1到100之间所有能被3整除的数之和标准答案就三五行一个循环从1数到100一个if判断当前数能不能被3整除一个变量把合格的数攒起来。写过的人会觉得没什么可讲的但真拿去编译运行结果从 1584 到 1782 各种数字都有人跑出来过还有人打印了满屏的中间过程却始终没敢确认最后那个数对不对。这篇就把这道题从题意边界、整数取模、循环边界、累加器初值、类型溢出一直拆到函数封装和自测顺便聊聊同样一套骨架怎么迁移到“被3和5同时整除”“被3或5整除”这类变化题上。适合刚学完for和if、想把这几个基础件真正用扎实的人也适合已经写了几个月、想回头把自己的代码习惯捋一遍的人。看完你拿到的不只是一个 1683而是一套可以反复套用的判断流程。1. 先拆题意这道题真正在考什么1.1 三个动作遍历、判定、累加题目读起来一句话落到代码上是三个彼此独立的动作。第一个动作是确定候选集合也就是“1到100之间”的所有整数这是遍历的边界第二个动作是写筛选条件把“能被3整除”翻译成计算机能执行的表达式核心是取模运算%第三个动作是做累积用一个变量把每次筛出来的数加进去最后输出。这三个动作分别对应循环控制、条件分支和累加模式是后面几乎所有数据处理代码的最小骨架。之所以强调“三个动作”是因为新手最典型的错误就是把它们的顺序或者位置搞混。比如把sum i写在if外面那就变成了求 1 到 100 的总和把if写在循环外面只判断一次i的初值把printf写在循环里面结果输出 33 行。单独看每一行都没错组合起来就完全不是题目要的东西。拆成三个动作之后你就可以逐块检查候选集合对不对、条件表达式对不对、累积的位置对不对排查效率会高很多。还有一个容易被忽略的点这三个动作有严格的先后关系。判定必须发生在遍历内部累加必须发生在判定成立之后。一旦位置错位代码依然能编译通过也可能跑出一个看起来合理的数字这是最危险的——错误被一个“像那么回事”的结果掩盖了你不知道自己错了。1.2 “1到100之间”的边界到底怎么算边界这件事值得单独拎出来说。中文里的“1到100之间”在绝大多数教材和考试语境下指的是闭区间也就是包含 1 和包含 100。所以循环条件应该写成i 100而不是i 100。这里的差别在这道题上不是无关紧要的能被3整除的最大数是 99如果写成i 10099 依然会被算进去结果还是 1683表面上看不出问题。但如果题目换成“1到100之间所有能被2整除的数之和”100 就在边界上i 100会直接漏掉 100结果少 100。我一般建议养成习惯边界值单独拿出来手算一遍。这道题就手算首项和末项——首项是 3末项是 99两个都落在区间内说明边界取法没问题。这个动作只要十几秒但能挡掉一大类 off-by-one 错误。很多人调半天调不出来最后发现就是和差了一个。另外还有一个隐藏边界如果题目改成“求1到n之间”n 小于 3 的时候怎么办比如 n 2区间内根本没有能被3整除的数。这时候如果代码写的是for (i 1; i n; i)循环会正常跑完两次sum保持初值输出 0逻辑上是对的。但如果用步长法for (i 3; i n; i 3)n 2 时循环一次都不进sum还是 0也对。所以只要sum初始化正确这个边界自动就是安全的。真正会出事的是后面要讲的公式法里面的整数除法会把 2/3 直接截断成 0这个我们放到第 4 节细说。1.3 为什么新手会在这几行代码上翻车翻车的原因不在语法在注意力分配。这道题太短短到大脑会自动跳过验证环节。写完之后人会有一种“这么简单还能错”的自信于是不编译、不打印中间结果、不手算核对。我见过最典型的一次是有位朋友把判断写成了i / 3 0他自己看了一遍觉得“除得尽就是整除”非常顺。实际上i / 3 0只在i取 0、1、2 的时候成立1 到 100 里只有 1 和 2 满足最后输出的是 3。他跑了之后看到 3愣了一下还是没意识到哪错了因为 3 本身是个“看起来合理”的数。这就是基础题的杀伤力错误的结果往往也是个小整数不刺眼。所以我在讲这道题的时候会强制加一条规则——无论多简单的程序都必须有一个不依赖这段代码的答案作为基准。这道题的基准可以用等差数列手算出来也就是下一节要做的事。有了基准你输出的任何数字都能立刻被判定真假而不是靠感觉。2. 最朴素版本的完整实现与逐行拆解2.1 完整代码与运行结果先把能跑的版本贴出来后面所有讨论都基于它。#include stdio.h int main(void) { int sum 0; int i; for (i 1; i 100; i) { if (i % 3 0) { sum i; } } printf(1到100之间所有能被3整除的数之和 %d\n, sum); return 0; }编译运行输出是1到100之间所有能被3整除的数之和 1683代码一共不到 20 行但没有一行是多余的。#include stdio.h是为了用printfint sum 0定义累加器并清零int i是循环变量放在外面声明是 C89 风格的写法老编译器比如一些教学环境用的旧版编译器不支持在for里声明变量main的返回值写int而不是void这是标准要求返回 0 表示程序正常结束。如果你想看得更清楚可以在if里面加一行打印把每次命中的数都输出if (i % 3 0) { printf(%d , i); sum i; }这样能看到3 6 9 12 ... 99一串数最后再输出和。这个版本特别适合第一次写的时候自检因为你能一眼数出是不是 33 个数也能看出有没有漏掉 99 这种边界项。2.2 逐行解释三个必须想清楚的问题第一个问题sum的初值为什么必须是 0。局部变量在 C 语言里不会自动清零它拿到的是一块栈上的旧内存值是不确定的。如果不写 0sum可能一开始就是 1234567后面加得再对结果也是错的。更麻烦的是这种错误在不同机器、不同编译器、不同运行次数下表现不一致有时候跑出正确结果有时候跑出天文数字特别难查。所以累加器的初始化不是“可写可不写”是硬性要求。顺带说一句用gcc -Wall -Wextra编译的时候未初始化的局部变量是会报警告的把警告当错误看加-Werror能挡住不少这类问题。第二个问题判断为什么是i % 3 0而不是别的。%是取模运算符a % b的结果是 a 除以 b 的余数。能被 3 整除等价于除以 3 余数为 0所以i % 3 0。这里最容易犯的错是把写成变成赋值。if (i % 3 0)在 C 里是语法错误因为赋值表达式不能作为左值给 if 用……实际上这行会直接编译报错因为i % 3不是左值但如果写成if (x 0)这种编译器可能只给个警告然后条件恒为假循环体永远不执行最后输出 0。所以比较运算一定要双等号。第三个问题sum i为什么在if里面。这决定了你求的是“能被3整除的数之和”还是“1到100的总和”。if的作用域就是它后面那个大括号sum i写在括号内只有条件成立才执行。如果把大括号去掉、只留一行行为是一样的但强烈建议保留大括号——加一行调试打印的时候很容易忘了补括号然后printf跑在if外面人就懵了。2.3 先用等差数列把答案算出来做基准在跑代码之前先手算一遍。1 到 100 里能被 3 整除的数是 3, 6, 9, ..., 99这是一个等差数列首项 a₁ 3公差 d 3末项 aₙ 99。项数怎么算从 3 到 99一共是 99 ÷ 3 33 项。等差数列求和公式是S n × (a₁ aₙ) / 2代入33 × (3 99) ÷ 2 33 × 102 ÷ 2 33 × 51 1683。还有一个等价算法3 6 ... 99 3 × (1 2 ... 33)而 1 加到 33 是 33 × 34 ÷ 2 561乘 3 得 1683。两种算法互相印证说明这个基准是可靠的。这个基准的价值在于它完全不依赖你的代码。代码输出 1683说明大概率对了输出别的数你都不用调试直接知道有问题。更进一步如果输出 1584你会发现它正好等于 1683 − 99那问题就定位到“漏了 99”上直接去查循环边界比盲目加打印快得多。能通过差值反推错误位置是排查基础题最有效的一招。3. 核心细节取模、整数除法与类型选择3.1%到底在算什么负数取模为什么会咬人取模运算可以理解成“分东西看剩下几个”。你有 17 颗糖每人分 3 颗能分给 5 个人剩 2 颗所以17 % 3 2。能被整除就是“刚好分完不剩”余数为 0。这个类比对理解正数取模足够了。负数是另一回事。C99 标准规定取模结果的符号跟被除数一致。所以-7 % 3等于-1不是 2。这道题里i从 1 开始不会碰到负数但如果你把题目改成“求 -100 到 100 之间……”i % 3 0依然是对的因为 −99、−96 这些数取模确实得 0但如果你用i % 3 2之类的写法去筛“余2”的数负数部分就会全错。这个知识点在这道题里用不上但顺手记住以后处理环形缓冲区、哈希取模、日期计算的时候迟早会遇到。还有人会问%和除法有什么区别能不能用i / 3 * 3 i来判断整除可以逻辑上等价但可读性差很多而且当i是负数或者涉及溢出时行为更微妙。判断整除%是最直接的表达没有理由绕。3.2int够不够把溢出边界算清楚这道题的结果是 1683int的上限在 32 位环境下是 2147483647差着好几个数量级绝对安全。但“这道题安全”不等于“这类题安全”我建议你把溢出边界算一遍养成习惯。假设题目改一下求 1 到 10⁹ 之间所有能被 3 整除的数之和。项数是 10⁹ ÷ 3 ≈ 3.33 亿首项 3末项接近 10⁹和大约是 3.33×10⁸ × 5×10⁸ ≈ 1.67×10¹⁷。这个数字远远超过int的上限约 2.1×10⁹也超过了 32 位unsigned int的上限约 4.3×10⁹必须用long long至少 64 位上限约 9.2×10¹⁸。如果用int硬算结果会回绕成一个负数或者小正数看起来像“算错了”实际是溢出。判断要不要升类型我一般用这个粗算方法估算结果的最大可能值跟目标类型的上限比留一个数量级的安全余量。估算的时候不用精确数量级对就行。这道题上限 1683int绰绰有余一旦题目里的 n 上了百万级别就该考虑long long了。类型典型位宽上限约能安全承载的求和规模int32 位2.1×10⁹1 到 10⁵ 量级的求和unsigned int32 位4.3×10⁹略大一些但不建议拿来存和long32/64 位视平台不确定跨平台代码里别用long long64 位9.2×10¹⁸1 到 10⁹ 量级的求和表里特意标了long是“不确定”因为它在 Windows 上是 32 位在 Linux 64 位上是 64 位。写跨平台代码的时候需要确定宽度就用long long或者stdint.h里的int64_t别用long赌运气。3.3 为什么不该用浮点数来求和偶尔能看到有人把sum定义成float或double理由是“求和嘛浮点更通用”。在这道题上这么写不会报错输出甚至也是 1683但这是个坏习惯。浮点数用二进制表示小数像 0.1 这种值本身就存不准累加次数一多误差会累积。这道题里加的全是整数且数值都不大双精度能精确表示所以看不出问题但换成“求 1 到 10⁷ 之间……之和”结果会超出双精度能精确表示整数的范围2⁵³末几位就开始飘。更实际的问题是浮点数输出要处理格式printf(%f)打印出来是1683.000000还得截断比较大小的时候要用误差范围而不是。为了一个纯整数的求和问题引入这些麻烦完全不划算。凡是参与运算的量都是整数、且结果也是整数的场景就用整型。这是条很硬的规矩。4. 从“能跑”到“写得好”三种写法对比4.1 写法A全遍历加取模教学首选就是第 2 节那个版本。它的优点是跟题面一一对应题面说“1到100”“能被3整除”“求和”代码就是遍历 1 到 100、判断能否被 3 整除、累加。一一对应的好处是可读性极高任何人扫一眼就知道在干什么不需要额外注释。它的代价是要跑 100 次循环、做 100 次取模。在这个规模下完全无所谓但如果这道题是某个大程序里的热点路径被调用几百万次那 100 次迭代和 33 次迭代的差距就会被放大。不过我要说清楚在教学和面试场景下这种写法是最该先写出来的因为它把三个动作完整暴露出来改题、加条件都很方便。追求性能是后一步的事别一上来就为了省几次循环把代码写成别人看不懂的样子。4.2 写法B从 3 开始、步长 3工程里更常见既然只有 3 的倍数会被选中那干脆不遍历非倍数直接从 3 开始每次加 3int sum 0; int i; for (i 3; i 100; i 3) { sum i; }循环体从 100 次降到 33 次而且省掉了if和取模。这段代码短、快、意图明确——“把所有 3 的倍数加起来”读起来比写法 A 还顺。但步长法有个前提起点必须落在第一个有效值上。这里起点是 3因为 1 到 100 里第一个能被 3 整除的正整数就是 3。如果题目改成“求 5 到 100 之间能被 7 整除的数之和”第一个有效值是 7你还得算一下 5 往上取整到 7 的倍数是多少——也就是((5 7 - 1) / 7) * 7这种向上取整的写法很容易写错。所以步长法更适合“从 1 开始”或者“起点恰好是倍数”的场景起点不规整的时候老老实实全遍历加取模反而更稳。另外提一句性能上的真相现代编译器对i % 3这种除数是常量的取模会优化成乘法加移位根本不做除法指令。所以写法 A 和写法 B 在这道题上的实际运行时间差别小到测不出来。选哪个主要看可读性和扩展性不是看那几十纳秒。4.3 写法C等差数列公式常数时间既然是等差数列那就可以直接套公式一次乘除就出结果连循环都不需要int n 100; int k 3; int count n / k; /* 100 / 3 33项数 */ long long sum (long long)k * count * (count 1) / 2;算一下count 333 × 33 × 34 ÷ 2 3 × 561 1683和循环版本一致。这个写法的时间复杂度是 O(1)跟 n 多大完全无关n 是十亿也一样快。公式法有两个必须注意的地方。第一整数除法的截断方向。n / k向下取整对于“不超过 n 的最大 k 的倍数”这个语义刚好是对的。如果 n 100、k 333 就是项数如果 n 2、k 32 / 3得 0项数为 0和为 0边界自动正确。这个性质很漂亮不用额外判断。第二乘法顺序要防溢出。k * count * (count 1)在 count 很大时会先炸掉所以要么把其中一个操作数转成long long让它整体按 64 位算要么调整顺序先除以 2。我一般直接把(long long)k放在最前面靠类型提升把后面全部拉到 64 位简单粗暴不烧脑。4.4 三种写法怎么选写法循环次数可读性扩展性适用场景A 全遍历 取模100最高最好教学、面试、条件复杂时B 步长法33高一般起点是倍数、追求直观高效C 公式法0中差n 极大、需要反复调用我的实际选择是默认写 A确认瓶颈在 B 或 C 再换。绝大多数业务代码里这道题的规模根本构不成瓶颈为了省 67 次循环把代码变得不好懂是典型的过早优化。但如果是嵌入式环境、或者这段逻辑在大循环里被调用上万次那 B 和 C 就值得了。判断标准很简单这段代码跑多少次如果只有一次怎么清楚怎么写。顺带说一个选型上的坑写法 C 虽然快但它不能直接推广到“能被3或5整除”这种条件。因为或的条件没有闭式公式得用容斥原理拆成三个等差数列再加减而写法 A 只要把if里的条件改成i % 3 0 || i % 5 0就完事了。所以公式法是个特化技巧不是通用解法心里要有数。5. 把它写成能复用的函数5.1 抽出sum_of_multiples函数题目只要求一百以内的 3 的倍数但真实写代码的时候这类逻辑往往要改参数反复用。与其每次复制粘贴改数字不如抽成一个函数#include stdio.h /* 求 1..n 之间所有能被 k 整除的正整数之和 */ long long sum_of_multiples(int n, int k) { long long sum 0; int i; if (n 0 || k 0) { return 0; } for (i k; i n; i k) { sum i; } return sum; } int main(void) { printf(%lld\n, sum_of_multiples(100, 3)); return 0; }几个设计上的取舍值得说一下。返回类型用long long而不是int是为了让这个函数在 n 变大时不至于突然失效调用方拿到大结果也不会溢出。参数校验里k 0必须拦住因为i k在 k 为 0 的时候会变成死循环k 为负数的时候循环条件方向反了也会出问题。n 0直接返回 0避免无意义的循环。函数内部用的是步长法而不是全遍历因为函数名叫“求能被 k 整除的数之和”从 k 开始步长 k 是最自然的表达。注意起点写的是i k而不是i 1这要求 k 本身是正整数——参数校验已经保证了这一点所以安全。5.2 参数校验该做到什么程度参数校验容易走两个极端一种是什么都不校验函数拿到脏数据直接崩另一种是把校验写得比业务逻辑还长。我的经验是只拦会导致错误结果或死循环的输入其余的交给调用方。这个函数里k 0会导致死循环k 0会让循环条件和累加方向都错n 0会导致没有有效区间这三个都是必须拦的。至于n是不是特别大、k是不是大于n前者由返回类型兜底后者循环自然一次不进返回 0不需要额外写判断。还有一种做法是用assert而不是if返回。区别在于assert是开发者契约表示“调用方不应该传这种值传了就是 bug”if返回 0 是运行时兜底表示“我容错你随便传”。公共接口用后者内部函数用前者。5.3 用断言把测试用例固化下来手算基准只能验证一个输入而函数一旦参数化就得验证多个输入。最省事的办法是用assert写一组用例编译跑一遍全过就静默退出#include assert.h int main(void) { assert(sum_of_multiples(100, 3) 1683); /* 本题答案 */ assert(sum_of_multiples(3, 3) 3); /* 只有一个有效值 */ assert(sum_of_multiples(2, 3) 0); /* 没有有效值 */ assert(sum_of_multiples(0, 3) 0); /* n 非法 */ assert(sum_of_multiples(100, 0) 0); /* k 非法防死循环 */ assert(sum_of_multiples(100, 1) 5050); /* 1 到 100 总和 */ assert(sum_of_multiples(100, 100) 100); /* 只有一个有效值在边界 */ printf(all tests passed\n); return 0; }这组用例的设计思路是覆盖边界而不是覆盖数量。n 3和n 2卡的是“有没有第一个有效值”这条线n 100, k 100卡的是“有效值刚好落在右边界”这条线k 0卡的是死循环k 1用 5050 这个全世界都知道的答案做对照一旦 5050 不对说明累加逻辑本身有问题而不是筛选逻辑有问题。这样七条断言就能把大部分改坏的情况拦下来。注意assert在定义NDEBUG宏之后会被编译掉。发布版本如果靠断言做校验校验会静默消失。所以断言只用来做开发期自测不要拿它代替业务上的参数检查。6. 常见错误清单与排查实录6.1 高频错误速查表下面这张表是我这些年见到的、在这道题上真实出现过的错误按出现频率排。你可以拿自己的代码对一遍。现象可能原因定位方法输出 5050sum i写在了if外面看缩进和大括号位置输出 0sum没初始化或条件是if (i % 3 1)之类永假式打印sum初值检查条件表达式输出 1584循环条件写成i 100漏了 991584 99 1683差值正好是末项输出 1782循环条件写成i 102或者上限写错1782 − 1683 99多算了一项输出 3条件是i / 3 0只在 i 为 1、2 时成立输出一堆数printf在循环内看输出行数是不是 33 行卡住不动for后面多写了分号或者i 3写成i 0看循环变量的更新语句输出负数或大得离谱sum类型太小导致溢出估算结果量级换成long long编译报错 undeclared用了i但没声明或for内声明用了 C89 编译器把int i提到函数开头表里最值得说的是“输出 1584”这一行。很多人调这个错的时候会去改if条件、改取模、加打印绕一大圈。其实只要做一次减法1683 − 1584 99而 99 正好是能被 3 整除的最大数立刻就知道是上限没取到。差值分析是排查求和类问题的第一手段比单步调试快得多。6.2 三个真实踩坑场景场景一循环体被一个分号吃掉了。有人写for (i 1; i 100; i);然后下面接一个大括号包着if和sum i。这个分号让for的循环体变成了空语句循环跑完 100 次i变成 101然后才执行大括号里的代码——只执行一次判断 101 能不能被 3 整除不能sum保持 0。这个错误的隐蔽性在于代码“看起来很整齐”缩进完全正常编译器也不会报错有些编译器会给个空循环体的警告。防的办法是循环体永远加大括号即使只有一行并且在编辑器里把行尾空白显示出来。场景二sum定义在了循环里面。有人图省事写成for (...) { int sum 0; if (...) sum i; }然后在循环外面printf(%d, sum)。这行printf直接编译不过因为sum的作用域只在循环体内。如果他把printf也挪进循环就会输出 33 行每行是单个倍数。这类问题本质上是作用域没搞清楚解决办法很简单跟“累积”有关的变量一律定义在最外层跟“单次迭代”有关的变量才放循环里。场景三把“能被3整除”理解成“除以3等于0”。前面提过这个错误来自中文语义和代码语义的错位。“能被3整除”在数学上意思是“3 能整除它”也就是它除以 3 余 0而在代码里i / 3 0表达的是“i 除以 3 的商是 0”也就是 i 比 3 小。这两个意思差了十万八千里。避免这种错位的方法只有一个把中文条件逐字翻译成“余数为0”这种可执行的表述再写代码。翻译过程中一旦发现翻不过去说明理解有问题先停下来想清楚。6.3 我常用的三步排查法遇到这类求和题结果不对我一般按这个顺序查基本三轮内能定位。第一步先验证范围。把if去掉直接求 1 到 100 的和看是不是 5050。是说明循环边界和累加逻辑都没问题错在筛选条件不是说明问题在循环本身先修循环。这一步把问题空间一刀切成两半。第二步缩小规模。把上限从 100 改成 10手动列出 3、6、9期望和是 18。规模一小你可以把每次迭代的i和sum都打印出来一眼扫完不用盯一百行输出。第三步打印关键中间量。在if内部打一行printf(hit: %d, sum%d\n, i, sum);看命中的数是不是 3、6、9 这样递增看sum的增量是不是等于当前i。如果命中的数里混进了 4、7 这种问题在条件如果sum的增量不等于i问题在累加语句。这三步的共同点是每一步都在缩小可疑范围而不是盲目改代码。新手最容易犯的错是“感觉这里不对改一下试试”试了十次都不知道自己在试什么。有明确的假设和验证手段比试错快十倍。7. 一道题能长出多少变化7.1 同时被 3 和 5 整除条件怎么写最稳最直接的改法是把条件换成i % 3 0 i % 5 0逻辑上完全正确1 到 100 里满足的数有 15、30、45、60、75、90和是 315。但更地道的写法是i % 15 0因为“同时被 3 和 5 整除”等价于“被 15 整除”3 和 5 互质最小公倍数是 15。两种写法结果一样后者少做一次取模。选哪个看你的目的是什么。如果是为了表达“两个条件都要满足”这个语义写更清楚将来要加第三个条件也方便如果是为了性能或者代码简洁写 15 更好但得在注释里说明 15 是 3 和 5 的最小公倍数否则接手的人得自己想一下。我倾向于写 15 加一行注释因为这类“最小公倍数”的推导是数学常识写清楚就行不用每次都让 CPU 算两遍。7.2 被 3 或 5 整除容斥原理不能忘把改成||问题就有意思了。1 到 100 里能被 3 或 5 整除的数之和是多少不能简单地把“3 的倍数之和”加上“5 的倍数之和”因为 15 的倍数被算了两次。正确的做法是容斥3 的倍数之和 16835 的倍数之和5, 10, ..., 100共 20 项20 × (5 100) ÷ 2 105015 的倍数之和15, 30, ..., 90共 6 项6 × (15 90) ÷ 2 315结果 1683 1050 − 315 2418。用代码验证的话条件写(i % 3 0) || (i % 5 0)跑出来应该也是 2418。这个例子特别好因为它揭示了一件事用循环暴力求和天然不会重复计数用公式拆分就必须自己处理重叠。所以在条件变复杂的时候暴力循环反而是更不容易错的选择这也是我前面说“默认写写法 A”的原因之一。7.3 换成 while、do-while 和递归同一个逻辑换控制结构写一遍是很好的练习能帮你理解“循环三要素”初值、条件、更新到底在哪。while版本int i 3; int sum 0; while (i 100) { sum i; i 3; }do-while版本int i 3; int sum 0; do { sum i; i 3; } while (i 100);do-while的特点是先执行一次再判断所以哪怕初始条件不满足循环体也至少跑一次。在这道题上因为初始i 3一定满足i 100两者结果一样。但如果把上限改成 2while版本一次都不进sum是 0do-while版本会先加一次 3sum变成 3然后退出。这就是do-while的陷阱它不适合“可能一次都不该执行”的场景。判断该用哪个就看“零次执行”是不是合法状态。求和问题里区间为空是合法的所以while更合适。递归版本也贴一下主要是为了理解递归和循环的对应关系long long sum_rec(int i, int n, int k) { if (i n) { return 0; } return i sum_rec(i k, n, k); }调用sum_rec(3, 100, 3)得到 1683。递归写法优雅但没必要33 层调用栈虽然不深但每层都有函数调用开销而且 n 变大时有栈溢出风险。这道题用递归纯属练手工程里不推荐。7.4 把上限和除数都做成参数之后一旦把n和k都参数化这道题就变成了一个通用工具函数能回答很多问题1 到 1000 里 7 的倍数之和、1 到 50 里 4 的倍数之和都是一行调用的事。这时候前面的测试用例就体现出价值了——你改的是参数逻辑没动如果某个用例挂了说明你在改造过程中动坏了什么而不是“本来就不对”。再往前一步可以把结果范围也放宽求[lo, hi]区间内 k 的倍数之和等价于sum_of_multiples(hi, k) - sum_of_multiples(lo - 1, k)。这个“前缀和相减”的思路在算法题里非常常见一次封装就能把区间求和的问题全部覆盖掉。这就是把一道课后题做成工具的价值你解决的不是这一个数而是一类问题。我个人在实际写这类小工具的时候最大的体会是真正花时间的从来不是写那几行循环而是想清楚边界、想清楚类型、想清楚零次执行算不算合法。这三个问题想明白了代码敲下去基本一次就对想不明白代码写得再工整也是靠运气在跑。所以现在我再遇到这种“太简单”的题反而会多花两分钟先把 1683 手算出来再把边界值列出来最后才动手。这两分钟省下的调试时间往往不止两分钟。
RELATED

相关推荐

信息流产品设计:从算法原理到用户粘性优化

信息流产品设计:从算法原理到用户粘性优化

1. 信息流产品的底层逻辑:从工具到时间黑洞2006年Facebook首次推出News Feed功能时,用户曾发起大规模抗议活动,认为这种未经许可的内容推送侵犯了隐私。但今天,信息流已成为所有社交产品的标配,我们平均每天要滑动屏幕…

📅 2026/11/2 4:28:01
GitHub热门项目推荐系统:技术评估与精选解析

GitHub热门项目推荐系统:技术评估与精选解析

1. 项目概述"每日GitHub热门项目推荐 | 2026年3月8日"是一个持续更新的技术资源聚合项目,旨在为开发者筛选GitHub平台上当日最具价值的开源项目。不同于简单的排行榜,这个推荐系统通过多维度的技术评估和社区活跃度分析,帮助开发者…

📅 2026/11/2 4:28:01
算法岗二面秒挂真相:项目深挖、手撕代码与场景题拆解

算法岗二面秒挂真相:项目深挖、手撕代码与场景题拆解

上周和一位做算法的朋友吃饭,他端着碗说自己投了某头部大厂算法岗,一面聊得挺顺,二面进去不到四十分钟就被请了出来,第二天一早收到流程结束的邮件,从面试到出结果不到四十八小时。他一边扒饭一边感慨:效率…

📅 2026/11/2 4:28:01
MORE NEWS

更多资讯

📰

你的迭代器为何总在“第二次”失灵?——Python iter() 与 next() 的协议真相与完全驾驭手册

你的迭代器为何总在“第二次”失灵?——Python iter() 与 next() 的协议真相与完全驾驭手册 在 Python 中,for 循环几乎无处不在,但支撑它运转的底层机制——iter() 和 next()——却常常被开发者忽略。你可能已经习惯写 for x in data:&#…

📰

【八个月网安课程】第十三周·周五:命令拼接符号、管道符与长度限制绕过

在掌握了命令执行函数后,今天将聚焦于如何突破命令字符串本身的限制:当分隔符被过滤时,如何用其他符号替代;当命令长度受限时,如何通过精巧的构造在极短空间内实现完整攻击。这是命令注入从理论走向实战的关键一步&…

📰

基于 LSTM 的车载 CAN 总线入侵检测:从 125 万条真实数据的“毒打”到工程化落地

一、项目背景与真实数据的挑战 智能网联汽车里,CAN 总线是连接各个 ECU 的核心网络。不过 CAN 总线在设计之初极度缺乏安全认证与加密机制,很容易遭到黑客的 DoS、重放、模糊测试等攻击。 这个项目主要是利用深度学习(LSTM)的时序…

📰

为什么电池测试结果会出现噪声和波动?到底是电池问题还是设备问题?

现场最让设备工程师头疼的情况之一就是:“数据有点飘。”它可能表现为:电压抖动。电流波动。曲线出现尖峰。不同通道数据不一致。那么到底是什么原因?一、首先要考虑测试对象本身电池并不是理想元件。在充放电过程中,它本身就可能…

📰

Python 数据处理进阶(二十四)把脚本变成工具:argparse、logging 与定时任务

写在前面 这是第二批的最后一篇。前 23 篇把「怎么处理数据」讲完了,这一篇讲怎么让脚本被别人用起来。 写给自己用的脚本,改参数、print 调试、手动运行都没问题;但只要交给同事、或者要每周自动跑,就必须补三件事:参…

📰

猫抓 cat-catch 完整教程:5 分钟嗅探网页视频并下载 M3U8

猫抓 cat-catch 完整教程:5 分钟嗅探网页视频并下载 M3U8 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓 cat-catch 是一款开源浏…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬