ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数组求和与奇偶分类:遍历、累加与边界处理的完整攻略

数组求和与奇偶分类:遍历、累加与边界处理的完整攻略 数组求和的那些题从上学考到面试从C语言考到Python本质上翻来覆去就那几件事遍历、累加、按条件过滤、再分门别类。今天拿这道“数组元素求和与奇偶分类”例题做引子把数组操作里最核心的套路、最容易踩的坑、以及后续会碰到的变形题一次性捋清楚。不管你是刚学编程的新手还是准备笔试面试的老手这题都能帮你把数组的基础功打扎实。很多初学者看到“求和”两个字第一反应就是无脑循环叠加看到“奇偶分类”又开始纠结要不要搞两个新数组。这些思路本身没错但缺乏对题目本质的分析容易陷入“能跑就行”的误区。我见过太多人在这一步跑通了代码却在后续更复杂的题里栽跟头——因为没搞明白求和的核心在于累加器的边界控制分类的核心在于条件判断的覆盖完整性和稳定性。这道题的最好打开方式是把它当成数组入门阶段的一块试金石。它覆盖了数组创建的几种方式、循环遍历的几种写法、条件分支的几种组织方式还隐藏了一个常被忽略的考点——奇偶性在负数场景下如何处理以及分类后是否要保持原有顺序。明白这些后面遇到指针数组、二维数组、字符串数组、动态数组时才能触类旁通。1. 题目拆解与核心考点分析在动手写代码之前先把题目背后的考点摸清楚。数组类题目看着千变万化但出题人真正想考察的无非是三个层面第一你知不知道数组在内存里是连续存储、按下标随机访问的第二你能不能写出正确、不越界、不遗漏的循环遍历第三你能不能根据条件对元素做二次加工——求和也好分类也好本质都是对元素做“有条件”的加工。这道“数组元素求和与奇偶分类”例题恰好把这三个层面全部覆盖了。先说求和它考察的是累加器的正确使用方式。一个常见的入门错误是初始化累加变量时把它写在循环内部每轮都会被重置另一个错误是数组下标从1开始遍历漏掉第一个元素。这些错误都不是语法问题而是对“数组下标从0开始”这一语言特性没有形成肌肉记忆。再说奇偶分类。很多人以为判断奇偶就是num % 2 0这个认知在正整数范围内没问题一旦数组里混入负数就会翻车。比如-3 % 2在C语言和Java里结果是-1而不是1如果代码只写了num % 2 1判断奇数-3就会被漏掉。这是这个例题里最有价值的一个隐藏考点也是面试官最爱挖的坑。所以这道题的正确拆解方式不是“求和怎么做、分类怎么做”而是把它们当成两个子问题分别设计求和阶段用遍历维护累加器分类阶段用条件判断配合“取余结果与0比较”而不是“与1比较”。理清这个逻辑代码写起来既简洁又稳当面试时也能清晰地讲出每一步的意图而不是背模板。1.1 输入与输出的边界设计做数组题目第一件事不是写循环而是先确定输入和输出的边界。拿这道题来说输入是一个整数数组可能为空可能只有一个元素可能含有负数也可能含有重复值。输出则取决于题目要求如果只要求输出总和那输出一个整数如果要求把奇数和偶数分别输出或分别存储那输出两个新数组或两行结果。边界设计直接影响代码的健壮性。空数组求和结果应为0而不是报错或返回垃圾值单元素数组求和结果就是该元素本身奇偶分类时它只会落入其中一个分类。很多编程题在线评测系统如LeetCode、牛客网会特意设置这些边界用例用来筛掉那些“只写了主流程、没考虑特殊情况”的代码。我建议在动手写代码前先写出三组测试用例一组是常规数据如{1, 2, 3, 4, 5}一组是边界数据如{}和{7}还有一组是负数和零混合如{-3, -2, 0, 4}。把这三组用例在脑子里过一遍代码该怎么写基本就清晰了。1.2 复杂度分析为什么选择一次遍历不少人在分类时会想先把数组扫一遍区分奇偶再各扫一遍求和这样思路清晰。这个做法没错但不够优。一次遍历完全可以同时完成求和与分类时间复杂度是 O(n)空间复杂度取决于是否额外创建数组存储分类结果。一次遍历的核心优势在于数组元素在内存中是连续存储的遍历时缓存局部性更好执行效率更高更重要的是把多个操作合并到一次循环里减少了循环控制和边界判断的重复代码出错的概率更低。对于这个例题一次遍历的做法是循环体内同时做“累加到总和”和“判断奇偶加入对应分类”。这里要顺便强调一下空间复杂度的概念。如果分类后需要保留奇数和偶数两组数据供后续使用那必然要开辟额外数组空间复杂度是 O(n)如果只要求打印或求各自的个数那只需要两个计数器空间复杂度是 O(1)。做题前先问自己分类的目的是什么是“需要结果集”还是“只需要统计量”这个问题直接决定了要不要创建新数组也是面试官考核你是否理解数据结构的常用切入点。2. 基础解法一次遍历实现求和与分类理解了核心考点和边界设计代码实现就很自然了。下面我用 C 语言写一版最朴素的实现因为它最接近底层能帮你看清数组操作的每一个细节随后会给出 Java 和 Python 版本方便你对照理解不同语言的差别。#include stdio.h void classify_and_sum(int arr[], int n, int *sum_odd, int *sum_even, int odd[], int *odd_count, int even[], int *even_count) { *sum_odd 0; *sum_even 0; *odd_count 0; *even_count 0; for (int i 0; i n; i) { // 奇偶判断用 % 2 是否等于 0而不是等于 1 if (arr[i] % 2 0) { even[*even_count] arr[i]; (*even_count); *sum_even arr[i]; } else { odd[*odd_count] arr[i]; (*odd_count); *sum_odd arr[i]; } } } int main() { int arr[] {1, 2, 3, 4, 5, -6, -7}; int n sizeof(arr) / sizeof(arr[0]); int odd[100], even[100]; int sum_odd, sum_even, odd_count, even_count; classify_and_sum(arr, n, sum_odd, sum_even, odd, odd_count, even, even_count); printf(总和: %d\n, sum_odd sum_even); printf(奇数之和: %d\n, sum_odd); printf(偶数之和: %d\n, sum_even); printf(奇数数组: ); for (int i 0; i odd_count; i) { printf(%d , odd[i]); } printf(\n偶数数组: ); for (int i 0; i even_count; i) { printf(%d , even[i]); } printf(\n); return 0; }这段代码里有一个细节值得重点讲指针的使用。int *odd_count和int *even_count这类指针参数在C语言里很常用作用是让函数内部修改的值能传递到外部。很多初学者在函数里直接int count 0然后count回到 main 后 count 还是原来的值——这是C语言经典错误传值调用不会改变实参。这里用指针传地址本质上就是在“传递数组元素”之外增加了一个“传递计数器地址”的动作。2.1 奇偶判断的正确姿势取余结果与0比较再强调一次那个值得记一辈子的坑判断偶数要写成num % 2 0判断奇数要写成num % 2 ! 0或者num % 2 1只在非负场景下用。为什么不推荐num % 2 1因为负奇数对2取余的结果是 -1 而不是 1。你可以自己跑一下在 C 语言里执行-3 % 2结果是 -1。如果代码写成num % 2 1判断奇数-3 就会被错误地分到偶数那一组。这不仅仅影响分类还会导致奇数之和计算出错。而写num % 2 ! 0无论正负所有奇数都会被正确识别同理num % 2 0对所有偶数也是普适的。这个规则不仅适用于C语言Java、C、C# 等语言的取余行为也都遵循“结果符号与被除数一致”的规则所以这一条可以放心地用在整个编程生涯里唯一需要额外注意的是 Python它的%结果符号与除数一致-3 % 2在 Python 里结果是 1反而能直接用 1。但为了代码统一和逻辑可读性我的建议是一律用“与0比较”的写法无论什么语言都万无一失。2.2 累加器的初始化与作用域累加器累加求和变量的初始化位置是另一个高频错误点。正确的做法是在进入循环之前初始化为0循环体内每轮加上当前元素。如果写在循环体内每轮都会被重置最后结果就是最后一个元素的值而不是总和。另一个值得注意的点是作用域。如果求和变量声明在某个语句块内部循环结束后就访问不到了。所以要把sum、sum_even、sum_odd这类变量声明在函数级别、循环之外。这点在代码组织上看似不起眼却是很多新手从“能编译”到“能正确运行”之间的一道坎。还有一点是关于变量命名的。我见过不少人的代码写成int s 0;然后循环里s arr[i];最后输出时已经忘了s代表什么。这类代码自己调试时都费劲别人更看不懂。建议命名用total_sum、even_sum、odd_sum这类描述性强的标识符哪怕多敲几个字母也值得这属于编码规范里最基础的“可读性”要求。3. 奇偶分类的进阶思考稳定分区与稳定性问题基础题做完我们来想一个升级问题分类后的奇数和偶数是否要保持它们在原数组中的相对顺序换句话说如果原数组是{1, 2, 3, 4}分类后奇数数组是{1, 3}、偶数数组是{2, 4}这是保持顺序的但如果原数组是{3, 1, 4, 2}保持顺序的话奇数数组应该是{3, 1}而不是{1, 3}。“是否保持原有顺序”这个问题在计算机里有一个专业术语叫稳定性。稳定分类的算法会保留原数组中相同类别元素的相对顺序不稳定分类则可能打乱。对于数组元素求和与奇偶分类这个例题你完全可以用两个新数组把两种分类分别存下来只要遍历顺序是从左到右、往新数组尾部添加那自然就是稳定的。但如果你用交换法原地分类比如双指针从两端交换稳定性就会被破坏。原地分类是一个常见的进阶考点。经典做法是双指针一个从左往右找奇数一个从右往左找偶数找到就交换。这种做法空间复杂度是 O(1)不需要额外数组非常适合内存敏感的场景。它的核心逻辑是左边指针停在奇数上右边指针停在偶数上二者交换后奇数去了左边偶数去了右边然后指针继续靠拢直到相遇。这个思路在“把负数移到左边、正数移到右边”“把0移到末尾”等一大类题目里都能复用。但双指针法有个代价就是分类结果的顺序被打乱了。所以做这类题时必须先搞清楚题目要的是“稳定分类”还是“不关心顺序”。如果题目没说通常默认不关心顺序此时双指针原地处理是更优解如果题目明确要求保持原顺序那就老老实实开辟新数组存储。3.1 从基础题到高频变体指针数组与二维数组的延伸说回热词里出现的“指针数组”“二维数组”“函数指针数组”这些概念它们和这道例题是什么关系关系在于无论数组里的元素是整数、字符串还是指针遍历、求和、分类的“骨架”都是相似的区别只在于“比较条件”和“累加规则”。指针数组就是一个数组里面的每个元素是地址。比如char *names[] {Alice, Bob, Cindy}每个元素是一个字符串的首地址。对指针数组做分类通常不再按数值奇偶判断而是按字符串长度、首字母范围等条件分组。但遍历方式、边界控制、分组存储的思路跟这道奇偶分类题一模一样。这就是例题的价值所在把基础逻辑练熟换条件、换类型时能迅速套用。二维数组又是另一类常见延伸。二维数组本质上是“数组的数组”比如int matrix[3][4]是3行4列的结构。对二维数组求和的常见操作有求所有元素总和两重循环、求每行之和外层循环固定行、内层累加该行、求对角线之和只有方阵才有”主对角线“和”副对角线“条件分别为i j和i j n - 1。这些题在考试和实际工程中都高频出现底子就是这家例题里练的“遍历 累加 条件控制”。3.2 字符串数组与字符数组的求和变形还有一种变形把“求和”从数值范畴扩展到字符范畴对字符数组求和累加的是字符的 ASCII 码值对字符串数组求和累加的是各字符串的长度总和。这类题形如“给一个字符串数组计算所有字符串长度的总和并分类为奇数长度/偶数长度”底层逻辑跟本题完全一致只是把arr[i] % 2换成了strlen(arr[i]) % 2。这里要提醒一个C语言常见问题字符数组要以\0结尾否则strlen会继续往后读内存直到碰上一个0字节才停下来轻则算错长度重则越界崩溃。给字符数组初始化时要么显式写char s[] hello自动加结束符要么手动给最后一个元素赋值\0。这个坑在涉及到“字符串数组转字符串”“数组转字符串”等操作时特别常见——很多人转换后得到的字符串尾部带着乱码就是因为忘加结束符。4. 实战中的避坑指南与经验技巧前两节把原理和进阶都梳理完了现在聊点实打实的经验。这些内容不是在教科书上能轻易找到的而是我反复调试、反复被虐之后总结出来的实用心得。按场景分类整理成下面的避坑指南。4.1 数组初始化的两个极端初始化是新手踩坑重灾区。常见的错误有两个极端要么不初始化直接使用要么错误地初始化。不初始化的典型例子是声明int arr[10];然后直接用arr[i] 1此时 arr 里的值是上次某块内存遗留的垃圾值结果完全不可控。正确做法是int arr[10] {0};或int arr[10] {1,2,3};其余自动补0。另一个极端是初始化方式不对。有人会用int arr[10]; arr[10] {0};试图整体赋0这在C语言里是编译错误——数组初始化语法只能在声明时使用声明之后只能逐个元素赋值或者用memset函数。memset(arr, 0, sizeof(arr));是运行时整体置0的常用做法适用于动态数组和栈数组但要注意memset是按字节操作的对 int 数组置0没问题对浮点数组置0也没问题但不能用来把 int 数组重置成1字节序会把1变成0x01010101。4.2 数组越界不会报错但后果很严重C语言和C的数组越界不会在运行时自动报错而是直接访问相邻内存。看起来程序还能跑但数据已经被静默破坏。这比直接崩溃更可怕——崩溃还能定位问题静默污染则可能让你排查很久。常见的越界场景包括循环条件写成了i n多访问了一个位置、用一维下标访问二维数组、在char数组尾部忘写\0。我实际遇到过最隐蔽的一次越界是在动态数组的场景我malloc了 n 个 int 的空间却在循环里填了 n1 个元素程序没崩但堆内存布局被破坏后续另一个变量的值莫名其妙变了。从那以后我养成了两个习惯一是循环边界写完后默念一遍“size是元素个数下标最大是size-1”二是在代码关键位置加断言开编译器的边界检查选项。4.3 动态数组和可变数组的使用误区动态数组的价值在于运行时确定大小避免浪费栈空间。现代语言里Java 的ArrayList、Python 的list本身就是可变数组动态扩容由语言运行时处理开发者不用操心C语言则需要自己用malloc/realloc管理内存。这里最容易出的问题有三个忘记释放内存内存泄漏、realloc 后原指针指向失效、realloc 失败导致原数据丢失。一个稳妥的realloc使用模式是先用一个新指针变量接收返回值判断是否为空不为空时才替换原指针。千万不要直接arr realloc(arr, new_size)因为如果 realloc 失败会返回 NULL 并释放原内存原指针随即变成野指针。类似这样“先检查后赋值”的习惯在 C 语言里怎么强调都不过分。4.4 数组去重与对象数组去重的经验热词里提到了“数组去重”“对象数组去重”这也是数组操作的高频任务。去重的本质是“分类”的逆操作——分类是把元素分到不同集合去重是保证同一元素只出现一次。基础数组去重有三种思路双重循环逐个比较O(n²)适合小数据量、排序后相邻去重O(n log n)、用哈希集合记录已出现元素O(n)但额外空间。数据量一大第三种思路基本是首选。对象数组去重比基础数组去重多了一层复杂性比较的是对象的属性而非对象本身。比如一个含有id和name的数组你要按id去重就得先提取id字段作为比较键存储到哈希集合里。Java 里要重写equals和hashCodePython 里要让对象成为哈希对象或改用其他技巧。这类题在面试中出现的频率远高于单纯数值去重因为它考察的是“判断相等的依据由什么决定”这一核心问题。4.5 数组切片的灵活运用Python 的切片语法arr[start:end:step]是处理数组的利器而且思路也能迁移到其他语言。比如统计奇数和偶数可以不写循环直接用切片配合函数式方法实现。这在题目性能要求不高、追求代码简洁时非常合适arr [1, 2, 3, 4, 5] odd [x for x in arr if x % 2 ! 0] even [x for x in arr if x % 2 0] total_sum sum(arr)这个写法的底层逻辑和C语言完全一样遍历数组、逐个判断条件、把结果收集到新列表只是语法层面帮你把循环封装起来了。理解列表推导式的本质再回头看 C 语言里的手动循环就不会觉得两种风格水火不容。我自己在实际工作中会混合使用小规模数据用列表推导式提高效率大规模数据用底层循环和生成器来控制内存占用。切片里最经典的坑是边界。arr[:n]取前 n 个元素但如果 n 超过数组长度Python 不会报错而是直接取到末尾这既是便利也是隐患——比如你本想取前5个元素结果数组只有3个元素代码不报错后续的逻辑就会基于错误的数据继续运行。Negotiating这类“静默改变行为”的边界性质务必在关键业务里加显式判断。4.5 常见问题与排查方案速查表现象可能原因排查方向总和始终为0或只等于最后一个元素累加器初始化在循环体内检查累加器声明位置是否在循环外奇数结果缺少负值用num % 2 1判断奇数改用num % 2 ! 0遍历时输出最后一个有效元素后报错循环条件写成i n改成i n注意数组下标从0开始字符串输出尾部有乱码字符数组未以\0结尾初始化时显式留出结束符位置并赋0realloc后旧内存崩溃直接ptr realloc(ptr, size)无检查先存新指针判空后再替换分类结果乱序双指针原地交换导致不稳定明确题目是否要求稳定否则开辟新数组这张表里的场景基本覆盖了数组操作中大部分经典故障。如果你在编码过程中遇到异常行为先对着表格自查一遍大概率能直接定位问题所在。关于这道“数组元素求和与奇偶分类”例题我最后想补充的一点个人体会是不要因为题目简单就轻视它。越是基础的题目越能暴露一个人对底层机制的理解深度。把这道题的边界条件、负数处理、稳定性和复杂度分析全部讲清楚你在面试中展现出的就不仅仅是一段跑得通的代码而是完整的工程思维。后续无论遇到树状数组、二维数组还是对象数组的复杂题目回头看这道例题的解法你会发现它们共享着同一套底层逻辑确定遍历方式明确判断条件维护好状态变量。把这三件事刻进脑子里数组类的题目就已经解决了一大半。
返回列表