最近在给一位刚学编程的学员做作业辅导时,翻到一道非常典型的入门题:给定一个整数数组,求所有元素之和,同时把元素按奇偶分类统计。题目本身看着很简单,可实际写起来,我发现大多数人不是不会写循环,而是没有从“遍历—判断—累加”这条主线去理解问题。有人用内置求和方法算完总和,再单独写一遍循环做分类,白白遍历两次;有人在判断负数奇偶时写出了错误结果;还有人完全不设计测试用例,空数组直接让程序崩溃。
别小看这道题。它背后串着遍历框架、条件分支、累加变量、边界测试四条线,面试里也常拿它当热身题。这篇就把“数组元素求和与奇偶分类”这道例题彻底拆开讲透,覆盖求和写法、奇偶判定、完整实现、测试用例设计和几个常见变体。只要你把这一道题吃透,后面学排序、二分查找、前缀和都会顺很多。
1. 这道例题的隐藏考点:一次遍历搞定两件事
1.1 为什么求和和分类总被放在同一道题里
很多初学者觉得“求和”和“奇偶分类”是两个独立的知识点,放在一起只是老师随手拼的。其实它们共享同一个核心机制:遍历数组。
求和的本质是:依次访问每个元素,把它累加到一个变量里。奇偶分类的本质是:依次访问每个元素,用一个条件判断决定“这个值该进哪个统计变量”。所以这个例题真正考察的是,你能不能意识到“一次遍历可以做多件事”。
我见过一些学员的代码,写成这样:
- 第一遍循环求总和。
- 第二遍循环判断偶数,累加偶数和。
- 第三遍循环判断奇数,累加奇数和。
功能是对的,但遍历了三遍。虽然时间复杂度仍然是 O(n),常数项变大,代码却明显冗余。更关键的是,这种写法说明写代码的人没有把握住“遍历过程中每一次都能同时做多种操作”这个思想。
正确的思考方式是:遍历的每一次迭代,都是一个“加工节点”。在这个节点上,你可以累加总和,可以判断奇偶,可以更新计数,也可以把符合条件的值放进另一个数组。这些操作彼此独立,完全可以放在同一个循环体里完成。
1.2 动手之前先画一遍“纸上演算”
我辅导学员时,总是要求他们先把程序“人肉执行”一遍,再写代码。就拿一个 5 元素的小数组举例。
假设数组是[5, -2, 0, 3, 8]。在纸上画一张表,每一行记录遍历到第几个元素时的状态:
| 当前元素 | 总和 | 偶数和 | 奇数和 | 偶数个数 | 奇数个数 |
|---|---|---|---|---|---|
| 初始 | 0 | 0 | 0 | 0 | 0 |
| 5 | 5 | 0 | 5 | 0 | 1 |
| -2 | 3 | -2 | 5 | 1 | 1 |
| 0 | 3 | -2 | 5 | 2 | 1 |
| 3 | 6 | -2 | 8 | 2 | 2 |
| 8 | 14 | 6 | 8 | 3 | 2 |
这张表走完,你对变量的变化过程就有了直观感觉。调试的时候,只要把程序输出和这张表比对,哪里出了问题一眼就能看出来。
这个习惯看似笨拙,其实是程序员的基本功。真正遇到复杂 bug 的时候,你总不能全靠 log 输出,而是要在脑子里模拟数据流动。一开始就把“纸上演算”练熟,后面写任何循环代码都稳得多。
2. 求和不是背代码:for、while、内建函数三种写法怎么选
2.1 for 循环:最稳的通用写法
先说最通用的 for 写法。以 Python 为例:
total = 0 for x in arr: total += x这段代码大家都会写,但我想强调两点。
第一,这里用的是for x in arr,直接遍历元素值,而不是用下标for i in range(len(arr))。如果只求和,直接遍历元素更简洁、更不容易出错。如果用下标,还得每次写arr[i],代码可读性差一些。
第二,累加变量total一定要在循环开始前初始化。新手经常忘记,导致第一次累加时使用一个不存在的变量。
如果题目要求未来可能要改“每隔两个元素求和”,那么用下标遍历会更合适。但就本题而言,直接遍历元素是最自然的选择。
2.2 while 循环:什么时候它更合适
while 写法是这样的:
i = 0 total = 0 while i < len(arr): total += arr[i] i += 1这种写法比 for 啰嗦,但它最大的优势是手动控制下标。当你需要在循环体内跳过某些元素、修改循环步长、甚至根据条件回退时,while 更灵活。
不过要注意一个最常见的坑:忘记写i += 1。忘写的后果就是死循环。初学阶段我建议少用 while,先用 for 把逻辑练熟,等确实遇到“需要复杂下标控制”的场景再切到 while。
2.3 内建求和函数:省事,但要分清场景
Python 里有现成的sum(arr),一行代码就得到总和。问题在于,本题还要做奇偶分类。如果你先用sum(arr)求总和,再另写一个循环做分类,虽然代码也能跑,但白白遍历了两遍。
更好的做法是:在同一个循环里既累加总和,也累加偶数和、奇数和。这样一次遍历全部搞定。
假如题目改成“只求和,不做任何分类”,那我会毫不犹豫用sum(arr)。工具要选对场景,没有绝对的最优写法,只有适合当前问题的写法。
2.4 三种写法的取舍表
| 写法 | 代码量 | 可读性 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| for 遍历 | 中等 | 好 | 中等 | 大多数数组遍历 |
| while 遍历 | 较多 | 一般 | 高 | 需要手动控制下标、步长 |
| 内建 sum() | 最少 | 极好 | 低 | 只求和、不做分支处理 |
三种写法的时间复杂度都是 O(n),额外空间基本都是 O(1)。所以性能上没有本质差别,差别全在代码表达力和维护性上。
如果你刚开始学,建议先把 for 循环版练熟。它能应对绝大多数场景,也是后续理解排序和搜索算法的基础。
3. 奇偶判定里最容易翻车的三个细节:负数、零和位运算技巧
3.1 负数取模:教科书里没讲透的坑
判断奇偶,很多教材会这么教:x % 2 == 0是偶数,x % 2 == 1是奇数。
前半句没问题,后半句在负数场景下就有隐患。以 C 语言为例,负数的余数符号与语言实现有关。-3 % 2的结果在很多 C 编译器里是-1,而不是1。这时候用x % 2 == 1判断奇数,就会把-3错误地排除掉。
Python 的情况又不同,-3 % 2的结果是1,因为 Python 的取模结果与除数符号一致。也就是说,同样一行判断,换一种语言结果可能完全不一样。
最稳妥的写法是:
- 判断偶数:
x % 2 == 0 - 判断奇数:
x % 2 != 0
用“不等于 0”而不是“等于 1”,无论正负都正确。这是一个值得记住的通用经验。判断一个属性的“否定面”,用“不等于”往往比“等于”更安全。
3.2 0 的归属和边界用例
0 % 2 == 0,所以 0 属于偶数。这一点多数人都知道,但写测试用例时却常常忽略。
我在辅导中见过一位学员,用数组[1, 3, 5]测程序,结果正确;换成[0]就报错,因为他没有对空数组做保护。还有人在分类时用了两个独立的 if,而不是 if-else,导致 0 被同时算进偶数和其他分支,统计数据全乱了。
0 是测试用例里性价比最高的元素之一,成本低,却能同时验证“边界值处理”和“分支结构”是否正确。以后你测试任何条件判断,都先往用例里塞一个 0。
3.3 位运算:用二进制最低位判断奇偶
除了取模,还有一种更底层的判断方式:看二进制的最低位。
一个整数是偶数,意味着它的最低位是 0;是奇数,最低位是 1。所以:
if x & 1 == 0: # 偶数 else: # 奇数x & 1是按位与运算,只保留二进制最低位的值。这种写法在底层性能要求高的场景里更快,但对入门学习者来说,取模写法已经足够。
我之所以提它,是因为很多企业面试或竞赛题里,会有人用位运算判断奇偶。你至少要知道别人这么写是什么意思,不至于看到& 1就发懵。实际开发里,代码可读性通常优先于微小的性能差异,所以取模写成x % 2 != 0完全够用。
4. 一道完整例题的演练:输入、输出、用例与复杂度
4.1 题目完整描述
把前面的讨论汇总成一道完整的例题:
给定一个整数数组
nums,请计算并输出:
- 所有元素之和;
- 所有偶数元素之和;
- 所有奇数元素之和;
- 偶数元素的个数;
- 奇数元素的个数。
这道题的设计合理性在于:总和等于偶数和加奇数和,这个关系可以作为程序正确性的一个验证手段。
4.2 参考实现:一次循环完成所有统计
def analyze_array(nums): """ nums: List[int] 返回: (total, even_sum, odd_sum, even_count, odd_count) """ total = 0 even_sum = 0 odd_sum = 0 even_count = 0 odd_count = 0 for x in nums: total += x if x % 2 == 0: even_sum += x even_count += 1 else: odd_sum += x odd_count += 1 return total, even_sum, odd_sum, even_count, odd_count注意这里用的是if-else,而不是两个独立的if。因为一个整数非偶即奇,用了if-else以后,每个元素只进一个分支,统计结果不会重复。
你可能会问:为什么不把total += x也放进分支里?因为total需要涵盖所有元素,放在分支外面更直观,也不会重复累加。
如果你用的是 C 语言或 Java,循环写成:
int total = 0; int evenSum = 0, oddSum = 0; int evenCount = 0, oddCount = 0; for (int x : nums) { total += x; if (x % 2 == 0) { evenSum += x; evenCount++; } else { oddSum += x; oddCount++; } }结构完全一样,只是语法不同。
4.3 测试用例设计:空数组、全偶、全奇、负数混合
这是我觉得这道题最有价值的部分。大多数初学者写完代码就跑一遍示例数组,发现结果“差不多”就交作业了。但真正专业的做法是,用一组有代表性的用例把代码的边界情况都打一遍。
| 输入数组 | 期望总和 | 偶数之和 | 奇数之和 | 偶数个数 | 奇数个数 | 验证意图 |
|---|---|---|---|---|---|---|
[] | 0 | 0 | 0 | 0 | 0 | 空数组保护 |
[0] | 0 | 0 | 0 | 1 | 0 | 0 属于偶数 |
[2, 4, 6] | 12 | 12 | 0 | 3 | 0 | 全偶数组 |
[1, 3, 5] | 9 | 0 | 9 | 0 | 3 | 全奇数组 |
[-4, -3, -2, -1] | -10 | -6 | -4 | 2 | 2 | 负数奇偶判定 |
[5, -2, 0, 3, 8] | 14 | 6 | 8 | 3 | 2 | 混合场景 |
每跑完一组用例,都顺便验证一下total == even_sum + odd_sum。如果相等,说明累加逻辑没有遗漏;如果不相等,说明某个分支出了问题。这就是我在后面会讲的“不变量校验”思想。
4.4 时间与空间复杂度
这个算法对每个元素做一次访问和常数次运算,所以时间复杂度是 O(n),其中 n 是数组长度。
额外空间方面,除了若干统计变量,没有创建新的数组或大规模数据结构,所以空间复杂度是 O(1)。这是“一次遍历”方案最突出的优点:时间上不浪费,空间上也不浪费。
对比一下“先 sum 再循环分类”的做法:如果只用sum()求总和,再单独分类,时间上是两次遍历,仍然是 O(n),但常数更大;如果分类时要分别创建偶数组和奇数组,空间就会变成 O(n)。初学者往往意识不到这些差别,等处理几十万条数据时,差别就会变得很明显。
5. 做完别急着交:五个变体把知识点吃透
5.1 变体一:只统计奇偶个数,不求和
如果题目改成“只输出奇数和偶数的个数”,我们只需要保留 count 变量,去掉和相关的累加。这个变体几乎没有难度,但它提醒你:同一套遍历框架,统计目标和累加目标可以自由组合。
很多入门练习题就是从这类小改动开始的。你别觉得简单就跳过,快速敲一遍,能加深对变量作用的印象。
5.2 变体二:把偶数移到数组前部
这是奇偶分类题目中非常经典的扩展:给定数组,要求把所有偶数放在前半部分,奇数放在后半部分,不要求保持原有顺序。
常见做法是双指针:
def move_evens_front(nums): left = 0 right = len(nums) - 1 while left < right: while left < right and nums[left] % 2 == 0: left += 1 while left < right and nums[right] % 2 != 0: right -= 1 if left < right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1 return nums这个变体用到了“从两边向中间扫描”的思路,和快速排序的 partition 操作很像。它不再关心总和,而是专注于“分类后进行位置交换”。如果你能把这道变体写出来,说明你已经从“会遍历”进步到“会利用下标控制元素位置”。
5.3 变体三:把偶数和奇数分别存入两个新数组
有时候题目要求输出两个数组,比如所有偶数和所有奇数。实现时要用两个空列表,遍历时用append分别追加。
evens = [] odds = [] for x in nums: if x % 2 == 0: evens.append(x) else: odds.append(x)很简单,但代价是额外空间变成 O(n)。这类写法很适合用来讲解“空间换时间”的概念。虽然这里时间没有变少,但理解额外空间的来源很重要。
5.4 变体四:统计偶数下标位置的元素和
把“判断元素奇偶”换成“判断下标奇偶”,就出现了一个新题目:
计算所有偶数下标位置的元素之和。
写法是把if x % 2 == 0改成if i % 2 == 0,同时要改用下标遍历:
total_even_index = 0 for i in range(len(nums)): if i % 2 == 0: total_even_index += nums[i]这个变体告诉你,“奇偶分类”的对象不一定是数组元素,也可以是下标。很多算法题里,下标本身就是一种信息,例如“求交替和”“求波峰波谷”,都离不开下标参与判断。
5.5 变体五:利用“和关系”做自动校验
前面提过total == even_sum + odd_sum。把它写进程序,成为自动化测试的一部分,是一种简单有效的“不变量校验”思想。
很多初学者觉得测试就是“多 print 几次”,但真正的工程实践里,我们会把校验逻辑做成断言。例如:
assert total == even_sum + odd_sum跑完主体逻辑后,如果断言失败,程序会立刻报错。这种方式比肉眼看输出靠谱得多,因为人的眼睛很容易忽略细微错误。把这个习惯保留到以后写复杂算法里,能帮你尽早发现系统性错误。
我个人辅导学员时,发现一个普遍现象:大家能写出主循环,但很少主动设计边界测试和量关系校验。这其实是编程思维从“能跑”向“可靠”转变的关键一步。把这道例题的五种变体都敲一遍,你对数组遍历和条件分支的理解会上一个台阶。
我做编程辅导这几年,发现最影响学习进度的不是语法,而是“不验证自己在做什么”。很多学习者习惯写完代码直接看结果,结果对就算完成,结果不对就开始乱改。真正稳妥的做法,是先在纸上走一个最小数组,再用边界用例去验证逻辑,最后用“总量等于分量之和”这类不变量做校验。整个过程多一点耐心,后面调任何 bug 都快很多。
如果你刚学完这道题,不妨把它当作起点。先按第 4 节的完整实现写通,然后用第 3 节的边界用例测一遍,最后挑两三个变体练习。等你能不查资料把“求和 + 奇偶分类 + 偶数前置”这三个版本都写出来,你的数组基本功就算是真正打牢了。