C语言实验:按各位数字之和排序的实现与技巧
发布时间:2026/10/6 13:36:41 作者:尧图编辑部 阅读量:1,286

1. 题目拆解先搞清楚到底要对什么排序1.1 一个容易被误读的排序规则C语言实验——各位数字之和排序这道题几乎每个学C语言的人都会遇到。它通常的表述是输入n个整数按每个整数各位数字之和的大小从小到大输出这些整数。我第一次拿到这道题的时候也踩了个坑——扫了一眼题目以为是对数字本身排序结果把原始数排完一交输出全错。实际上题目想要的是这样一个过程输入123 45 9计算各位数字之和123645999按和值排序123(和值6) → 45(和值9) → 9(和值9)输出123 45 9或连同和值输出也就是说排序的关键字是各位数字之和而不是数字本身。原始整数只是跟着和值一起移动的乘客。这个理解一旦错了后面所有代码都是白写。这道题之所以经典是因为它把三个基础知识点串在了一起整数的数位拆解、排序算法、以及排序时如何保持数据与关键字的对应关系。大多数实验课把它安排在学完数组和函数之后目的就是让你练这三件事。网上搜翁恺c语言练习题也能看到类似题说明它是C语言教学里公认的入门必练。1.2 写代码前必须确定的四个输出细节很多同学拿到题就敲键盘结果写出来的程序自己跑感觉没问题一交上去就错。原因往往不是算法不会而是题目里的几个细节没有先想清楚。我在动手前一定会把下面四个问题逐条确认决策点常见要求如果不确定怎么办升序还是降序多数是升序和值从小到大看题目括号里的说明和值相同时怎么排多数要求保持原输入顺序稳定排序用稳定排序或交换时用大于才换负数怎么处理一般按绝对值求各位数字之和先取绝对值再拆位输出内容输出原始整数还是整数和值看题目样例输出这四个点不是小事。比如输出格式有些实验平台要求每个数之间用一个空格隔开行尾不能有多余空格有些要求每个数占一行还有些要你输出类似123(6)这种带括号的格式。写代码之前把这些确定下来比写完再改省事得多。另外还要确认一项输入格式。题目一般会给一个整数n表示数字个数然后一行n个整数。但也有些变体是不给n一直读到文件结束。这两种输入方式的写法完全不同前者可以用scanf(%d, n)然后循环读后者要配合while (scanf(%d, x) ! EOF)。我先按最常见的先给n来做后面第七节会讲怎么改成EOF版本。2. 数字拆解两种求各位数字之和的写法及取舍2.1 方案Awhile循环取模最直接的数学写法求一个整数的各位数字之和最经典的做法是反复取末位、去掉末位直到数字变成0。原理很简单任何整数对10取余得到的就是个位数字除以10个位就被丢掉原来的十位变成了新的个位。int digitSum(int x) { int sum 0; if (x 0) { x -x; // 负数先取绝对值 } while (x 0) { sum x % 10; // 取当前个位 x / 10; // 去掉个位 } return sum; }这里有两个细节值得多说一句。第一是if (x 0)的处理。C语言里负数对10取余结果是负数比如-123 % 10在大多数编译器里得到-3累加出来的和就变成负数了。这不符合各位数字之和的直觉定义所以要先取绝对值。如果你确定题目不会出现负数可以省掉这一步但只要题目没说输入全是正整数我会建议保留。第二是循环条件。while (x 0)在x等于0的时候不会执行函数直接返回0。这个行为是正确的因为0的各位数字之和就是0。但有一个隐藏问题如果按do-while写就得小心x0时也能进入循环算一次。这两种写法各有适用场景我更推荐上面这种简单的while。2.2 方案B字符串法处理大数和负数更稳还有一种求数位和的办法不把整数当数字处理而是先把它的字符串形式读进来再遍历每个字符减去0得到对应的数字。比如123这个字符串1 - 0就等于1。#include stdio.h #include string.h int digitSumByString(const char *s) { int sum 0; for (int i 0; s[i] ! \0; i) { if (s[i] 0 s[i] 9) { sum s[i] - 0; } } return sum; }这个写法最明显的好处是它不需要把字符串先转成整数。如果输入的数字很大比如20位的数字串用int或long long读会溢出但字符串法完全不受影响。负号也天然被绕过了——循环里判断s[i]是数字字符才累加负号直接忽略。不过它也有代价读入方式变了。如果题目要求先读整数再算你得用sprintf把整数转成字符串这就绕了一圈。比如int x 456; char buf[32]; sprintf(buf, %d, x); int s digitSumByString(buf);两种方法对比下来对比点数学取模法字符串法适用整数范围int范围内稳妥任意长度数字串都能处理负数处理需要先取绝对值字符判断天然忽略负号代码量短小稍长但也不复杂与题目贴合度高本身就是整数运算适合输入就是字符串的场景我在做这个实验题时默认用数学取模法因为它更贴合整数运算的考点但如果题目给的输入包含超长数字我会立刻切到字符串法。两种写法都建议亲手敲一遍因为它们考察的是不同的基本功。函数封装好之后后面排序部分完全不用关心数位和是怎么算的——这就是写独立函数的第一个好处。3. 先别急着排序直接排和值数组会丢数3.1 数组下标法省事但容易丢对应很多人的第一版代码长这样开一个sums[100]数组把所有数位和存进去然后对这个数组做冒泡排序。跑起来一看和值确实排好了可问题来了——原始整数一个都没输出。因为sums数组里只存了和值没存原始数。图省事的做法是再开一个数组arr[100]存原始数。排序的时候比较sums数组的元素交换时把arr数组和sums数组对应位置一起交换。// 同时交换两个数组的元素 int temp sums[j]; sums[j] sums[j1]; sums[j1] temp; temp arr[j]; arr[j] arr[j1]; arr[j1] temp;这个思路没错但代码写起来很啰嗦而且一旦要跟着交换的数据变多比如还要输出和值、还要保留序号交换代码会膨胀得很难看漏写一行的概率也大大增加。我见过不少同学在这个版本里只交换了sums忘了交换arr排查半天找不到原因。3.2 结构体方案从写作业到写程序的思维转变更合理的做法是在一开始就把原始数和数位和放进同一个结构体里让它们成为一个整体。排序的时候比较键是结构体里的digitSum交换时整个结构体一起动天然不会丢对应关系。typedef struct { int value; // 原始整数 int digitSum; // 各位数字之和 } Number;这个设计思路说大不大说小不小。它体现的其实就是聚合相关数据的思想——在真实项目里一条学生记录可能包含学号、姓名、成绩一个商品可能包含名称、价格、库存。程序处理的数据从来不是孤立的单个值而是一组有关系的字段。C语言实验里提前接触结构体后面学链表、学数据库都会轻松很多。用结构体的冒泡排序交换代码变得非常整洁Number temp; temp numbers[j]; numbers[j] numbers[j1]; numbers[j1] temp;看到没整个结构体直接赋值即可。C语言里结构体支持整体赋值不需要写numbers[j].value numbers[j1].value这种逐字段搬家的代码。这个用法很多人到大二写课设才开始用其实在实验题里就可以养成习惯。结构体方案唯一的门槛是你得先掌握结构体的定义和访问成员语法。这对刚学完数组、正准备做这道实验题的人来说可能有点超前。但如果你的实验课进度已经讲了结构体或者你愿意自己翻书先看两页我非常推荐直接上结构体。题目本身并没有限制你只能用数组而结构体是这道题在数据组织维度上最优雅的解法。4. 排序算法选型实验题首选冒泡排序4.1 冒泡排序的实现思路与完整代码排序算法有冒泡、选择、插入、快速、归并……但实验题和面试题的标准不一样。实验题的核心目标是在尽量短的时间内写出正确、能讲清楚逻辑、方便写实验报告的代码。冒泡排序在这里几乎是完美的选择。冒泡的思路用一句大白话说每一轮从头到尾扫一遍遇到相邻两个元素顺序不对就交换一轮下来最大的元素就像气泡一样冒到了最后。对n个元素来说最多要冒n-1轮因为最后剩下的那一个自然有序。完整代码放在结构体语境下是这样// 按 digitSum 从小到大排序稳定冒泡 for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (numbers[j].digitSum numbers[j1].digitSum) { Number temp numbers[j]; numbers[j] numbers[j1]; numbers[j1] temp; } } }这里面有两个关键点我多说几句。第一内层循环的n - 1 - i是冒泡排序性能的关键。第i轮结束后数组末尾已经有i个元素是排好的再去扫它们纯属浪费。这个上界不能写成n - 1那样程序也能跑但会多做很多无意义的比较。实验报告里如果要分析复杂度这个细节也影响你对最好情况的判断。第二比较条件是而非。这个细节直接决定了排序的稳定性只有前一个和值严格大于后一个才交换相等的两个元素就不会交换所以它们在数组里的相对顺序保持不变。如果题目没要求和结果看起来一样但如果题目要求和值相同按原输入顺序输出这里必须用。有些学生在这里用输出和值相同的数据顺序就会被打乱白白丢分。4.2 若实验要求其他算法选择排序和qsort怎么套有些实验指导书会指定用选择排序。选择排序的思路是每一轮从未排序部分挑出最小的放到已排序部分的末尾。套到这道题上核心动作是找最小和值的下标然后交换。它的代码长这样for (int i 0; i n - 1; i) { int minIdx i; for (int j i 1; j n; j) { if (numbers[j].digitSum numbers[minIdx].digitSum) { minIdx j; } } if (minIdx ! i) { Number temp numbers[i]; numbers[i] numbers[minIdx]; numbers[minIdx] temp; } }这里要注意的是选择排序的交换是交换两个不相邻的元素它会直接改变相等元素的相对顺序所以它天然不稳定。如果题目要求稳定排序别用手写选择排序。还有一条更进阶的路直接用C标准库的qsort。这个函数是快速排序的实现功能非常强只是它需要你提供一个比较函数。对结构体数组排序时比较函数是这样写的#include stdlib.h int cmp(const void *a, const void *b) { Number *pa (Number *)a; Number *pb (Number *)b; return pa-digitSum - pb-digitSum; } qsort(numbers, n, sizeof(Number), cmp);cmp返回负数、0、正数分别表示a应该排在b前面、相等、后面。如果实验老师允许使用库函数一行qsort加一个回调函数比手写任何排序都省事。但用它有一个前提你得理解回调函数这个机制否则写不出正确的cmp。我的建议是先手写冒泡把逻辑练透再用qsort对比一下两种能力都具备才最好。5. 完整可运行的参考代码从读入到输出的每一步说明5.1 完整代码把前面几节的内容合到一起就是一份能够直接编译运行的参考程序。我习惯按读入 → 求数位和 → 排序 → 输出四段来组织这样结构清晰实验报告也好写。#include stdio.h #define MAXN 100 typedef struct { int value; int digitSum; } Number; // 求各位数字之和负数按绝对值处理 int digitSum(int x) { int sum 0; if (x 0) { x -x; } while (x 0) { sum x % 10; x / 10; } return sum; } int main(void) { int n; Number numbers[MAXN]; printf(请输入数字个数 n); scanf(%d, n); printf(请输入 %d 个整数, n); for (int i 0; i n; i) { scanf(%d, numbers[i].value); numbers[i].digitSum digitSum(numbers[i].value); } // 按各位数字之和从小到大排序冒泡排序 for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (numbers[j].digitSum numbers[j1].digitSum) { Number temp numbers[j]; numbers[j] numbers[j1]; numbers[j1] temp; } } } // 输出排序结果 printf(排序结果按各位数字之和升序\n); for (int i 0; i n; i) { printf(%d(%d) , numbers[i].value, numbers[i].digitSum); } printf(\n); return 0; }这份代码我建议你亲手敲一遍而不是直接复制。敲的过程中你会注意到几个平时容易忽略的地方结构体成员访问用点号、typedef后面分号别漏、冒泡内层循环的边界条件。这些错误都是编译器和运行结果会直接告诉你的亲手犯过一次错比看十遍别人的代码都记得牢。5.2 运行验证拿一组手算过的数据对拍写完程序一定要用自己手算过的数据来验证别随便输几个数就提交。验证的核心思路是结果自己在纸上能算出来然后对比程序输出是否一致。比如我们输入n 5 123 45 9 12 70手算各位数字之和123 → 123 645 → 45 99 → 9 912 → 12 370 → 70 7按和值从小到大排列12(3) → 123(6) → 70(7) → 45(9) → 9(9)程序输出的每一行都应该和这个手算结果一致。这个和值字段纯粹是为了方便我们肉眼验证你可以保留也可以按题目要求去掉。一定要养成先手算一组数据再拿程序跑的习惯这是排查程序正确性最原始也最有效的方法。我再提醒一个很实际的做法先用一个课程上简单的样例跑通再自己设计一个能把边界情况全部覆盖的测试用例。比如我自己习惯用这样一组测试0 5 -1 23 -100。里面有0数位和为0、负数和较大数混合能把这类题最容易出问题的几种情况一次测完。6. 实验课上最容易踩的四个坑6.1 负数符号到底要不要计入数位和这个问题看起来小实际影响很大。从题目语义上来讲各位数字之和指的是各个数位上数字的加和这个数字本身没有正负号所以-123的和应该是1236而不是-6。但C语言的运算符不会自动帮你处理这件事。-123 % 10在许多编译器里得到-3直接累加就错了。所以求数位和的函数一定要在最前面处理负数要么像我上面那样取绝对值要么用标准库里的abs()函数但abs()里同样要#include stdlib.h。哪怕题目说输入全是正整数我也建议保留这行判断——它不是多余的防御而是让你的函数变得更健壮的常规操作。以后你写的函数可能被别的地方调用调用者不一定会先帮你过滤数据。6.2 固定数组大小的取舍宏定义还是动态分配用int numbers[100]这种固定大小数组对付课程实验完全够用但有两个问题需要考虑。第一个问题是不够用的风险。如果测试数据里n大于你的数组容量程序会越界后果是难以预料的——可能内存被覆盖、可能报段错误、可能输出莫名其妙的结果。所以要么把容量开得足够大要么用宏定义把容量提取出来在所有用到的地方统一引用。#define MAXN 100 Number numbers[MAXN];这样要改容量只改一处就可以。第二个问题是看起来很呆。如果实验老师比较严格可能希望看到更灵活的做法。有两个思路一是用C99的变长数组Number numbers[n]这是C99标准支持的很多OJ和编译器也接受二是用malloc动态分配Number *numbers (Number *)malloc(n * sizeof(Number)); if (numbers NULL) { printf(内存分配失败\n); return 1; } // ... 用完记得 free(numbers);动态分配的代码量多一些但体现的是程序能处理未知规模输入的思维。如果这个实验是计分项目我会用动态分配如果就是平时练习宏定义固定数组就够了。优先级永远是先跑对再优化。6.3 和值相同时要不要保持原顺序这可能是全题最容易丢分也最难自查的地方。如果题目要求和值相同的按输入顺序输出那么冒泡排序中比较必须是而不是。因为会让相邻且相等的两个元素发生交换改变相对顺序。如果用qsort情况更微妙。标准库的qsort不保证稳定性即使cmp返回0两个相等元素的相对顺序在排序后也可能变化。所以如果明确要求稳定排序不要用qsort。我建议你在动手前直接问自己一个问题如果两个数数位和相同题目期望输出它们的先后顺序是原来的顺序还是无所谓 大多数课程实验不会特别强调这点但有些作业或PTA题会明确要求。万一题目没说我会默认保持稳定排序这样两种要求下都能用因为稳定排序的结果在不要求稳定时也是合法的。6.4 输出格式引起的隐藏扣分代码逻辑全对输出格式不对照样会扣分。对实验报告这种人工批改的场景格式问题可能只是印象分但对PTA、OJ这类在线评判平台输出多一个空格、少一个换行都算答案错误。常见的输出格式坑有三个行尾空格。有些题要求数字之间用空格隔开但末尾不能有空格。最简单的处理是第一个数前面不打空格后面的数前面打空格for (int i 0; i n; i) { if (i 0) { printf( ); } printf(%d, numbers[i].value); } printf(\n);换行缺失。printf(\n)少写一个\n在线评测系统很可能判格式错误。中英文括号混用。如果要求输出123(6)这种格式注意必须是英文半角括号()不是中文全角。在代码里不小心敲成中文符号编译器会报错这还算好发现的最怕的是编译器不报错比如在字符串里输出结果看起来对但就是跟标准输出对不上。我一直建议写输出逻辑之前先手写一遍期望的输出文本看清楚空格、换行、括号在什么位置。7. 进阶思路当实验题不再只要求过7.1 双数组联动排序不用结构体的备选方案如果你想拓宽自己的思路可以试试不用结构体用两个普通数组完成同样的事情。核心思路是比较sums数组时交换的时候把vals数组也一起交换。int vals[MAXN]; int sums[MAXN]; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (sums[j] sums[j1]) { int t sums[j]; sums[j] sums[j1]; sums[j1] t; t vals[j]; vals[j] vals[j1]; vals[j1] t; } } }这段代码比结构体版本更朴素但会让你更深刻地体会到同步交换这件事的繁琐。如果你先写了这个版本再去敲结构体版本你会立刻理解为什么结构体是更好的组织方式——不是因为它能实现更多功能而是因为它把容易出错的同步工作交给了语言本身。还有一种完全不同的思路不交换元素而是额外维护一个下标数组排序时交换的是下标。这在很多排序算法里很常见尤其在排序代价较高的场景下比如你要排序的不是简单的整数而是一整条记录时搬动下标的成本远低于搬动数据本身。这个思路作为扩展了解一下对理解排序的本质是建立一种顺序这个概念很有帮助。7.2 把代码封装成可复用函数向工程化靠拢实验题做完了不要急着关掉编译器。把代码拿回来看一眼你会发现main函数里塞了读数据、求数位和、排序、输出四件事。这样写虽然能过实验但离好代码还有距离。一个更工程化的写法是把每件事拆成独立函数int readNumbers(Number nums[], int limit); void computeDigitSums(Number nums[], int n); void sortByDigitSum(Number nums[], int n); void printNumbers(const Number nums[], int n);这样写的好处在于每一个函数都可以单独测试。比如digitSum这个函数我可以写一个专门的小程序只测digitSum(0)、digitSum(-123)、digitSum(9)这些边界情况确认它没问题了再放进主程序里用。主函数变得像读文章一样清楚。完全不需要注释光看函数名就知道程序做了什么。将来遇到相似场景可以直接复用。比如后面学结构体排序、做学生成绩管理系统sortByDigitSum稍加改动就能变成sortByScore。C语言实验课往往只检查程序能不能跑通但能跑通和写得好之间还有很长一段路。如果你每次实验都愿意多花十分钟做一次函数拆分一个学期下来函数抽象、模块化的意识会远超同龄人。这个东西不是靠背知识点学来的是靠一次次动手练出来的。我个人做实验的习惯是先快速写一个能跑的版本确认思路正确之后再重构一遍代码拆函数、整理输出格式、补上边界条件。第一遍是会做第二遍是做好。这道各位数字之和排序题恰好包含拆位、聚合数据、排序、格式化输出四个C语言基础要素做第二遍的时候你对它的理解会和第一遍完全不一样。