← 返回题目列表

什么是排序的稳定性?哪些排序稳定、哪些不稳定?为什么它重要?

高频 中等 第 10 / 26 题 更新于 2026/07/28
排序稳定性多关键字排序

简化版

稳定性指:排序后,值相等的元素相对顺序保持不变。稳定的排序有冒泡、插入、归并、计数、桶、基数;不稳定的有选择、希尔、快速、堆排。稳定性在多关键字排序里至关重要:先按次要键排、再按主要键用稳定排序,才能保证主键相同的记录仍按次要键有序。

详细版

定义:如果排序前 A 排在 B 前面,且 A、B 的排序键相等,那么排序后 A 仍在 B 前面,就是稳定的;否则不稳定。

按分数排序 [(小明,90), (小红,85), (小刚,90)]
稳定结果:  [(小红,85), (小明,90), (小刚,90)]  ← 90 分的小明仍在小刚前(原顺序)
不稳定可能: [(小红,85), (小刚,90), (小明,90)]  ← 小明小刚顺序被打乱

稳定性一览:

稳定 ✅不稳定 ❌
冒泡排序选择排序
插入排序希尔排序
归并排序快速排序
计数排序堆排序
桶排序
基数排序

口诀:稳定「冒插归 + 计桶基」,不稳定「选希快堆」。

完整版教学

一、稳定性到底是什么

稳定性只在「有相等的排序键」时才有意义。它关心的是:当两个元素的排序键一样时,它们排完序后的先后顺序,是否和排序前一致。

  • 如果对基本类型(纯数字)排序,相等就是完全相同,谁前谁后无所谓,稳定性看不出区别。
  • 但对对象/记录排序(按某个字段排),相等的键背后是不同的对象。这时「相等元素的相对顺序」就有实际意义了——稳定性才凸显出来。

二、为什么稳定性重要:多关键字排序(核心)

稳定性最重要的应用是多关键字排序。比如要「按部门排序,同部门内按入职时间排序」。做法是:

  1. 先按次要键(入职时间)排序
  2. 再按主要键(部门)用稳定排序

因为第二次排序是稳定的,部门相同的记录会保持第一次排好的入职时间顺序。如果第二次用不稳定排序,部门内的入职时间顺序就会被打乱,前功尽弃。

目标:先按部门、部门内按入职时间
① 先按入职时间排 → 全部按时间有序
② 再按部门稳定排 → 部门相同的,时间顺序不变 ✓
若②不稳定 → 部门相同的时间顺序被打乱 ✗

这就是「稳定排序 = 能叠加多轮排序而不破坏前面结果」,是数据库 ORDER BY 多列、Excel 多级排序的底层原理。

三、为什么这些排序稳定

稳定的排序有个共同点:只在「严格必要」时移动元素,且相等时不改变相对顺序

  • 冒泡:只在 a[j] > a[j+1](严格大于)时交换,相等不换。
  • 插入:a[j] > cur 时才后移,遇到相等就停,插在它后面。
  • 归并:合并时 a[i] <= a[j] 相等取左边,左边本来在前。
  • 计数/基数:按值域/按位分配,天然保持先来后到。

它们都不会让相等元素「越过」彼此

四、为什么这些排序不稳定

不稳定的排序都有**「远距离交换/移动」**,会让相等元素跨越彼此:

  • 选择排序:[5a, 5b, 3] 选最小的 3 和 5a 交换 → [3, 5b, 5a],5b 跑到 5a 前。
  • 快速排序:分区时基准两侧的远距离交换会打乱相等元素。
  • 堆排序:建堆和「堆顶换末尾」的跨越式交换会打乱。
  • 希尔排序:不同 gap 分组的跨组移动会打乱。

一个远距离交换就可能把某个元素甩到与它相等的元素后面,破坏稳定性。

五、能不能把不稳定改成稳定

一些不稳定排序可以「人为改稳定」,但有代价:

  • 通用技巧:给每个元素附加一个「原始下标」作为次级比较键——当主键相等时,按原始下标比较。这样任何排序都变稳定,代价是需要额外存下标、比较变复杂。Java 的 Arrays.sort(对象) 保证稳定(用 TimSort),Arrays.sort(基本类型) 用快排不保证稳定(但基本类型不需要稳定)。
  • 快排、堆排改稳定通常得不偿失,实际中要稳定就直接选归并/TimSort。

六、把不变量、推演与工程边界落到代码上

算法正确性的核心不是记住某个 while,而是始终维护这个不变量:稳定排序后,键相等记录的原始相对次序保持不变。

对应的状态推进是:判断具体实现是否让相等元素发生跨越,而不能只背算法名称。每次推进前都应能解释“为什么被排除的部分不可能再包含答案”,否则只是碰巧通过样例。

初始化边界与状态
while 尚未结束:
    根据当前状态作出唯一可证明安全的选择
    更新边界、计数或局部结构
    断言不变量仍然成立
返回不变量在终止状态下推出的答案

复杂度不能只背一个符号。稳定性不改变渐进时间复杂度,却可能要求额外空间或更受限的移动方式。

带数字走一遍:先按姓名稳定排序,再按部门稳定排序,最终部门为主键且同部门保持姓名序。手算时要同时记录下标、区间含义和本轮排除的候选,才能暴露等号与边界错误。

核对项结论
前提多字段记录、分阶段排序和业务顺序有语义时需要稳定性
时间复杂度取决于所选排序算法
额外空间取决于所选排序算法与稳定化方案
关键边界快速排序、堆排序的远距离交换常破坏稳定;相等时取左保证归并稳定
替代方案若键能拼成完整复合键,也可一次排序显式表达优先级

易错点:模板只有在前提和不变量一致时才成立;换了区间定义、重复值规则或输出语义,等号与推进方式就必须重新推导。

实现完成后至少检查五类用例:

  • 空输入或题目允许的最小规模,验证初始化不会越界。
  • 单元素与两个元素,验证循环条件和最后一次推进。
  • 大量重复值,验证相等分支、稳定性或去重语义。
  • 已满足目标性质与极端逆序/偏斜分布,观察最好和退化路径。
  • 上面的数字样例逐轮手算,并与程序记录的状态轨迹逐项对照。

七、常见误区与追问

  • 误区:只要记住模板就适用于所有输入。 本题成立的前提是“多字段记录、分阶段排序和业务顺序有语义时需要稳定性”,前提被破坏后必须换算法或重新证明。
  • 误区:复杂度只写 取决于所选排序算法 就算完整。 还要说明输入参数、预处理、递归栈以及最坏退化条件;本题的推导是“稳定性不改变渐进时间复杂度,却可能要求额外空间或更受限的移动方式”。
  • 误区:重复值和边界值不会改变代码。 快速排序、堆排序的远距离交换常破坏稳定;相等时取左保证归并稳定。
  • 追问:为什么每次推进不会漏掉答案? 因为始终维护“稳定排序后,键相等记录的原始相对次序保持不变”,被舍弃区域已由顺序或状态关系证明不可能更优。
  • 追问:用一个数字例子怎么讲? 可以从“先按姓名稳定排序,再按部门稳定排序,最终部门为主键且同部门保持姓名序”开始,逐轮写出状态与被排除区间。
  • 追问:什么时候不该使用这个方案? 当前提不满足或退化代价不可接受时,应考虑“若键能拼成完整复合键,也可一次排序显式表达优先级”。
  • 追问:如何设计自测避免只过样例? 同时覆盖最小规模、重复值、极端分布、无解/边界解,并对关键状态加断言。

八、加强记忆

排序稳定性 = 排序后相等元素的相对顺序不变。稳定的:「冒插归 + 计桶基」(冒泡、插入、归并、计数、桶、基数);不稳定的:「选希快堆」(选择、希尔、快速、堆排)。稳定的共性是「相等不越过彼此」,不稳定的都有「远距离交换」。重要性在多关键字排序:先排次要键、再用稳定排序排主要键,才能保持「主键相同按次要键有序」(数据库 ORDER BY 多列同理)。要稳定就选归并/TimSort。