首页 / 资讯中心 / 文章详情

C语言冒泡排序完全解析:函数实现10个整数排序

C语言冒泡排序完全解析:函数实现10个整数排序 ★ FEATURED ARTICLE
从大一学生手里接过最初的代码到工作后自己封装算法库这个题目几乎贯穿了我和C语言打交道的全部时间。很多初学者觉得冒泡排序就是一道背下来的考试题背完就忘下次遇到排序类问题依然无从下手。但我更愿意把这道题看作是C语言学习里第一道真正的分水岭——它同时牵扯了函数、数组、循环、传参这几块最核心的基本功它值得你认认真真对待而不是只看一眼答案。这篇文章我会把实现一个冒泡排序函数、在主函数里调用它对10个整数从小到大排序这件事从原理、代码实现、调试经验到算法对比完整拆开讲一遍。不管你是刚学到指针和函数的新手还是想把这个经典题重新整理一遍的进阶同学这篇文章应该都能给你一点不一样的东西。1. 这道题藏着的考点它逼你把三块基本功一次性打通1.1 表面考排序实际考的是函数、数组、循环的组合能力我见过太多人拿到这个题目第一反应就是我会把两层循环写出来就完事。可真到动手写的时候要么内存访问越界要么交换逻辑写反要么数组传参之后函数里拿不到正确长度。其实原因很简单这道题在练习排序之前先要求你具备三样东西。第一是函数的抽象思维。你得能意识到排序这件事本身可以独立出来它接收一个数组做完之后让数组变有序至于输入输出、主程序流程那是另外的职责。第二是数组在函数间传递的机制。很多新手第一次知道数组传到函数里会退化成指针这个坑就是从这个题目开始的。第三是循环边界的精确控制。外层循环几趟内层循环比较到哪一位怎么写才不会多比较一次或者漏掉一个元素这三样东西恰好是C语言从语法入门走向真正写程序的关键一步。我经常说一句话如果你能独立把这道题写得干净、无警告、边界清楚那么你的C语言基础已经通过了最基本的验收。1.2 为什么排序场景特别适合用来练手排序有一个其他练习题不具备的优势结果可验证、错误可定位。如果你写了一个循环边界出错的程序结果往往是输入10个数之后输出的顺序不对你能立刻看到哦最后一位没排进去或者第一位是乱的。换句话说排序问题的反馈链路非常短非常适合用来调试和学习。你再对比一下让新手写一个链表反转或者二叉树遍历——错了之后你根本不知道错在哪控制变量太少。但排序不是输入输出都是明确的每一轮循环干了什么也都能用printf一步步看干净。这也是为什么从学校教材到企业面试冒泡排序永远是第一个被拿出来做教学样例的算法。它不复杂却把编程里最基础的逻辑结构和工程习惯都串在了一起。2. 冒泡排序的核心机制先想清楚一轮做了什么再谈第二轮2.1 一轮遍历的本质相邻比较把最大值慢慢顶到尾部冒泡排序最朴素的理解就是从第一个元素开始依次比较相邻的两个元素如果前一个比后一个大就把它们交换位置。这样每比较一次较大的数就往右挪一个位置。走完一整趟之后当前数组里最大的那个数就会像气泡一样浮到最右边。你可以想象一群同学按身高随机站成一排老师从左边开始检查看见左边的人比右边的高就让他们换位置一路检查到最后。走完这一遍最高的同学一定被冒泡到了最右边。虽然中间过程可能会有别的人也被换动但可以确保的是每一轮结束时这一轮范围内最大的元素一定已经到位。这个过程用代码描述就是一层for循环遍历i从0到某边界每次比较arr[j]和arr[j1]如果前者更大就把两者交换。注意决定交换与否是看两个相邻元素的排列是否破坏了我们想要的从小到大的顺序。从大到小排序也只需要把这个比较符号换一下方向而已。2.2 为什么需要N-1轮内层循环边界每轮缩减如果数组有10个数第一轮结束之后最大的数已经到了第10个位置我们下一轮就不需要再拿第9个位置和第10个位置去做比较了。第二轮只需要处理前9个元素把剩下的9个里最大的一个放到第9个位置。以此类推。每一轮可以确定当前未排序范围里的一个最大值这个最大值被放到该范围的最后一位也就是说每一轮之后待排序的范围就缩小一个元素。那到底需要多少轮当范围里只剩一个元素时它自然就是最小的那个不需要再排了。所以对于10个元素一共需要9轮也就是 N-1 轮。这里有一个新手最常踩的坑把外层循环和内层循环的边界都写成了固定的 N-1 或者 N结果就是程序虽然能跑但做了大量无用的比较。比如外层循环跑了10轮其实第10轮里只剩第1个元素了根本不需要再比。更严重的情况是内层循环写成了 j N那么当 j N-1 时访问 arr[j1]这就是数组越界读到了一个不存在的内存位置。2.3 交换两个变量那三行代码背后的陷阱交换在排序里是最基础的操作。标准做法是借用一个临时变量int temp arr[j]; arr[j] arr[j1]; arr[j1] temp;我记得在带新人的时候经常看到有人写出arr[j] arr[j1]; arr[j1] arr[j];这种两行交换逻辑上完全等价于把后一个值覆盖了两次最后两个数变成了相同的数。这个错误非常典型本质原因是没有理解赋值操作会覆盖原有数据这个最基础的事实。还有人说可以用异或运算a ^ b; b ^ a; a ^ b;来交换这种做法在语法上没问题但可读性差很多而且在某些边界场景下还可能产生额外开销。写排序代码不需要炫技三行临时变量交换是最稳妥、最直白、最不容易出错的写法。关于这一点我在后面讲调试经验时还会再提一次因为它真的是高频错误。3. 函数拆分与主函数调用完整代码解读3.1 一份可以直接编译运行的完整代码下面这份代码就是这道题的完整解答。我写代码有一个习惯每一段都要有清晰职责排序函数只负责排序主函数只负责输入输出和调用。#include stdio.h // 冒泡排序函数 void bubble_sort(int arr[], int n) { int i, j, temp; // 外层循环控制轮数一共进行 n-1 轮 for (i 0; i n - 1; i) { // 内层循环比较相邻元素范围随轮数缩减 for (j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } } int main(void) { int nums[10]; int i; printf(请输入10个整数\n); for (i 0; i 10; i) { scanf(%d, nums[i]); } // 调用排序函数 bubble_sort(nums, 10); printf(从小到大排序后的结果为\n); for (i 0; i 10; i) { printf(%d , nums[i]); } printf(\n); return 0; }把这段代码保存成bubble_sort_demo.c用gcc bubble_sort_demo.c -o bubble_sort_demo编译然后运行就可以看到完整的输入输出效果。这里我用的是数组长度直接写10因为题目明确要求处理10个整数。实际开发中更常见的写法是用sizeof(nums) / sizeof(nums[0])来自动计算长度但在函数内部没法这样算原因下一小节就讲。3.2 为什么排序要单独拆成一个函数而不是全部堆在main里不少人写这个题目时图省事直接在main函数里写完两层循环就完事了。这样写确实也能得到正确结果但从工程习惯的角度来说并不好。把排序逻辑抽成独立函数有几个好处。首先是可复用。今天对10个数排序明天可能要对100个数排序、对struct数组排序或者对一个文件里的数据排序你只需要把bubble_sort调整成更通用的样子然后在别的地方继续调用。如果排序代码写在main里下次再想用就只能复制粘贴那维护起来就是个灾难。其次是可读性。main函数应该是整个程序的目录页一进去就知道程序做什么接收输入、调用排序、输出结果。至于排序是怎么做的那是bubble_sort函数内部的细节看main的人不该被10行循环逻辑干扰。最后是可测试。独立函数意味着你可以单独为它写测试代码输入一组固定数据检查排序结果是否符合预期。如果代码堆在main里你想测试排序逻辑必须连带着把整个程序跑起来效率低而且容易受其他部分影响。3.3 数组传参的骗局为什么函数里能改掉main的数组这是新手最容易困惑的一点。以前学普通变量传参时老师会强调形参是实参的拷贝函数里修改形参不影响实参。但到了数组这里你发现bubble_sort函数里改了arr[0]main函数里的nums[0]也变了。这不是矛盾吗答案在于C语言里数组作为函数参数时并不会把整个数组拷贝一份传进去而是传递了数组首元素的内存地址即指针。我写的void bubble_sort(int arr[], int n)等价于void bubble_sort(int *arr, int n)。函数里的arr本质上就是一个指向int类型的指针它指向main函数中nums数组的首地址。因此通过arr[j]去访问和修改数据修改的就是main函数里那块内存上的数据。这带来一个重要推论在函数内部用sizeof(arr)得到的是指针的大小而不是整个数组的大小。比如在我的64位机器上sizeof(arr)给出来是8字节而sizeof(nums)是40字节。这也是为什么我在函数里必须显式传递一个n作为数组长度。我经常看到有同学试图在bubble_sort内部用sizeof(arr)/sizeof(arr[0])去自动推断长度结果算出来永远是1或者2排序根本排不完全——就是没有意识到这个传参的本质。4. 运行验证与高频错误排查我用经验帮你把该踩的坑提前踩完4.1 输入环节的注意事项scanf别犯低级错误很多排序写对了程序却表现异常问题往往出在最前面的输入环节。最经典的一个错误是写scanf(%d, nums[i]);忘记加取地址符。scanf的工作机制是往你提供的地址写入数据所以必须传入变量的地址nums[i]。忘记写会让scanf试图往一个随机地址上写数据轻则数据错乱重则直接段错误崩溃。还有个不太明显的小问题当用户在同一行输入10个数用空格或回车隔开都是可以的scanf会跳过空白字符找到下一个数字。但是如果你在输出提示语和scanf之间没有处理好缓冲区问题偶尔会遇到输入好像被跳过的错觉。例如前面如果用过scanf(%c...)缓冲区里残留的回车符可能会被后续的读取吃掉。在这个题目里都是用%d读取整数不受影响但一旦你扩展程序去读字符就要格外注意。4.2 五个最容易出现的错误以及各自的排查思路我总结了一下这道题里出现频率最高的错误基本就是下面五类。我平时带人时几乎每次都能遇到其中至少一个。错误类型典型写法现象修复方法交换写错arr[j] arr[j1]; arr[j1] arr[j];连续出现重复数字原数据丢失用temp临时变量三行交换内层循环越界for(j0; jn; j)程序崩溃或结果异常可能访问了arr[n]边界改成j n-1-i外层轮数过多for(i0; in; i)结果大概率正确但多一轮无用功改成i n-1scanf漏地址符scanf(%d, nums[i])运行时崩溃或读入乱码改成scanf(%d, nums[i])函数内计算数组长度sizeof(arr)/sizeof(arr[0])排序不完全n被算成1或2由调用者显式传入n这五种错误里交换写错是最隐蔽的因为它不一定会崩溃程序安静地执行完输出的结果却是错的。你盯着屏幕看半天也可能想不到是交换逻辑的问题只会觉得我确实比较了啊。所以我有一个建议出问题时先用一组极小的数据做测试比如输入5 3 1 2 4这5个数手动算好每一轮的结果然后看程序每一轮输出的中间状态就能很快定位到底哪一步开始不对。4.3 调试技巧用printf把每一轮结果打出来很多人不知道冒泡排序是最容易用printf调试的算法。我分享一下自己的调试方式在bubble_sort函数的内层循环结束之后临时加一行代码// 打印每一轮的结果 printf(第%d轮排序结果, i 1); for (int k 0; k n; k) { printf(%d , arr[k]); } printf(\n);如果你输入的是9 3 5 1 7 2 8 4 6 0你会看到第一轮结束时0并不是立刻跑到最前面而是最大值9先到了最后一位。这是很多人第一次认识冒泡排序时的反直觉点。通过观察每一轮打印出的序列你可以很直观地判断每一轮过后最后一位是不是当前范围内最大的数比较范围是否在逐轮正确缩小哪一轮开始数据就不再变化这说明已经有序调试完之后记得把临时打印的代码删掉或者用宏开关包起来不要留在最终的交付代码里。5. 冒泡排序的优化与横向对比它凭什么没被淘汰5.1 提前结束机制当某一轮没有发生任何交换时数组已经有序标准冒泡排序有一个非常明显的缺陷即使数组已经有序它依然会把N-1轮全部走完做约N(N-1)/2次比较但一次交换都不发生。这在数据量大的时候完全是浪费。有一个极其简单且有效的优化思路如果某一轮遍历过程中一次交换都没有发生那就说明整个数组已经有序可以立刻终止。实现方式是在每一轮开始前设置一个标志只要发生交换就改变标志循环结束后检查这个标志。void bubble_sort_optimized(int arr[], int n) { int i, j, temp; int swapped; for (i 0; i n - 1; i) { swapped 0; // 每轮开始时假设没有交换 for (j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; // 发生了交换 } } // 如果没有发生任何交换说明已经有序 if (swapped 0) { break; } } }这个优化让冒泡排序在最好情况下的时间复杂度从O(n²)降到了O(n)也就是当输入数据本来就是有序的时候只需要一趟遍历就可以确认并结束。对于教学场景来说这个优化算是对算法效率的第一次直观体验。5.2 复杂度分析为什么它只适合小规模数据知道了原理之后应该能自己推导时间复杂度。假设数组长度为n第一轮内层循环比较n-1次第二轮比较n-2次依次递减第n-1轮比较1次总比较次数约等于 (n-1) (n-2) ... 1 n(n-1)/2因此冒泡排序的时间复杂度是O(n²)。无论数组是否有序比较次数都是n(n-1)/2这一量级加上提前结束优化后最好情况为O(n)最坏和平均情况依旧是O(n²)。空间复杂度方面因为只用了一个临时变量不随数据规模增长所以空间复杂度为O(1)。它是原地排序。稳定性上如果两个相邻元素相等我们不交换它们因此相等元素的相对顺序不会改变所以它是稳定排序。这些性质单独看没什么但每次我都会和新人强调一个结论O(n²)级别的排序算法只适合数据量在几千以内的小场景。当数据量到几万以上冒泡排序的表现就会明显吃力这时候应该转向更快的排序算法。不过这不意味着冒泡没有学习的价值——它的实现简单、行为直观是理解交换排序思想的最佳入口。5.3 和选择排序、插入排序放在一起看学冒泡的时候最忌讳孤立地学把它和另外两个O(n²)级别的排序对比一下很多概念立刻清晰了。算法核心思想最好情况最坏情况稳定性特点冒泡排序相邻比较大数后移O(n)优化后O(n²)稳定最直观交换次数多选择排序每轮找最小放到前面O(n²)O(n²)不稳定交换次数少但无法提前结束插入排序将元素插入已排序部分O(n)O(n²)稳定对近乎有序的数据非常快这个对比给我们一个很有用的启发没有绝对最好的算法只有最匹配场景的算法。如果你需要保持相等元素的先后顺序那冒泡和插入可以如果你只想尽量减少数据交换次数选择排序往往会好一些如果数据是近乎有序的插入排序表现得简直像开挂一样。我在我自己写的工具代码里很少直接裸写冒泡排序但当我需要处理的数据量非常小例如排序一个固定长度的配置项或者代码对可读性要求特别高、性能不是瓶颈时冒泡依然会被我从工具箱里拿出来。这也是我觉得初学者不该跳过它的原因——你永远不知道什么时候一个稳定又直观的排序方法就是最合适的选择。6. 从这道题延伸出去的思考数组传参、复用逻辑和函数封装6.1 为什么这道题值得反复练习到闭眼能写我经常对新手说一句话计算机的基础题要练到形成肌肉记忆为止。什么叫肌肉记忆不是背代码而是当你拿到实现冒泡排序这个需求时脑子里直接浮现出三层东西轮数控制、边界计算、交换三连。不用再从头推导一遍内层循环到底该写成n还是n-1-i。反复练习这道题还有一个间接好处以后遇到快速排序、归并排序甚至堆排序你不需要重新理解排序在程序里是做什么的这个问题因为框架已经在了——你需要学的只是换一种拆分和合并的策略。冒泡排序是那棵树的树根其他排序算法是长在树根上的枝杈。我建议一个练习方法不看任何参考代码把这个题目用三种方式各写一遍。第一遍是今天这篇的标准写法第二遍加上提前结束优化第三遍把函数改成接受任意长度数组的泛化版本。三遍写完这个知识点基本就焊死在你的脑回路里了。6.2 把排序函数变得更通用你已经开始摸到工程设计的门槛有同学会问题目要求排10个整数那如果我要排double类型或者排一个结构体数组该怎么办这个问题提得很好因为它触及了函数封装和类型无关的边界。一种做法是写多种类型的排序版本比如bubble_sort_int、bubble_sort_double简单但冗余。另一种做法是用函数指针和void指针让排序函数不知道类型只负责按给定的比较规则交换数据。这是C语言里实现泛型函数的经典思路也是后续理解qsort的关键。我做一个简单的演示把排序函数改造成接受比较器的方式void bubble_sort_generic(void *base, int n, int size, int (*compare)(const void *, const void *)) { int i, j; char *arr (char *)base; // 按字节访问 for (i 0; i n - 1; i) { for (j 0; j n - 1 - i; j) { char *elem1 arr j * size; char *elem2 arr (j 1) * size; if (compare(elem1, elem2) 0) { // 逐字节交换 for (int k 0; k size; k) { char temp elem1[k]; elem1[k] elem2[k]; elem2[k] temp; } } } } }这段代码里的size是每个元素占的字节数compare是外部传入的比较函数通过返回正负来决定是否交换。这样的设计让一个排序函数可以处理任何类型的数据。虽然它比固定int版本的冒泡要复杂不少但思路并不神秘——就是我们把比较方式和元素大小这两件事也抽象成了参数。如果你能看懂这个版本是怎么一步步从固定类型版本演化出来的那你的C语言水平已经超过相当一部分人了。编写一个冒泡排序函数然后调用它对10个整数排序题目本身极其简单但如果你愿意把注意力从解题转移到理解机制上你会发现它背后连着的是函数、数组、指针、复杂度分析甚至泛型设计的整条知识链。我自己在回顾这道题时最大的体会是会背答案和真正理解在代码里是很容易被辨别出来的。能把边界条件讲清楚的人遇到问题时知道从哪开始排查的人和只会默写代码的人面对同样的报错信息反应速度完全不一样。希望这篇文章能帮你把这道题从会写推进到懂写那接下来不管是学更复杂的算法还是开始做真正的项目你都会走得更稳。
阅读完成 · 觉得有帮助?
咨询建站