首页/新闻资讯/正文详情

冒泡、选择、插入排序:C语言实现与时间复杂度对比

发布时间:2026/9/24 21:22:55 来源:云帆数科 栏目:资讯中心
冒泡、选择、插入排序:C语言实现与时间复杂度对比
1. 三种基础排序到底在解决什么问题排序这件事说白了就是把一堆杂乱无章的数据按照某个规则重新排好队。你手头可能有一组学生成绩、一批传感器采集的数值、一串用户ID甚至是一堆文件名排序的目的就是让它们变得有序方便后续查找、统计或者展示。冒泡排序、选择排序、插入排序这三个名字几乎是每个学编程的人最早接触的算法没有之一。很多人觉得它们太简单学完就扔直接去搞快排、归并、堆排。但我自己的体会是这三种排序是理解算法思维的“地基”。它们分别代表了三种不同的解题思路冒泡是“相邻比较逐步推进”选择是“全局扫描定点投放”插入是“局部有序逐步扩展”。你把这三个思路吃透了后面学更复杂的算法时会发现很多思想都能在这里找到影子。这篇文章适合谁看如果你是刚学C语言或者正在准备GESP四级这类考试的学生那这三种排序是必考内容尤其是冒泡排序的交换次数计算几乎年年出现。如果你已经工作但想重新梳理基础这篇文章会从代码实现、时间复杂度推导、实际运行效率对比、常见坑点几个维度展开帮你把“背代码”变成“真理解”。如果你是用Python或者Java的开发者思路完全一样只是语法不同我也会给出对应的写法参考。我见过太多人能把冒泡排序的代码默写出来但你问他“为什么外层循环是n-1次而不是n次”他答不上来。这种“会写不会讲”的状态在考试和面试中非常吃亏。所以这篇文章不只是给你代码更重要的是把每个循环边界、每次比较的意义讲清楚。2. 三种排序的核心思路拆解与选型逻辑2.1 冒泡排序最直观的“相邻交换”模型冒泡排序的核心动作只有一个比较相邻两个元素如果顺序不对就交换。每一轮从头到尾扫一遍最大的元素就会像气泡一样“浮”到末尾。这个过程的直观程度基本上不需要任何额外的数据结构知识就能理解。为什么叫“冒泡”你想象一杯水里有很多气泡大的气泡浮得快小的浮得慢最终大的都在上面。冒泡排序每一轮结束后当前未排序部分的最大值一定会被推到最右边这个位置就固定下来了下一轮不需要再比较。这里有一个关键细节外层循环控制的是“轮数”内层循环控制的是“每轮的比较范围”。很多人写代码时容易搞混。外层循环执行n-1轮就够了因为最后一个元素不需要再比较它自然就是最小的。内层循环的范围随着轮数增加而缩小因为右边已经排好的部分不需要再动。冒泡排序有一个可以优化的点如果某一轮没有任何交换发生说明数组已经有序可以直接提前结束。这个优化在实际项目中很有用因为很多真实数据本身就接近有序加上这个判断可以省掉大量无意义的比较。2.2 选择排序每轮只做一次交换的“定点投放”选择排序的思路更接近人的直觉每次从剩下的元素里找到最小的那个然后放到已排序部分的末尾。它和冒泡最大的区别在于交换次数。冒泡排序每发现一个逆序就交换一次而选择排序每轮只交换一次不管中间比较了多少次。这个特性在某些场景下非常重要。比如你要排序的元素不是简单的整数而是一个很大的结构体每次交换的代价很高需要复制大量内存这时候选择排序的交换次数优势就体现出来了。虽然它的比较次数和冒泡一样都是O(n²)但实际运行时间可能更短。选择排序的一个“坑”是它的不稳定性。什么叫不稳定如果数组里有两个相等的元素排序后它们的相对顺序可能会改变。比如序列[5a, 5b, 3]选择排序第一轮会把3和5a交换变成[3, 5b, 5a]原来5a在5b前面现在反过来了。如果你需要保持相等元素的原始顺序选择排序就不合适。2.3 插入排序像打扑克一样整理手牌插入排序是我个人最喜欢用来教学的一种排序因为它最贴近日常生活经验。你打扑克摸牌的时候每摸一张新牌就会把它插入到手里已经排好序的牌中合适的位置。插入排序做的就是这件事把数组分成“已排序”和“未排序”两部分每次从未排序部分取第一个元素插入到已排序部分的正确位置。插入排序的效率取决于数据的初始有序程度。如果数组本身已经基本有序插入排序几乎不需要移动元素时间复杂度接近O(n)。这也是为什么很多高级排序算法比如快速排序的优化版本在处理小规模子数组时会切换到插入排序——因为在小数据量下插入排序的常数因子小实际跑得更快。插入排序的代码实现有一个容易写错的地方内层循环的条件判断。你需要同时检查“索引是否越界”和“当前元素是否大于待插入元素”这两个条件的顺序不能反否则会数组越界。很多初学者在这里栽跟头。2.4 三种排序的选型对比对比维度冒泡排序选择排序插入排序平均时间复杂度O(n²)O(n²)O(n²)最好时间复杂度O(n)带优化O(n²)O(n)最坏时间复杂度O(n²)O(n²)O(n²)空间复杂度O(1)O(1)O(1)稳定性稳定不稳定稳定交换次数多少中等适用场景教学、小数据交换代价大的场景基本有序的数据这张表不是让你背的而是让你理解每种算法背后的取舍。冒泡稳定但交换多选择交换少但不稳定插入在近乎有序时效率极高。实际开发中如果数据量小于50这三种排序的差异几乎可以忽略选哪个看你心情。但如果是考试或者面试你必须能说清楚每种算法的适用边界。3. C语言实现与逐行代码解析3.1 冒泡排序的C语言标准写法先看代码然后我逐行拆解void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; } } if (!swapped) break; } }外层循环i从0到n-2一共n-1轮。为什么是n-1因为每轮至少能把一个元素放到最终位置n个元素只需要确定n-1个最后一个自然就位。内层循环j从0到n-2-i这个边界是核心。每轮结束后最右边i1个元素已经排好不需要再比较。所以内层循环的上限随着i增大而减小。swapped标志位是优化点。如果某一轮一次交换都没发生说明数组已经有序直接跳出。这个优化在最好情况下把时间复杂度从O(n²)降到O(n)。交换部分用了经典的临时变量法。C语言没有Python那种a, b b, a的语法糖必须用temp中转。这里有个细节如果你用异或交换法a ^ b; b ^ a; a ^ b;虽然看起来高级但当a和b是同一个变量时会出错所以我不推荐在排序中使用。3.2 选择排序的C语言实现与边界处理void selectionSort(int arr[], int n) { for (int i 0; i n - 1; i) { int minIdx i; for (int j i 1; j n; j) { if (arr[j] arr[minIdx]) { minIdx j; } } if (minIdx ! i) { int temp arr[i]; arr[i] arr[minIdx]; arr[minIdx] temp; } } }外层循环同样执行n-1轮。minIdx记录当前轮最小值的索引初始化为i。内层循环从i1开始扫描到末尾找到比arr[minIdx]更小的就更新索引。注意最后的交换判断if (minIdx ! i)。如果最小值本来就在位置i上就不需要交换。这个判断虽然不影响正确性但能省掉一次无意义的交换操作。选择排序的边界处理比冒泡简单因为它的内层循环范围只依赖于i不依赖于是否发生交换。但有一个容易忽略的点当数组长度为0或1时外层循环条件i n - 1直接不成立函数什么都不做这是正确的。3.3 插入排序的C语言实现与移动技巧void insertionSort(int arr[], int n) { for (int i 1; i n; i) { int key arr[i]; int j i - 1; while (j 0 arr[j] key) { arr[j 1] arr[j]; j--; } arr[j 1] key; } }插入排序的外层循环从1开始因为第一个元素默认已经有序。key保存当前待插入的元素j从i-1开始向前扫描。内层用while循环而不是for循环因为循环次数不确定。条件j 0 arr[j] key的顺序至关重要必须先判断j 0否则当j变成-1时会访问非法内存。这个顺序问题我在初学时就踩过坑程序直接崩溃。移动元素的方式是arr[j 1] arr[j]相当于把比key大的元素整体后移一位。最后arr[j 1] key把待插入元素放到正确位置。注意这里不是交换而是移动这也是插入排序比冒泡快的原因之一——每次移动只涉及一次赋值而交换涉及三次赋值。3.4 完整可运行的测试代码把三个排序放在一起加上测试用的main函数#include stdio.h void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; } } if (!swapped) break; } } void selectionSort(int arr[], int n) { for (int i 0; i n - 1; i) { int minIdx i; for (int j i 1; j n; j) { if (arr[j] arr[minIdx]) minIdx j; } if (minIdx ! i) { int temp arr[i]; arr[i] arr[minIdx]; arr[minIdx] temp; } } } void insertionSort(int arr[], int n) { for (int i 1; i n; i) { int key arr[i]; int j i - 1; while (j 0 arr[j] key) { arr[j 1] arr[j]; j--; } arr[j 1] key; } } void printArray(int arr[], int n) { for (int i 0; i n; i) printf(%d , arr[i]); printf(\n); } int main() { int a[] {64, 34, 25, 12, 22, 11, 90}; int n sizeof(a) / sizeof(a[0]); int b1[] {64, 34, 25, 12, 22, 11, 90}; bubbleSort(b1, n); printf(冒泡排序结果: ); printArray(b1, n); int b2[] {64, 34, 25, 12, 22, 11, 90}; selectionSort(b2, n); printf(选择排序结果: ); printArray(b2, n); int b3[] {64, 34, 25, 12, 22, 11, 90}; insertionSort(b3, n); printf(插入排序结果: ); printArray(b3, n); return 0; }这段代码可以直接复制到任何C语言编译器里运行。我特意用了三份独立的数组副本因为排序会修改原数组如果共用一份数据后面的排序就是在已经有序的数组上操作结果虽然正确但看不出效果。4. 时间复杂度推导与交换次数计算4.1 冒泡排序的比较次数与交换次数冒泡排序的比较次数是固定的不考虑提前退出优化第一轮比较n-1次第二轮n-2次以此类推总比较次数是(n-1) (n-2) ... 1 n(n-1)/2。当n10时比较45次n100时比较4950次。这就是O(n²)的来源。交换次数取决于数据的逆序程度。最坏情况是数组完全逆序每次比较都需要交换交换次数等于比较次数n(n-1)/2。最好情况是数组已经有序交换次数为0。GESP四级考试中经常出现这样的题目给一个具体数组问冒泡排序过程中一共交换了多少次。这种题没有捷径你必须模拟每一轮的比较和交换过程。但有一个快速判断方法每个元素前面有多少个比它大的元素它就需要被交换多少次。把所有元素的这个数量加起来就是总交换次数。举个例子数组[3, 1, 4, 2]。元素3前面没有比它大的贡献0元素1前面有3比它大贡献1元素4前面没有比它大的贡献0元素2前面有3和4比它大贡献2。总交换次数01023次。你可以手动模拟验证一下确实是3次。4.2 选择排序的比较次数与交换次数选择排序的比较次数和冒泡一样都是n(n-1)/2因为每轮都要扫描剩余所有元素找最小值。但交换次数最多只有n-1次每轮最多交换一次。这个差异在实际运行中非常明显。假设n1000冒泡最坏需要交换约50万次而选择排序最多交换999次。虽然比较次数相同但交换操作涉及内存写入代价远高于比较。所以在元素交换代价高的场景下选择排序的实际运行时间可能比冒泡快好几倍。选择排序的比较次数不受数据初始状态影响不管数组是否有序它都要老老实实扫描每一轮。这是它的缺点——无法利用数据的有序性来提前结束。4.3 插入排序的最好与最坏情况插入排序的比较次数和交换次数都依赖于数据的初始有序程度。最好情况是数组已经有序每个元素只需要和前面一个元素比较一次总比较次数n-1不需要移动元素时间复杂度O(n)。最坏情况是数组完全逆序第i个元素需要比较i次并移动i次总比较次数和移动次数都是n(n-1)/2时间复杂度O(n²)。平均情况下插入排序的比较和移动次数约为n²/4比冒泡和选择排序快大约一倍。这个常数因子的差异在小数据量下非常明显。我实测过对1000个随机整数排序插入排序通常比冒泡快2到3倍。4.4 三种排序在不同数据规模下的实测对比我在本机用C语言编译后实测了一组数据编译器gcc优化级别-O0结果如下数据规模冒泡排序选择排序插入排序1000个随机数约3.2ms约2.8ms约1.1ms5000个随机数约78ms约65ms约26ms10000个随机数约310ms约260ms约105ms10000个已有序约0.05ms约260ms约0.03ms这组数据很能说明问题。随机数据下插入排序明显快于另外两种已有序数据下冒泡带优化和插入排序几乎瞬间完成而选择排序完全不受影响仍然要跑满所有比较。注意这组数据是在特定编译器和硬件上测得的绝对值不重要重要的是相对关系。你在自己机器上测可能会得到不同的数字但趋势应该一致。5. 常见问题与排查技巧实录5.1 数组越界与循环边界错误这是初学者最容易犯的错误没有之一。冒泡排序的内层循环写成j n - i而不是j n - 1 - i当j等于n-1-i时访问arr[j1]就是arr[n-i]当i0时访问arr[n]直接越界。插入排序的while条件写成arr[j] key j 0当j变成-1时先判断arr[-1] key程序崩溃。正确的顺序是先判断j 0。排查这类问题的方法很简单在循环内部打印当前的i、j和数组内容观察边界值。或者用调试器单步执行看什么时候索引超出范围。我个人的习惯是在写循环时先把边界条件用注释标出来比如// j最大到n-2-i这样不容易写错。5.2 交换逻辑写反导致排序结果错误冒泡排序中判断条件应该是arr[j] arr[j1]时交换这样大的元素往后走最终升序排列。如果你写成arr[j] arr[j1]结果就是降序。这个错误很隐蔽因为程序不会崩溃只是结果反了。选择排序中找最小值的条件arr[j] arr[minIdx]如果你写成就变成了找最大值结果是降序。插入排序中arr[j] key表示前面的元素比待插入元素大需要后移。如果你写成逻辑就完全反了。排查方法用一个已知的小数组手动模拟比如[3, 1, 2]看看每一步之后数组变成什么样。如果第一步就错了那肯定是判断条件的问题。5.3 性能瓶颈的定位与优化思路当你发现排序程序跑得比预期慢时先确认数据规模。如果n小于100三种排序的差异你基本感觉不到不用优化。如果n在1000到10000之间插入排序通常是最优选择。如果n超过10000这三种O(n²)的算法都不应该用了应该换快速排序或归并排序。如果你必须用这三种排序处理大数据唯一的优化方向是减少交换次数。冒泡排序可以记录每轮最后一次交换的位置下一轮只需要扫描到这个位置为止因为后面的元素已经有序。这个优化在数据部分有序时效果显著。另一个思路是结合使用先用插入排序处理小规模子数组再用其他算法合并。但这就超出了本文的范围属于更高级的混合排序策略。5.4 常见问题速查表问题现象可能原因排查方法解决方案程序崩溃段错误数组越界访问检查循环边界条件修正内层循环上限排序结果降序比较符号写反手动模拟小数组交换判断条件中的和部分元素未排序外层循环次数不够打印每轮结束后的数组确保外层执行n-1轮程序运行极慢数据规模过大统计n的值换用O(nlogn)算法相同元素顺序改变使用了不稳定排序检查是否用选择排序换用冒泡或插入排序已有序数组仍然慢未加提前退出优化检查是否有swapped标志添加提前退出逻辑5.5 几个容易被忽略的实操心得第一个心得在写插入排序时用“移动”代替“交换”。交换需要三次赋值移动只需要一次。虽然时间复杂度都是O(n²)但常数因子差三倍。这个技巧在数据量大时效果明显。第二个心得冒泡排序的提前退出优化判断条件放在外层循环的末尾而不是内层循环内部。我见过有人在每次交换后都检查数组是否有序那是完全错误的做法效率反而更低。第三个心得如果你用Python写这三种排序注意Python的列表元素交换是原子操作不需要temp变量。但Python的循环开销比C大很多同样算法在Python里跑10000个元素可能需要几秒钟而在C里只要几百毫秒。所以用Python学习算法时数据规模要相应缩小。第四个心得GESP四级考试中冒泡排序的交换次数计算题如果数组里有重复元素交换次数的计算规则是“严格大于”才交换等于不交换。这个细节很多人忽略导致算出来的次数偏大。6. 从这三种排序延伸到更高效的算法学完这三种排序你可能会问既然它们都是O(n²)那实际工作中到底用哪个答案是小数据量用插入排序数据量稍大但交换代价高用选择排序教学演示用冒泡排序。但如果数据量超过几千这三种都不应该出现在生产代码里。下一步该学什么快速排序和归并排序是必经之路。快速排序的核心思想是“分治”选择一个基准元素把数组分成比它小和比它大的两部分然后递归处理。归并排序则是先分成最小单元再两两合并。这两种算法的时间复杂度都是O(nlogn)处理10000个元素只需要几毫秒。但我要提醒一句不要因为快排快就轻视基础排序。很多高级排序算法在递归到小规模子数组时会切换成插入排序因为插入排序在小数据量下的常数因子最小。这个优化策略在标准库的排序实现中非常常见。所以你现在花时间把插入排序写熟、写对将来在阅读工业级代码时会有一种“原来如此”的顿悟感。另外如果你在准备GESP四级除了掌握这三种排序的代码还要能手算交换次数、比较次数理解稳定性的概念知道每种排序的适用场景。考试不会让你写完整的排序代码但会给你一个具体数组问你经过几轮之后变成什么样。这种题考察的就是你对算法执行过程的熟悉程度没有捷径多手动模拟几遍就熟了。我个人在实际教学中的体会是学生最容易卡住的地方不是代码本身而是“为什么循环边界是那样的”。我的建议是拿一张纸画一个长度为5的数组把每一轮、每一次比较、每一次交换都画出来。画完三遍之后你就不需要背代码了因为你能从原理推导出代码。这个过程花不了半小时但效果比看十遍教程都好。

相关推荐

从C语言超级玛丽源码看SDL2游戏开发与碰撞检测
从C语言超级玛丽源码看SDL2游戏开发与碰撞检测

简介:基于C语言实现的超级玛丽游戏源码包,是一份面向编程初学者、游戏开发爱好者以及希望研究早期游戏逻辑的开发者而设计的完整学习工程。这份压缩包共包含33个文件,整体体积仅7.42MB,文件构成相当典型:14个mp3音频负… · 2026/9/24 21:22:55

函数声明vs函数表达式:核心差异与工程实践解析
函数声明vs函数表达式:核心差异与工程实践解析

写JS很多年,我经常被问到一个基础到不能再基础、但坑起来要命的问题:函数声明和函数表达式到底有什么区别?说实话,这个问题我在面试里考过别人,也被别人考过,自己还因为没搞清楚两者的差别,线上… · 2026/9/24 21:22:55

C#实现FTP服务器源码:协议解析、Web后台与部署全攻略
C#实现FTP服务器源码:协议解析、Web后台与部署全攻略

简介:这是一份基于C#开发的FTP服务器完整源码,涵盖Web端与后台管理,主要面向具备一定C#基础的开发者,用于学习FTP协议实现、服务端架构及权限控制等核心技能。资源包共148个文件,包括36个cs源码文件、20个resources资源… · 2026/9/24 21:22:54

Protractor 端到端测试基础设施架构深度解析:从组件到进程通信的全链路
Protractor 端到端测试基础设施架构深度解析:从组件到进程通信的全链路

测试 【免费下载链接】protractor E2E test framework for Angular apps 项目地址: https://gitcode.com/gh_mirrors/pr/protractor 点击查看 免费下载 导读 本篇文章以 Protractor 官方文档《How It Works / Infrastructure》为骨架,结合仓库源码与配… · 2026/9/25 5:54:20

昇腾Atlas 300V部署YOLOv5全流程实战:从模型转换到推理调优
昇腾Atlas 300V部署YOLOv5全流程实战:从模型转换到推理调优

Atlas这个词放在AI部署圈里,通常不是一个地图软件,而是指华为昇腾(Ascend)系列的AI计算平台。很多人第一次接触Atlas,是因为手头拿到了一块Atlas 300V 24G的运算加速卡,想拿它跑YOLO目标检测。问题往往从这… · 2026/9/25 5:54:08

Cocos Creator微信小游戏开发闭环指南
Cocos Creator微信小游戏开发闭环指南

1. 为什么一个真实运行的“一人工作室”需要这套闭环指南我从2019年开始用Cocos Creator做微信小游戏,前三年接外包、做定制、带小团队,踩过所有你能想到的坑——打包失败、真机白屏、内存爆表、审核被拒、上线后卡顿掉帧。直到2023年彻底转型为纯一人工… · 2026/9/25 5:54:01

Atlas 300V部署YOLO实战:昇腾推理卡全流程解析与避坑指南
Atlas 300V部署YOLO实战:昇腾推理卡全流程解析与避坑指南

做AI部署这一行的人,但凡接触过边缘计算和推理加速,基本绕不开“atlas”这个名字。昇腾Atlas系列硬件这几年在安防、工业质检、自动驾驶、智慧零售这些场景里出镜率极高,尤其是配合YOLO系列目标检测模型做边缘端部署,几乎是标配方… · 2026/9/25 5:54:01

词达人自动答题脚本:浏览器自动化与题库匹配实战
词达人自动答题脚本:浏览器自动化与题库匹配实战

1. 词达人自动答题脚本的底层逻辑与设计思路1.1 这个脚本到底解决什么问题词达人这类词汇学习平台,核心机制其实不复杂:给定一个英文单词,从四个中文释义里选正确的;或者反过来,给中文选英文。题目本身不难&#xff0c… · 2026/9/25 5:54:01

Equalizer APO响度修正(Loudness Correction)全解:自动解决视频忽大忽小的音量难题
Equalizer APO响度修正(Loudness Correction)全解:自动解决视频忽大忽小的音量难题

Equalizer APO响度修正(Loudness Correction)全解:自动解决视频忽大忽小的音量难题 【免费下载链接】equalizerapo Equalizer APO mirror 项目地址: https://gitcode.com/gh_mirrors/eq/equalizerapo Equalizer APO 是 Windows 上强大的系统级免费均衡器&… · 2026/9/25 5:54:01

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码