
最近实验室的 C 语言课设里“各位数字之和排序”这道题出现频率特别高。刚看到题目描述的时候很多人以为又是一道普通的数组排序题结果真上手一写才发现坑全藏在“各位数字之和”这个自定义比较规则里。这道题表面上是练排序实际上把循环、取模、函数封装、自定义比较器的思路全串起来了算是 C 语言基础阶段性价比很高的一道综合实验。我打算完整拆一遍这道题的解法包括题目到底在考什么、代码应该怎么写、排序算法怎么选以及我这些年帮人改代码时遇到的高频 bug。无论你是刚学完数组和函数的大一新生还是在准备计算机二级、想复习排序算法的老手这篇都能给你点参考。为了保证不同基础的同学都能跟上我会把原理和代码挨个拆开讲尽量说人话。1. 题目解读与核心思路1.1 这道题到底在考什么先还原一下题目原型输入若干个整数按每个数的“各位数字之和”从小到大排序如果各位数字之和相同再按原值大小排序。比如输入12 23 4512 的数位和是 323 的数位和是 545 的数位和是 9所以排完顺序就是12 23 45。拆开看这道题其实考了三个点第一怎么取一个整数的每一位数字。这里要用到循环加取模% 10取个位/ 10去掉个位直到数字变成 0。这个技巧在以后处理回文数、水仙花数、数字反转时也会反复用到属于 C 语言基础里的高频基本功。第二怎么对一个数组按自定义规则排序。常规的冒泡、选择排序都是拿两个元素直接比较大小但这道题的比较对象不是元素本身而是经过digitSum()函数算出来的数位之和。这就要求你能把“比较规则”从元素比较中抽离出来理解排序的本质就是按规则交换位置。第三函数封装和模块化设计。实验报告通常要求你写独立的数位求和函数、排序函数而不是把全部逻辑塞在main()里。这个习惯对后续学数据结构、做课设都很重要。很多同学栽在思路上是因为一上来就想当然地用数组元素本身的大小排序排完才发现顺序完全不对。所以第一步不是急着写代码而是把题目里“比较的维度”搞清楚这也是我反复和学生强调的先看清规则再动手。1.2 为什么值得认真做这道题从教学角度来说这道题是“排序 数位处理”的复合题比单纯打一个冒泡排序有意义得多。它迫使你主动去思考“排序的规则是啥”而不是“排序的代码是啥”这两者之间有本质区别。我见过太多同学能把冒泡排序背得滚瓜烂熟但一旦问他“如果我想按字符串长度排、按绝对值排、按奇偶交替排你怎么办”他就卡住了。原因就在于他背的是代码模板而不是算法的思想。而这道题正好逼着你把“比较逻辑”独立出来。从实用性角度来说自定义比较规则在实际开发里到处都是。比如 Excel 里按 IP 地址排序如果直接按字符串排192.168.1.9会排在192.168.1.10后面因为字符串比较到“9”和“1”时已经分出大小了。正确的做法是把 IP 拆成四段整数按数值逐段比较。再比如 MySQL 里写ORDER BY如果你想要中文字符串按拼音排也要明确排序规则。这些本质都是“自定义比较器”的问题和这道实验题是同一个底层逻辑。所以别把这道题当成普通的课后作业做完之后可以自己扩展一下比如按数位乘积排、按数字反转后的大小排、按二进制中 1 的个数排。把比较函数换一换代码框架基本不用动这种“一次封装多处复用”的感觉就是模块化设计的萌芽。2. 代码实现与关键环节解析2.1 整体结构先铺一遍我先把一个比较完整、适合作为实验报告的代码版本贴出来然后一段一段拆解。这个版本采用模块化写法包含数位求和、冒泡排序、主函数并用常量定义数组最大长度方便改数据规模。#include stdio.h #define MAXN 100 int digitSum(int n) { int sum 0; if (n 0) { n -n; } while (n 0) { sum n % 10; n / 10; } return sum; } void bubbleSort(int a[], int n) { int i, j, temp; for (i 0; i n - 1; i) { for (j 0; j n - 1 - i; j) { int sum1 digitSum(a[j]); int sum2 digitSum(a[j 1]); if (sum1 sum2 || (sum1 sum2 a[j] a[j 1])) { temp a[j]; a[j] a[j 1]; a[j 1] temp; } } } } int main() { int a[MAXN], n, i; printf(请输入需要排序的数字个数); scanf(%d, n); printf(请输入%d个整数, n); for (i 0; i n; i) { scanf(%d, a[i]); } bubbleSort(a, n); printf(按各位数字之和排序的结果为\n); for (i 0; i n; i) { printf(%d , a[i]); } printf(\n); return 0; }这段代码核心逻辑很清楚先读入 n 和数组调用排序函数最后输出。digitSum负责算数位和bubbleSort里每比较两个数时先算各自的数位和再按规则交换。整体上没用什么高级技巧但作为实验报告的主体代码非常合适。2.2 数位求和函数怎么写才严谨数位求和是整个题目的前提。常见写法是循环取末位但有几个细节容易被忽略第一个细节是负数。题目如果没明说“非负整数”你最好在函数入口处做一次处理。比如n -n这样-123的数位和就是1236。如果不管负数while(n 0)这个循环根本不会执行-123的数位和会变成 0排序结果就完全不对了。我建议在函数开头统一转正这是最稳妥的做法。第二个细节是 0。如果输入包含 0while循环一次都不进sum保持初始值 0这在逻辑上是对的因为 0 的各位数字之和确实为 0。不过我见过有同学为了省事把digitSum写成递归函数却忘记处理 n 为 0 的边界条件导致死循环或者返回垃圾值。所以不管用循环还是递归都要先想清楚边界。第三个细节是返回类型。单个数位和不会超过9 * 位数即使 int 范围内最大数 2147483647数位和也只有 46所以用int返回完全够用。但如果后面扩展到 long long、甚至超长整数就要考虑用更大类型或者改字符串处理。这道题暂时不用想那么多但要有这个意识。2.3 排序部分为什么推荐自己写这个比较规则排序函数里最关键的一行是这个判断条件if (sum1 sum2 || (sum1 sum2 a[j] a[j 1]))很多第一次写这道题的同学会在这卡住因为他只记得冒泡排序的模板是if (a[j] a[j1])一换成就不会了。其实你要理解这里的a[j]和a[j1]仍然存在只不过它们的“排序权重”不是自身而是算出来的数位和。所以在每次比较前先调用digitSum()把权重算出来再用权重比较。如果数位和相等怎么办需求如果没有额外说明通常建议按原值升序作为二级规则否则排序结果会因为冒泡排序的稳定性而依赖于原始输入顺序这在自动判题系统里容易翻车。我给学生讲的时候喜欢打一个比方这就像先按姓氏笔画排笔画一样就按出生日期排总得有一个“兜底规则”不然两个人的相对先后就说不清了。这部分的经验是把比较规则单独提取成一个函数或者至少用一个独立的判断条件块不要把它塞进一个又长又乱的条件表达式里。虽然这段代码只有三行但如果你以后用 qsort、C 的 sort你会发现它们全是“自定义比较器”一个套路。在这里养成清晰抽象的习惯后面学起来会顺畅很多。3. 排序算法选型与复杂度对比3.1 冒泡、选择、插入到底选哪个实验题对算法一般没有硬性要求通常数据量在两位数以内任何排序都能瞬间跑完。但既然题目挂了“排序”这个热搜词我还是把主流选择列一下大家可以根据实验报告要求来选算法基本思想最好时间复杂度平均时间复杂度最坏时间复杂度额外空间稳定性冒泡排序相邻元素两两比较大数逐步后移O(n)O(n²)O(n²)O(1)稳定选择排序每轮选最小元素放到前面O(n²)O(n²)O(n²)O(1)不稳定插入排序将新元素插入到已排序区间的合适位置O(n)O(n²)O(n²)O(1)稳定快速排序分治选基准把数据分成左右两部分O(n log n)O(n log n)O(n²)O(log n)不稳定归并排序分治先拆分再合并有序区间O(n log n)O(n log n)O(n log n)O(n)稳定如果实验报告要求“至少写出一种排序方法”写冒泡排序最省事代码短、思路直白而且结合这道题的自定义规则反而比纯模板排序更出彩。如果你不想被时间复杂度问题纠缠写插入排序也行代码量和冒泡差不多而且对基本有序的数据表现更好。但如果你追求代码优雅也可以先用qsort这种库函数把比较器写好排序交给系统。我后面会单独讲这个写法因为它能让你直观体会到“比较器”在工业代码里的重要性。3.2 时间复杂度要怎么算才让人信服很多同学实验报告里写“本算法时间复杂度为 O(n²)”但说不清为什么。这里用冒泡排序举个简单例子。冒泡排序的外层循环跑了n-1趟第i趟内层需要比较n-1-i次。总比较次数是(n-1) (n-2) ... 1 n(n-1)/2忽略常数项和低阶项就是 O(n²)。这里的digitSum(a[j])每次调用也要做循环取模假设每个数最多有 d 位那么一次比较的额外开销是 O(d)。但因为 d 在 int 范围内最多 10 位可以当成一个很小的常数所以总复杂度仍然可以写成 O(n²)。如果数据规模大到一万、十万冒泡就不行了这时候可以把排序部分换成快排或归并复杂度降到 O(n log n)。我在实验扩展部分通常会让感兴趣的同学把 n 改大到 10000再用系统自带的计时函数对比一下冒泡和快排的差异这样你对“复杂度”会有非常直观的感受远比背概念强。3.3 稳定性问题别忽略稳定性是排序里一个容易忽略但后续很重要的话题。对于这道题如果题目只要求“按各位数字之和排序”没提相同数位和的相对顺序那理论上你可以随便排。但通常我们希望给出确定性的结果所以会加一个二级比较规则比如按原值升序。这时“稳定性”的意义就体现出来了如果你用稳定排序并且没写二级规则那么相同数位和的元素会保持输入时的相对顺序如果你用不稳定排序顺序就无法预测。自动判题系统一般会对输出做严格校验所以最稳妥的办法还是自己写明二级规则不要依赖排序算法的稳定性。这也解释了为什么我在冒泡排序里写了(sum1 sum2 a[j] a[j1])这个条件。加上这个条件之后不管用稳定还是不稳定排序只要比较规则一致结果就一致。这就叫“把决定权握在自己手里”。4. 完整的实验过程与现场记录4.1 从读题到上机的完整步骤这道题的上机流程可以拆成这么几步建议所有同学都按这个顺序走避免返工先手写几个测试用例把预期的输入输出列清楚。比如12 34 5 100 99算出来数位和分别是 3、7、5、1、18排序后的顺序是100 12 5 34 99。这一步能帮你验证代码逻辑对不对。打开编辑器先写digitSum函数单独测试它。你可以在main()里写几行临时代码输入几个数字打印数位和确认无误再注释掉。很多同学喜欢一口气写完整个程序再调结果错误一多根本不知道从哪查起我建议一步一步来。写排序函数。先用最熟悉的冒泡排序把比较条件替换成数位和比较保证主流程能跑通。测试。先用小数据比如 3 个数、5 个数人工能算出来再测边界数据比如负数、0、全部相同数位和的情况。最后再做代码整理把临时测试代码删掉补充注释保证实验报告里的代码整洁。我在带学生做这一步时发现一个很普遍的问题大家都急着写完整代码结果一编译就是十几行报错最后连哪一行出问题都不知道。其实分步实现、分步验证才是新手最快的路径。4.2 运行结果验证示例以输入6个整数为例请输入需要排序的数字个数6 请输入6个整数12 34 5 100 99 111数位和分别是12 → 334 → 75 → 5100 → 199 → 18111 → 3。按照数位和从小到大1001排第一12 和 111 都是 3按原值升序应该12在前111在后之后是 55、347、9918。所以输出应该是按各位数字之和排序的结果为 100 12 111 5 34 99如果你跑出来是这个结果说明核心逻辑没问题。如果你跑出来111 12或者12 111顺序不对就要检查排序里的二级规则是否写对了。我建议测试时专门构造一组“数位和相同但原值不同”的数据比如21 12 30数位和全是 3按原值升序应该是12 21 30。这组数据能直接看出你的稳定性处理和二级规则是否到位很多同学就是在这种用例上翻的车。4.3 在 VSCode 和 C-Free 里怎么跑起来关于开发环境我见到最多的组合是 VSCode MinGW或者学校机房里的 C-Free 5.0。这里分别做一点说明。VSCode 写 C 语言需要先装 C/C 扩展再配置好编译器路径。如果你用的 MinGWgcc装好后可以用终端命令直接编译gcc digit_sum_sort.c -o digit_sum_sort ./digit_sum_sort配置tasks.json和launch.json是新手比较头疼的地方其实网上有大量模板关键是确保program字段指向你自己的可执行文件路径。如果你只是为了做实验不调试也行直接用终端编译运行最快。C-Free 5.0 就比较省事了安装后新建工程把代码贴进去点编译运行就行。它比较老但胜在简单适合初次接触 C 语言的同学。这类 IDE 编译时偶尔会报中文注释或输入法全角符号的错误解决办法是尽量用英文写注释分号、括号一律在英文输入法下输入。这个看起来是小事但报错时真的能浪费你十分钟。5. 常见问题与排查技巧实录5.1 scanf 读取数据时的经典坑这道题最典型的运行时问题就是scanf读入个数和读入数组时出现“灵异现象”——程序还没有输入就跳过了或者输出结果乱码。常见的罪魁祸首有这几个忘记加取地址符比如写scanf(%d, a[i])这会导致程序往非法地址写入直接崩溃或数据错乱。很多编译器还不一定报错只能自己盯。输入缓冲区问题。如果你在scanf(%d, n)之后又用getchar()读字符就容易被上一个回车符干扰。但本题只读整数通常不会遇到只要记住一个原则不要混用scanf和getchar处理同一批输入。数组下标越界。读取个数n后如果输入的数字个数超过MAXN越界写入会破坏其它变量的值。这种问题不好查所以养成用#define MAXN 100定义上限的习惯并在读入前判断n MAXN。排查方法很简单在scanf之后加一句printf(%d, a[i])看看读进来的值是否正常。有同学觉得 printf 调试很土但在这种几百行的小程序里是最快最直观的定位方式。5.2 负数和 0 的处理容易出问题负数我在前面已经提过函数里转正即可。但有一个更隐蔽的坑如果你用n ! 0作为循环条件而不是n 0那么转正前的负数是能进入循环的但取模结果是负数加起来的数位和会是负的。比如-123按-3 -2 -1 -6来算这就不符合常规预期了。所以最规范的写法是if (n 0) { n -n; } while (n 0) { ... }至于 0前面说过了循环不进返回 0这个没问题。但如果题目要求的正整数范围不包括 0你可以在读入时做合法性校验不过实验题一般不会限制得这么细。5.3 输出格式和边界条件很多自动判题系统对输出格式要求非常严格比如每个数字之间用空格分隔但行尾不能有多余空格或者最后必须换行。如果实验报告只要求展示结果那问题不大但如果你是在 OJ 上练习就要注意了。我推荐用“判断是否最后一个元素”的方式控制空格输出for (i 0; i n; i) { if (i 0) { printf( ); } printf(%d, a[i]); } printf(\n);这种写法比printf(%d , a[i])更稳妥不会在行尾多出一个空格。另外如果输入数组里本身就有重复值排序输出也允许重复不需要去重别多写那些和题目无关的逻辑。5.4 用qsort扩展实现同一个排序如果你已经熟悉指针和函数指针可以试试用 C 标准库的qsort实现同样的功能代码会简洁不少#include stdlib.h int cmp(const void *a, const void *b) { int x *(const int *)a; int y *(const int *)b; int sx digitSum(x); int sy digitSum(y); if (sx ! sy) { return sx - sy; } return x - y; } qsort(a, n, sizeof(int), cmp);这个写法把“比较器”单独提出来了qsort内部会自动根据比较结果排序。好处是你不需要自己写排序循环坏处是你得理解const void *指针和类型强转这对刚学完指针的同学来说可能有点跳。我一般建议先用手写排序完成实验再额外尝试qsort作为拓展两个都跑了你对 C 语言的理解会上一个台阶。5.5 进一步扩展文件读写、字符串版、指针版做完基础版之后可以试着从这几个方向扩展改成从文件读入数据排序后再写入另一个文件。这就用到了fopen、fscanf、fprintf、fclose这一套文件操作。实验报告里如果能加上文件版本通常会显得更完整。如果输入是字符串形式的数字比如可能超过int范围的大数你需要用字符串存然后每一位字符转成数字再求和。这就会用到strlen、isdigit等函数和 C 字符串处理知识挂钩。如果想把数组换成动态内存分配就用malloc和free这样n多大就申请多大不会浪费空间。理解指针和内存管理对后续学数据结构帮助很大。这些扩展不需要全部做挑一个感兴趣的深入就行。我遇到过不少同学实验题做完之后觉得“不过瘾”顺着这些方向自己折腾了一下午最后对 C 语言的理解比上课一学期还深。学习编程这种事光看是没有用的必须上手改、上手跑、上手调试。6. 干货补充我的调试心得与代码风格建议很多人写 C 语言实验题觉得只要最后结果对了就万事大吉。但如果你以后还有二级考试、课程设计、甚至找实习笔试代码风格和调试习惯都会直接影响你的效率和别人对你的评价。先说代码风格。命名尽量不要用a、b、s1、s2这种没意义的缩写。虽然局部变量用短名没问题但函数名、比较规则这种关键部分最好能让人一眼看懂。比如digitSum表示数位和bubbleSort表示冒泡排序cmp表示比较器。读你代码的人包括两周后的你自己会感谢你。再说调试习惯。我强烈建议你学会使用“分段验证”的思路。把程序拆成若干个独立小模块每完成一个模块就立刻验证它。比如写完digitSum可以写个临时循环打印几个数的数位和写完排序先不要读入数据直接定义死一个数组看看排序输出对不对。确认没问题后再接上交互输入。这样每一步出问题都能立刻定位而不是到最后面对一大片报错无从下手。最后是边界测试。很多同学测完正常数据就交了结果判题系统里全是隐藏的边界数据。建议你每次实验都专门测三组数据全是负数、包含 0、数位和全部相同。这三组过了基本就稳了。这套“边界思维”不仅是写 C 语言实验题需要以后写任何代码、做任何功能都适用。我个人在实际带实验时最喜欢让同学做的一步是把这道题改一版按“二进制中 1 的个数”排序。改完你会发现代码框架完全不用动只需要把digitSum换成countOneBits比较规则就彻底变了。这种“换规则不换框架”的体验才是这道实验题真正想教会你的东西。