选择排序图解:从原理到C语言实现,一趟搞懂复杂度与稳定性

发布时间:2026/10/9 5:53:31
选择排序图解:从原理到C语言实现,一趟搞懂复杂度与稳定性 七大排序算法里选择排序经常被当成“最没有存在感”的那一个。它不像冒泡排序有反复交换的“动感”也不像快速排序那样顶着分治的光环但在期末复习、考研408乃至面试笔试里它的出镜率一点都不低。这篇就围绕选择排序展开用图解和动图视角把每一轮“选最小值、交换、锁定”的过程拆到最细顺带把比较次数、移动次数、稳定性这些考点一次性讲透——不管你是刚学数据结构的本科生还是正在刷408真题的考研党或者只是为了应付实验报告想快速写出像样代码这篇都能直接拿来用。1. 先把七大排序摆在一起看选择排序到底站在什么位置1.1 七大排序全景图为什么排序是个绕不开的坎数据结构的排序章节表面上是讲“怎么把无序序列变成有序”本质上是在讲“如何通过不同的策略在时间、空间、稳定性之间做取舍”。常见的七大排序通常指冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序。它们被反复比较的维度有三个时间复杂度、空间复杂度、稳定性。排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性冒泡排序O(n²)O(n²)O(1)稳定选择排序O(n²)O(n²)O(1)不稳定插入排序O(n²)O(n²)O(1)稳定希尔排序O(n^1.3)左右O(n²)O(1)不稳定归并排序O(n log n)O(n log n)O(n)稳定快速排序O(n log n)O(n²)O(log n)不稳定堆排序O(n log n)O(n log n)O(1)不稳定这个表我建议你直接背下来因为期末选择填空和408选择题都爱从这里出题。回到选择排序它和冒泡排序、插入排序一样属于“简单排序”但策略完全不同。冒泡排序是“相邻元素打擂台大的往后走”插入排序是“把新元素往前面已排好的序列里插”而选择排序的思路最朴素每一轮从剩下的元素里选出最小的直接放到它最终该待的位置上。1.2 为什么选择排序适合用动图来理解我在给学弟学妹讲排序的时候发现选择排序是最适合用“动图”理解的一个。原因很简单它的每一轮任务都极其清晰——第一轮从整个序列里挑一个最小值放到第一位第二轮从剩余序列里挑一个最小值放到第二位以此类推。每一轮的行动路径就是一条固定长度的“扫描线”扫过去、记下最小值的下标、交换、下一轮。这种“逐步收缩范围”的过程做成动图之后视觉上非常直观你会看到每一轮都有一个元素被“钉死”在序列前部剩下的范围越来越小。相比之下冒泡排序动图虽然也好看但它每轮要发生好多次相邻交换动作很碎归并排序和快排的动图又涉及递归分区初学者容易看晕。选择排序恰恰是那个“看一遍动图就能画出代码”的算法。我把动图拆成关键帧配合每一步的状态表格你会发现整个算法几乎没有藏着掖着的细节。2. 图解选择排序全过程用一组真实数据跑通每一轮2.1 经典案例[29, 10, 14, 37, 13] 的五轮完整推演我拿一个最常见的数组来演示[29, 10, 14, 37, 13]。这个序列不长但足够把选择排序的每一轮行为都看清楚。为了和后面代码对上我用下标 0 到 4 来描述位置。第一轮i0扫描范围 0 到 4把当前最小值假设为下标0的元素 29。然后从下标1开始往后找10 比 29 小于是把最小值下标更新为114 比 10 大不更新37 比 10 大不更新13 比 10 大不更新。这一轮扫描完最小值下标是1对应的值是 10。把它和下标0的 29 交换数组变成[10, 29, 14, 37, 13]。此时下标0的位置已经确定后续不再参与。第二轮i1扫描范围 1 到 4当前假设最小值为下标1的 29。从下标2开始14 比 29 小更新最小值下标为237 比 14 大不更新13 比 14 小更新最小值下标为4。扫完发现最小值是下标4的 13和下标1的 29 交换数组变成[10, 13, 14, 37, 29]。此时下标1确定。第三轮i2扫描范围 2 到 4当前假设最小值为下标2的 14。从下标3开始37 比 14 大不更新29 比 14 大不更新。扫完发现最小值仍然是下标2的 14于是和自己交换——表面上看数组没变化。很多人第一次看动图时会对这一步产生疑惑为什么没有交换动作其实程序逻辑里还是执行了一次“自己和自己交换”只是结果不变。第四轮i3扫描范围 3 到 4当前假设最小值为下标3的 37。从下标4开始29 比 37 小更新最小值下标为4。扫完发现最小值为下标4的 29和下标3的 37 交换数组变成[10, 13, 14, 29, 37]。第五轮i4只剩最后一个元素不需要扫描整个排序结束。2.2 动图关键帧拆解每一轮数组长什么样如果把上面这个过程做成动图每一帧的数组状态是这样的。我习惯把“已排序区间”用横线隔开这样视觉上一目了然轮次排序前状态选中的最小值排序后状态第1轮[29, 10, 14, 37, 13]10下标1[10, 29, 14, 37, 13]第2轮[10, 29, 14, 37, 13]13下标4[10, 13, 14, 37, 29]第3轮[10, 13, 14, 37, 29]14下标2[10, 13, 14, 37, 29]第4轮[10, 13, 14, 37, 29]29下标4[10, 13, 14, 29, 37]第5轮[10, 13, 14, 29, 37]不用选[10, 13, 14, 29, 37]这张表就是动图的核心帧。你把它按顺序连续播放就是一次完整的选择排序动画。我在做实验报告或者PPT的时候就是靠类似这样的表格配上箭头标注来替代动态演示的——如果老师要求交“动图演示”你也可以用Python的matplotlib或者PPT的平滑动画把这几帧连起来效果和程序跑出来的动画没有本质区别。2.3 每轮比较了几次一个很容易数错的小细节选择排序的“比较次数”是一个高频考点。上面这个5元素数组第一轮比较了4次下标1和0比、下标2和1比、下标3和1比、下标4和1比第二轮比较了3次第三轮2次第四轮1次。总比较次数是432110次正好等于n(n-1)/2 5*4/2 10。注意这里说的是“比较次数”不是“交换次数”。比较次数只和元素个数有关和数组本身是否有序完全无关。哪怕输入的是一个已经排好序的数组选择排序依然要老老实实扫描这么多次这一点和后面的复杂度分析直接挂钩期末考试特别爱挖这个坑。3. 手写选择排序代码C语言实现与关键细节拆解3.1 完整可运行的C语言代码说了半天原理直接上代码。我用的是最标准的写法注释写得很细直接抄去实验报告里也能用#include stdio.h // 交换两个int变量的值 void swap(int *a, int *b) { int temp *a; *a *b; *b temp; } // 选择排序升序排列 void selection_sort(int arr[], int n) { int i, j, min_idx; // 外层循环控制已排序区间的末尾位置 // 当只剩下最后一个元素时它必然是有序的所以循环到 n-1 即可 for (i 0; i n - 1; i) { // 假设当前轮次的第一个元素是最小值 min_idx i; // 内层循环在未排序区间 [i1, n) 中寻找真正的最小值下标 for (j i 1; j n; j) { if (arr[j] arr[min_idx]) { min_idx j; } } // 如果最小值不是当前轮次第一个元素才需要交换 // 就算相等交换一次也无伤大雅但加判断可以减少一次无意义的写操作 if (min_idx ! i) { swap(arr[i], arr[min_idx]); } } } // 打印数组 void print_array(int arr[], int n) { for (int i 0; i n; i) { printf(%d , arr[i]);? } printf(\n); } int main() { int arr[] {29, 10, 14, 37, 13}; int n sizeof(arr) / sizeof(arr[0]); printf(排序前: ); print_array(arr, n); selection_sort(arr, n); printf(排序后: ); print_array(arr, n); return 0; }编译运行没有任何问题输出就是排序前: 29 10 14 37 13和排序后: 10 13 14 29 37。3.2 三个容易被忽略的代码细节第一个细节内层循环为什么从i 1开始而不是从i开始因为最小值下标已经初始化为i如果从i开始无非是和自己多比较一次既浪费时间又不改变结果。从i 1开始是标准做法也是减少不必要比较的直观优化。第二个细节交换之前为什么要判断min_idx ! i本质上是为了避免“自己和自己交换”。虽然数字相同的自我交换不会改变数组内容但在某些编程环境下这会造成一次多余的内存写操作。更重要的是当数组规模巨大、元素是复杂对象时交换是有成本的这个判断能让最好情况下的交换次数降到0。第三个细节外层循环为什么是i n - 1而不是i n当排序进行到只剩最后一个元素时它已经在正确的位置上了——前面的 n-1 个元素都放好了最后一个自然是全局最大值。多循环一次只会让自己和自己比较一遍白白浪费时间。这个细节写实验报告时经常被忽略但它其实体现了对算法本质的理解。3.3 用Python写一遍理解“指针思维”和“值思维”的差异很多同学是先学的Python再学C或者反过来。选择排序用Python写只要10行但这10行里藏着一个重要的思维转换def selection_sort(arr): n len(arr) for i in range(n - 1): min_idx i for j in range(i 1, n): if arr[j] arr[min_idx]: min_idx j arr[i], arr[min_idx] arr[min_idx], arr[i] # 直接交换Python的arr[i], arr[min_idx] arr[min_idx], arr[i]是同时赋值不需要临时变量。而在C语言里交换必须借助一个临时变量temp这就需要swap函数。理解了这一点你在写C语言版时就不会忘记那三行交换代码了。4. 复杂度与稳定性为什么选择排序“比较次数恒定”却又“不稳定”4.1 时间复杂度最好和最坏都一样这既是优点也是缺点选择排序的时间复杂度分析是七大排序里最简单的因为它的比较次数和交换次数可以分开算。比较次数无论输入数据是否有序内层循环的比较次数都是固定不变的。第一次要比较n-1次第二次n-2次直到最后一次1次总比较次数是(n-1) (n-2) ... 1 n(n-1)/2。所以时间复杂度里那个O(n²)是“雷打不动”的。交换次数每轮最多交换一次一共n-1轮所以交换次数最多是n-1次。最好情况下数组本身已经有序如果加了min_idx ! i判断交换次数是0不加判断依然要和自己交换n-1次。综合起来选择排序的时间复杂度无论最好、平均、最坏都是O(n²)。这意味着用选择排序给一个大数组排序计算量不会因为运气好而减少这是一种“稳定但不够聪明”的特性。比起冒泡排序最好情况可以优化到O(n)选择排序在数据基本有序的场景下反而更吃亏。4.2 空间复杂度原地排序的典型代表选择排序只用了常数个临时变量循环里的i、j、min_idx以及交换用的temp没有开辟额外的数组也不需要递归调用栈所以空间复杂度是O(1)。这一点在408真题里会作为“原地排序”的案例出现需要和归并排序的O(n)空间区分开。4.3 稳定性分析一个反例讲清楚为什么“不稳定”稳定性的定义是如果两个相等的元素在排序前后的相对次序保持不变那么该排序算法是稳定的反之不稳定。选择排序为什么不稳定我用一个反例说明[5a, 5b, 3]其中5a和5b是两个相等的元素下标0和1分别是5a和5b下标2是3。第一轮选择排序会扫描全部元素找到最小值3然后让3和下标0的5a交换。数组变成[3, 5b, 5a]。此时两个5的相对顺序变了原本5a在5b前面现在5a跑到了5b后面。这就是选择排序不稳定的根源——它会把远处的最小值直接“跨过”中间的元素搬到前面这个搬运过程会打乱相等元素的原始次序。我在课堂上做过一个比喻选择排序像是“从一堆人里把个子最矮的直接拽到队伍最前面”迎面被拽走的人会插到别人前面有时候就会把本来排在后面、身高相等的人顶到后面去。如果你理解了这个生活场景稳定性这个概念就再也不会弄混了。4.4 稳定化改造能不能让选择排序变稳定很多人会问既然选择排序不稳定能不能在代码层面修一修答案是可以但要付出代价。方法是不用交换而是把最小值找到之后把中间的元素都向后移动一位让最小值“插入”到指定位置类似插入排序的移动操作。这样相等元素的相对次序就不会被交换打乱。int temp arr[min_idx]; for (int k min_idx; k i; k--) { arr[k] arr[k - 1]; } arr[i] temp;但问题是这种“移动覆盖”的操作让单轮的成本从O(1)变成了O(n)总时间复杂度不变常数却变大了。所以实际应用中几乎没有人这么做考试也不会要求你写稳定版选择排序但知道这个思路能让你对“稳定性”的理解更深一层。5. 选择排序的变种与实战运用从双向选择到链表排序5.1 双向选择排序每轮同时找最大值和最小值选择排序每轮只确定一个位置效率上有点“浪费”。双向选择排序的思路是每轮同时扫描未排序区间找出最大值和最小值最小值放到区间开头最大值放到区间末尾一轮确定两个位置总轮数直接减半。这个变种在数据结构实验报告里偶尔会出现写起来也不难核心是维护left和right两个指针每轮扫描left到right的范围分别记录最小值和最大值下标然后交换。需要注意的边界情况是如果最大值正好在left位置先交换最小值时会把最大值挪走需要额外处理。这个问题是很多同学实验报告翻车的重灾区。双向选择排序的时间复杂度依然是O(n²)只是常数减小了大约一半面试里提到它能体现你对基础算法的理解深度。5.2 链表上的选择排序不用交换数据直接改指针当要排序的数据是单链表而不是数组时选择排序依然可以工作因为每一轮找最小值只需要遍历不需要随机访问。找到最小值节点后把它从原链表中摘下来接到新链表尾部即可。这种方式避免了数组版本的“交换”操作变成了“摘节点 尾插”。链表的插入排序也很容易实现但选择排序在链表上反而更自然因为数组版需要频繁移动元素而链表版只需改指针成本更低。在408题里遇到“对单链表进行选择排序”这种题时记住一点每轮遍历找最小节点然后断开连接、尾插到结果链表中时间复杂度同样是O(n²)空间复杂度O(1)。5.3 什么时候真的应该用选择排序从工程角度说选择排序确实不是一个高效算法但它在两个场景下有实用价值一是数据量很小且对“交换次数”敏感的场景因为选择排序的交换次数在所有排序中是最少的最多 n-1 次而冒泡排序的最坏交换次数是n(n-1)/2差距巨大二是在“写简单代码快速完成任务”的场景下选择排序代码最直白不容易写错。如果在嵌入式系统里要排序一批很少的数据而且每次交换都意味着写Flash或移动机械结构那么选择排序的交换次数优势就会体现出来。这一点在面试题“为什么选择排序在特定场景下比冒泡排序更优”里可以直接引用。6. 选择排序高频考点与常见坑位排查6.1 期末和408常考的四个硬核问题第一给定一个序列写出第一轮选择排序后的结果。这类题考查的不只是“找最小值”还要注意“最小值在哪个位置”“交换后谁到了前面”。比如[5, 3, 8, 1, 9]第一轮结果是[1, 3, 8, 5, 9]——注意1和5互换3的位置没变。很多人误以为是“把所有比1小的都挪到前面”那就把选择排序和冒泡排序搞混了。第二计算比较次数。记住n(n-1)/2这个公式无论有序还是乱序都一样。有的题会给你“已经排好序的数组选择排序需要比较多少次”答案依然是n(n-1)/2而不是0。这是一个超级经典的陷阱。第三计算交换次数。最好情况有序交换0次最坏情况逆序交换n-1次。注意“每轮最多交换一次”这个特性是七大排序里交换次数上界最低的。第四判断稳定性。记住“不稳定”即可并能够举出反例。通常用[3, 3, 1]这个反例就能搞定第一轮1和第一个3交换两个3的次序就反了。6.2 写代码时最容易翻车的三个小细节代码层面的坑不多但每一个都致命。第一个是min_idx忘记更新内层循环里只比较不更新最小值下标结果就是排序后几乎没有变化。我在给低年级学弟学妹改实验报告时这类错误占了很大比例。第二个是内层循环写成了for (j i; j n; j)而不是for (j i 1; j n; j)。这个错误不会导致结果出错只会多几次无意义的自我比较但如果你在实验报告里分析比较次数就会和你实际运行的结果对不上。第三个是交换写成了赋值。arr[i] arr[min_idx]和swap(arr[i], arr[min_idx])完全是两回事。前者是覆盖后者才是交换。忘记用临时变量保存中间值数组里的元素就会被覆盖丢失。6.3 和冒泡排序、插入排序放在一起怎么选很多同学学完三个简单排序后反而被绕晕了。我给一个速查思路冒泡排序是“相邻比较大的上浮”一轮最多可以确定一个最大值最好情况可以提前终止插入排序是“把当前元素往前面有序区插入”对于基本有序的数据效率极高最好情况可以到O(n)选择排序是“每轮找最小放到最前”比较次数恒定交换次数少。排序算法核心操作最好情况最坏情况交换次数上界稳定性冒泡排序相邻交换O(n)O(n²)O(n²)稳定插入排序向前插入O(n)O(n²)O(n²)稳定选择排序选最小交换O(n²)O(n²)O(n)不稳定从这个表能直接看出如果数据基本有序用插入排序如果交换成本极高、数据量小用选择排序如果你只是想写出最不容易错的代码选择排序的代码结构最简单。理解了这些差异面试问“为什么不用冒泡排序”时你就能给出有层次的回答。我在实际带实验课的时候发现很多同学截图交实验报告但根本不理解每一轮数组的变化。我的建议特别简单拿一张纸把动图里的关键帧自己画一遍就像我这篇里那个5元素数组的表一样每轮把已排序区域和未排序区域用竖线分开。画完一遍之后代码里的min_idx和i、j的逻辑会彻底刻进脑子。这个方法比我讲一百遍都管用。如果你看到的动图网站不支持自己控制帧率你就自己用表格还原出关键帧这是最笨也最有效的学法。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询