排序稳定性决定相等元素原始顺序是否保留,影响分页二次排序、多字段排序等场景;稳定算法有归并、插入、冒泡、collections.sort(),不稳定算法有快排、堆排、arrays.sort(int[])。

排序稳定性到底影响什么
稳定性不是理论概念,它直接决定相同分数、同名、同时间戳的对象在排序后会不会“乱序”。比如你有一个 Student 列表,两个学生都是 85 分,原始顺序是张三在前、李四在后;如果用不稳定的排序(如 Arrays.sort() 对基本类型数组,或 selectionSort),排完可能李四跑到张三前面——而你根本没改过比较逻辑。
关键判断:只要业务要求“相等元素的原始先后关系必须保留”,就必须选稳定算法。典型场景包括分页查询后二次排序、多字段复合排序(先按部门,再按入职时间)、审计日志按时间+ID去重排序等。
- 稳定算法:归并排序、插入排序、冒泡排序、
Collections.sort()(对List) - 不稳定算法:选择排序、堆排序、快速排序、
Arrays.sort(int[])(对基本类型数组) - 注意:
Arrays.sort(Object[])是稳定的(底层用的是归并),但Arrays.sort(int[])不是——类型不同,行为完全不同
为什么归并排序是Java对象排序的默认靠山
Java 的 Collections.sort(List) 和 Arrays.sort(Object[]) 内部都用了 Timsort(归并+插入的混合优化),但归并排序本身是它的理论根基和可预测的稳定保障。当你需要自己实现可控、可调试、不依赖JDK内部优化的对象排序时,手写归并最可靠。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 不要为了“看起来高级”而手写归并去替代
Collections.sort(list, comparator)——除非你明确要避开 GC 压力(比如嵌入式环境)或需要自定义合并策略 - 归并排序必须用额外空间(
O(n)),对超大List<student></student>要预估堆内存,别在 256MB 容器里硬跑千万级对象归并 - 递归深度受数据量影响,极端情况(比如 1000 万元素)可能触发
StackOverflowError,此时应改用迭代版归并,或切回Collections.sort()(它已做栈保护)
手写归并排序时最容易崩的三个点
不是逻辑错,而是边界和引用搞混了。下面这些坑,90% 的现场 bug 都出在这儿:
-
mid = left + (right - left) / 2必须这么写,不能写成(left + right) / 2——整数溢出在大索引下会直接让mid变负,后续数组访问全崩 - 合并时临时数组长度必须是
right - left + 1,少一个就ArrayIndexOutOfBoundsException;复制回原数组时起始下标必须是left,不是0 - Comparator 传进归并函数后,在
merge步骤里必须全程用同一个实例比大小,千万别在merge里 new 一个新Comparator——尤其是涉及闭包或 lambda 捕获变量时,容易比着比着就NullPointerException
什么时候该放弃归并,换别的方案
归并不是银弹。如果你的 Student 列表其实只有几十个元素,或者已经基本有序(比如按学号插入的新数据),那归并的常数开销反而比 insertionSort 大;如果你只是临时查 TopN,用 PriorityQueue 更省。
- 小数据(
n ):直接用 <code>Collections.sort()或手写插入排序,快且省内存 - 只取前 K 个:用
PriorityQueue(最大堆),O(n log k)时间,O(k)空间,比全排序划算得多 - 流式/内存受限:考虑外部归并(external merge sort),把数据分块落盘再合并,这时候 Java 的
Files.lines().sorted()就不合适了,得自己控 IO
真正难的从来不是“怎么写归并”,而是想清楚:你排这个序,到底是为展示、为计算、还是为下游系统契约?稳定只是底线,不是全部。










