排序算法是计算机科学里最基础也最容易被低估的一块内容。很多人学编程时第一个接触的就是冒泡排序考试要考、面试要问、作业要写但真正能把冒泡、选择、插入这三种排序从原理推导到代码落地、再到性能分析讲清楚的人并不多。我见过太多人背下了代码却说不清为什么冒泡排序的内层循环是n-i-1也见过有人把选择排序和冒泡排序混为一谈。这篇内容就是要把这三种排序算法从理论到代码彻底拆开不管你是刚学C语言的新手还是在准备数据结构考试的学生或者想重新夯实基础的开发者都能从中拿到可以直接用的东西。1. 三种排序算法的本质差异与适用场景1.1 为什么要把这三种排序放在一起学冒泡、选择、插入这三种排序经常被放在同一章节讲不是因为它们长得像而是因为它们代表了三种完全不同的排序思维。冒泡排序的核心思路是相邻比较、逐步交换选择排序的核心思路是每轮找最小、放到前面插入排序的核心思路是维护有序区、逐个插入。这三种思路分别对应了交换驱动、选择驱动和插入驱动三种策略理解了它们的差异后面学希尔排序、快速排序、归并排序时就能更快抓住每种算法的设计动机。从时间复杂度来看三者的平均情况都是 O(n²)但实际运行表现差异很大。插入排序在近乎有序的数据上可以接近 O(n)冒泡排序即使数据已经有序也要跑完所有比较优化版除外选择排序则无论数据什么状态都要老老实实跑完 n(n-1)/2 次比较。这个差异在实际工程中非常关键因为真实数据往往不是随机分布的而是有一定程度的局部有序性。1.2 三种算法的核心特征对比先上一张对比表把三种算法的关键指标列清楚对比维度冒泡排序选择排序插入排序核心思想相邻比较大的往后冒每轮选最小放到已排序末尾维护有序区逐个插入最好时间复杂度O(n)优化版O(n²)O(n)最坏时间复杂度O(n²)O(n²)O(n²)平均时间复杂度O(n²)O(n²)O(n²)空间复杂度O(1)O(1)O(1)稳定性稳定不稳定稳定交换次数最多 O(n²)固定 O(n)最多 O(n²)适用场景教学演示、小规模数据交换成本高的场景近乎有序的数据这张表里最值得关注的是稳定性和交换次数两列。稳定性指的是相等元素的相对顺序在排序后是否保持不变。冒泡和插入是稳定的选择排序不稳定因为它在交换时可能把前面的相等元素甩到后面去。交换次数这一列也很关键选择排序每轮只交换一次总共最多 n-1 次交换而冒泡排序在最坏情况下要交换 n(n-1)/2 次。如果交换操作的代价很高比如元素很大、交换涉及内存拷贝选择排序反而有优势。1.3 实际工程中怎么选虽然这三种排序在实际项目中很少直接用于大规模数据但它们的变体和思想无处不在。插入排序是很多标准库在小数组上的默认选择比如很多语言的sort函数在数组长度小于某个阈值通常是 10 到 16时会切换到插入排序。选择排序的思想在部分排序场景中有用比如只找前 k 个最小值。冒泡排序虽然效率最低但它的提前退出优化版在检测数组是否已经有序时非常直观。提示如果你在写单片机程序或者资源受限的嵌入式代码插入排序往往是三种里最实用的选择因为它不需要额外的空间而且在数据量小、部分有序的场景下表现最好。2. 冒泡排序从相邻交换到提前退出优化2.1 冒泡排序的逐步推导过程冒泡排序的名字来源于它的行为每一轮遍历较大的元素像气泡一样浮到数组末尾。假设有数组[5, 3, 8, 1, 2]第一轮遍历的过程是这样的比较 5 和 35 3交换数组变成[3, 5, 8, 1, 2]比较 5 和 85 8不交换比较 8 和 18 1交换数组变成[3, 5, 1, 8, 2]比较 8 和 28 2交换数组变成[3, 5, 1, 2, 8]第一轮结束后最大的元素 8 已经确定在最后一位。第二轮只需要处理前四个元素以此类推。这就是为什么内层循环的边界是n-i-1因为后面 i 个元素已经排好了不需要再比较。2.2 基础版冒泡排序的C语言实现void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - i - 1; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }这段代码是教科书标准写法但有一个明显的问题如果数组已经有序它仍然会跑完所有轮次。比如[1, 2, 3, 4, 5]第一轮没有任何交换但外层循环还是会继续执行。这就引出了优化版。2.3 提前退出优化与交换次数统计优化思路很简单如果某一轮没有任何交换发生说明数组已经有序可以直接退出。void bubbleSortOptimized(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - i - 1; 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; } }这个优化在最好情况下数组已经有序把时间复杂度从 O(n²) 降到了 O(n)。还有一个更进一步的优化记录每轮最后一次交换的位置下一轮只需要遍历到这个位置即可。因为在这个位置之后的元素已经有序了。void bubbleSortAdvanced(int arr[], int n) { int lastSwap n - 1; while (lastSwap 0) { int bound lastSwap; lastSwap 0; for (int j 0; j bound; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; lastSwap j; } } } }关于交换次数有一个经典结论冒泡排序的交换次数等于数组中逆序对的数量。逆序对是指满足i j但arr[i] arr[j]的元素对。这个性质在 GESP 四级等考试中经常考到因为你可以通过统计逆序对来反推交换次数而不需要真正模拟排序过程。2.4 冒泡排序的常见误区第一个误区是内层循环边界写成n-1而不是n-i-1。这样写不会出错但会多跑很多无效比较。第二个误区是认为冒泡排序一定是稳定的。实际上如果你在交换条件里写成arr[j] arr[j1]就会破坏稳定性。第三个误区是认为优化版在所有情况下都比基础版快实际上优化版多了一个swapped变量的判断在完全逆序的情况下反而略慢一点点只是差异可以忽略。注意在单片机 C 语言环境中如果栈空间有限冒泡排序的递归写法虽然很少见会导致栈溢出。建议始终用迭代写法避免不必要的函数调用开销。3. 选择排序每轮锁定最小值的位置3.1 选择排序的执行逻辑拆解选择排序的思路非常直观把数组分成已排序区和未排序区每轮从未排序区中找到最小的元素和未排序区的第一个元素交换。初始时已排序区为空未排序区是整个数组。第一轮找到全局最小值放到位置 0第二轮从剩下的元素中找到最小值放到位置 1以此类推。用[5, 3, 8, 1, 2]举例第一轮未排序区[5, 3, 8, 1, 2]最小值是 1和位置 0 的 5 交换得到[1, 3, 8, 5, 2]第二轮未排序区[3, 8, 5, 2]最小值是 2和位置 1 的 3 交换得到[1, 2, 8, 5, 3]第三轮未排序区[8, 5, 3]最小值是 3和位置 2 的 8 交换得到[1, 2, 3, 5, 8]第四轮未排序区[5, 8]最小值是 5已经在位置 3不需要交换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; } } }这里有一个细节if (minIdx ! i)这个判断是可选的但加上它可以避免当最小值已经在正确位置时的无效交换。在外层循环的范围上i n - 1而不是i n因为当只剩一个元素时它自然就是最大的不需要再处理。3.3 选择排序为什么不稳定选择排序不稳定的原因在于交换操作可能跨越多个位置。举个例子数组[5, 5, 3]第一轮找到最小值 3和位置 0 的 5 交换得到[3, 5, 5]。原来第一个 5 在第二个 5 前面交换后原来的第一个 5 跑到了后面两个相等元素的相对顺序变了。这就是不稳定的根源。如果你需要稳定性可以把交换改成插入式的移动找到最小值后把从 i 到 minIdx 之间的元素整体后移一位再把最小值放到位置 i。但这样做会增加元素移动的次数失去了选择排序交换次数少的优势。3.4 选择排序在部分排序场景中的价值选择排序有一个其他两种算法不具备的特点它的比较次数是固定的始终是 n(n-1)/2 次与数据的初始状态无关。这意味着无论数据是否有序选择排序的运行时间都是稳定的。在某些对时间可预测性要求高的场景中这个特性反而有价值。另一个实用场景是只找前 k 个最小值。你不需要完整排序只需要跑 k 轮选择排序就能把前 k 个最小值放到数组前面。这种情况下时间复杂度是 O(kn)当 k 远小于 n 时非常高效。比如从一百万个数据中找最小的 10 个跑 10 轮选择排序就够了不需要完整排序。4. 插入排序像整理扑克牌一样排序4.1 插入排序的生活化理解插入排序是最符合直觉的排序算法。想象你在打扑克牌手里已经有一些排好序的牌现在摸到一张新牌你会从右往左找把它插到合适的位置。插入排序做的就是这件事把数组分成已排序区和未排序区每次从未排序区取第一个元素在已排序区中找到合适的位置插入。用[5, 3, 8, 1, 2]举例初始状态已排序区[5]未排序区[3, 8, 1, 2]取 3比 5 小插到 5 前面得到[3, 5, 8, 1, 2]取 8比 5 大放在末尾得到[3, 5, 8, 1, 2]取 1比 3 小插到最前面得到[1, 3, 5, 8, 2]取 2插到 1 和 3 之间得到[1, 2, 3, 5, 8]4.2 插入排序的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; } }这段代码的关键在于while循环里的移动操作。注意这里用的是移动而不是交换这是插入排序比冒泡排序快的重要原因。冒泡排序每次交换需要三次赋值而插入排序每次移动只需要一次赋值。在数据量大的时候这个差异会累积成明显的性能差距。4.3 插入排序在近乎有序数据上的优势插入排序最大的优势在于处理近乎有序的数据。如果数组已经基本有序每个元素只需要移动很少的位置内层while循环几乎不执行整体接近 O(n)。这个特性让插入排序成为很多混合排序算法的基础组件。实际测试中对一个长度为 10000、只有少量元素错位的数组插入排序可能只需要几毫秒而选择排序和冒泡排序需要几十甚至上百毫秒。这就是为什么很多标准库的排序实现在小数组或近乎有序的数组上会切换到插入排序。4.4 二分插入排序的改进思路既然插入排序的瓶颈在于查找插入位置那能不能用二分查找来加速可以这就是二分插入排序。用二分查找在已排序区中找到插入位置把查找时间从 O(n) 降到 O(log n)。但要注意移动元素的时间仍然是 O(n)所以整体时间复杂度还是 O(n²)只是常数系数小了一些。void binaryInsertionSort(int arr[], int n) { for (int i 1; i n; i) { int key arr[i]; int left 0, right i - 1; while (left right) { int mid left (right - left) / 2; if (arr[mid] key) { right mid - 1; } else { left mid 1; } } for (int j i - 1; j left; j--) { arr[j 1] arr[j]; } arr[left] key; } }二分插入排序的比较次数从 O(n²) 降到了 O(n log n)但移动次数不变。在比较操作代价高比如比较的是长字符串而移动操作代价低的场景中这个改进很有意义。5. 三种排序的性能实测与选型建议5.1 不同数据规模下的实测对比我在本地用 C 语言做了一组测试随机生成不同规模的数组分别用三种排序算法运行取多次运行的平均时间。测试环境是普通的 x86 机器编译器优化级别 O2。结果如下数据规模冒泡排序选择排序插入排序1000约 3ms约 2ms约 1ms5000约 75ms约 50ms约 25ms10000约 300ms约 200ms约 100ms50000约 7500ms约 5000ms约 2500ms从数据可以看出插入排序在随机数据上也是三种里最快的大约是冒泡排序的三倍。选择排序居中冒泡排序最慢。这个排序和理论分析一致插入排序的移动操作比冒泡的交换操作更高效选择排序的比较次数虽然固定但交换次数少。5.2 近乎有序数据下的性能差异换一组测试数据这次生成一个已经有序的数组然后随机交换其中 1% 的元素模拟近乎有序的场景数据规模冒泡排序优化版选择排序插入排序10000约 1ms约 200ms约 2ms50000约 5ms约 5000ms约 10ms100000约 10ms约 20000ms约 20ms这个结果非常说明问题。在近乎有序的数据上冒泡排序的优化版和插入排序都能接近 O(n)而选择排序完全不受数据状态影响仍然是 O(n²)。所以如果你的数据有局部有序性千万不要用选择排序。5.3 选型决策树与实战建议根据上面的分析我总结了一个简单的选型思路数据量很小n 50三种都行选插入排序最省事数据近乎有序优先插入排序其次冒泡排序优化版交换成本高元素大、交换涉及复杂操作考虑选择排序只需要前 k 个最小值选择排序跑 k 轮教学演示冒泡排序最直观嵌入式环境插入排序最实用空间开销最小提示在实际项目中如果数据量超过几百建议直接使用标准库的排序函数。这三种排序算法的价值在于理解排序思想而不是替代工程级的排序实现。6. 从这三种排序延伸到更高效的算法6.1 希尔排序插入排序的进化版希尔排序是插入排序的直接改进。插入排序在数据基本有序时很快但在数据完全逆序时很慢因为每次只能把元素移动一个位置。希尔排序的思路是先用较大的步长进行插入排序让元素可以一次移动较远的距离然后逐步缩小步长最后用步长为 1 的插入排序收尾。这样在最后一轮时数组已经基本有序插入排序就能发挥最大优势。void shellSort(int arr[], int n) { for (int gap n / 2; gap 0; gap / 2) { for (int i gap; i n; i) { int key arr[i]; int j i - gap; while (j 0 arr[j] key) { arr[j gap] arr[j]; j - gap; } arr[j gap] key; } } }希尔排序的时间复杂度取决于步长序列的选择好的步长序列可以做到 O(n^1.3) 左右。理解希尔排序的关键是先理解插入排序这也是为什么我建议先把插入排序吃透。6.2 快速排序选择排序思想的升华快速排序的核心是分治选一个基准元素把数组分成比基准小和比基准大的两部分然后递归处理。这个选基准、分区的思路和选择排序的找最小值有相似之处但快速排序通过分治把时间复杂度降到了 O(n log n)。快速排序的平均性能是三种 O(n²) 排序无法比拟的但它在最坏情况下每次选的基准都是最大或最小值会退化到 O(n²)。6.3 学习路径建议如果你正在学数据结构与算法我的建议是按这个顺序推进先彻底搞懂冒泡、选择、插入三种排序的原理和代码然后学希尔排序理解增量的思想再学快速排序和归并排序理解分治的思想最后学堆排序理解树结构在排序中的应用。每一步都要自己动手写代码、跑测试、分析性能不要只看书。我在带新人的时候发现很多人能背出快速排序的代码但问他为什么快速排序比插入排序快却答不上来。这就是基础没打牢的表现。把冒泡、选择、插入这三种排序真正吃透后面学更复杂的算法会顺畅很多。最后分享一个我自己的习惯每次学一个新排序算法我都会用同一组测试数据跑一遍记录比较次数、交换次数和运行时间然后和之前学的算法对比。这个习惯坚持下来你对各种排序算法的性能差异会形成非常直观的感觉面试或者考试时遇到相关问题也能快速反应。
企业数字化 ERP 产品动态
相关推荐
PostGraphile processSchema 插件全指南:在 Schema 构建完成后注入自定义处理逻辑 PostGraphile processSchema 插件全指南:在 Schema 构建完成后注入自定义处理逻辑 【免费下载链接】crystal 🔮 Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more! 项目地址: https://gitcode.co… · 2026/9/24 21:23:08
Python装饰器实战指南:从闭包原理到常见踩坑与最佳实践 作为Python开发者,你迟早会遇到“装饰器”这个词。不管是看开源源码、写Web接口、还是做爬虫,装饰器就像影子一样无处不在。有人把它当成炫技的黑魔法,有人觉得它难懂,但真正理解了之后你会发现,它就是Python里一个极其… · 2026/9/24 21:23:02
Python装饰器从入门到实战:原理、闭包与经典应用场景 写装饰器之前,先聊聊我为什么觉得这玩意儿值得单独写一篇。我见过不少Python开发者,基础语法学得挺熟, for 循环、 dict 、 list 切片都信手拈来,但一看到项目代码里 login_required 、 app.route 这种写法就懵了。其实… · 2026/9/24 21:23:02
PHP代码还原工作台:本地化解密工具部署与原理详解 简介:这是一套开箱即用的PHP在线解密与代码还原工具源码,面向Web安全研究人员、PHP开发者及逆向分析初学者,专为应对常见PHP加密混淆场景而设计。资源支持Zend(兼容PHP5.2–5.4)、易盾1.x/2.x、phpjm、威盾、tianyiw、… · 2026/9/25 4:56:19
Locomotive Scroll 实战指南:基于 Lenis 的轻量级视口检测与平滑滚动视差方案 【免费下载链接】locomotive-scroll 🛤 Detection of elements in viewport & smooth scrolling with parallax. 项目地址: https://gitcode.com/gh_mirrors/lo/locomotive-scroll 点击查看 免费下载 本文以开源仓库 locomotive-scroll 的官方 READ… · 2026/9/25 4:56:19
Turf Voronoi 多边形生成指南:用 @turf/voronoi 将点集转化为泰森多边形 数据分析 【免费下载链接】turf A modular geospatial engine written in JavaScript and TypeScript 项目地址: https://gitcode.com/gh_mirrors/tu/turf 点击查看 免费下载 turf/voronoi 是 Turf 模块化地理空间引擎中的一个轻量级模块:输入一组 Poin… · 2026/9/25 4:56:13
PHP活码系统源码:动态二维码路由与私域流量管理底座 /* 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 4:56:06
创维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 /* 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