Preface
排序不仅仅在计算机科学中被经常讨论,该问题也是数学家们经常研究的领域,例如对希尔排序的时间复杂度证明问题,多么的引人入胜。
在计算机中,经常用到的排序算法有:快速排序,堆排序,插入排序,希尔排序,选择排序,基数排序和桶排序等。若要对其分类,其分类标准也有多种。例如可以按照数据的物理位置可以分为:内部排序和外部排序,按照排序中对键(Key)的处理方式可以分为:比较排序和非比较排序,也可以按照排序前后数据的相对位置变化分为:稳定排序和不稳定排序。
排序算法的稳定性是本文的研究对象。研究排序算法的稳定性能够带来什么收益?
常见排序算法的稳定性与时间复杂度表
排序算法 | 最好时间复杂度 | 平均时间复杂度 | 最差时间复杂度 | 稳定性 |
---|---|---|---|---|
快速排序 | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N 2 ) O(N^2) O(N2) | 不稳定 |
堆排序 | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | 不稳定 |
插入排序 | O ( N ) O(N) O(N) | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | 稳定 |
希尔排序 | O ( N ) O(N) O(N) | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N s ) 1 < s < 2 O(N^s) 1 < s < 2 O(Ns)1<s<2 | 不稳定 |
选择排序 | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | 不稳定 |
冒泡排序 | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | 稳定 |
归并排序 | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N) | 稳定 |
常见排序算法的最佳实践场景
上表介绍了大部分常用排序算法的时间复杂度和稳定性信息。但仅了解这些还远远不够,因为时间复杂度这个指标从渐进的意义上来讲,并不是十分的精确,并不能说时间复杂度高的算法的运行时效率一定比时间复杂度低的差。同时,若仅仅考虑平均复杂度这个指标有些情况下会削足适履,适得其反。而对于计算机领域的工程师们来说,我们需要不断地探索具体场景下的最佳实践,做到对具体问题的量体裁衣。
直接插入排序和希尔排序的最佳实践场景:数组基本有序
冒泡排序和选择排序的最佳实践场景:数组规模较小
堆排序、快速排序和归并排序:数组规模较大
对于归并排序而言,具有很好的并行特性。
不得不说,计算机科学家们对于问题的考量可以说是面面俱到,锱铢必较。我记得《Parsing Technique》这本书中曾经说道:计算机科学家和数学家有很多相似的地方,但是二者最大的不同在于Time is essential for the computer scientist。没错,计算机科学家们总是会十分在意和追求算法时间上的高效。
排序算法的最佳实践场景问题,在大多数编程语言的标准库实现中均有印证。若读者有兴趣可以参考Java标准库中java.util.Arrays#sort的实现。可以说标准库排序算法是一种混合实现,针对于数组的规模和其他参考指标选择对应其最佳实践的排序算法。
排序算法稳定性
所谓排序算法的稳定性是指,对于待排序的一个序列,若排序前后两个键值相同的元素其相对位置并不发生改变,那么我们就称其为稳定的排序算法。例如下面示例中的描述
稳定的排序:
排序前:
A(3), B(2), C(2), D(1), E(0)
排序后:
E(0), D(1), B(2), C(2), A(2)
不稳定的排序:
排序前:
A(3), B(2), C(2), D(1), E(0)
排序后:
E(0), D(1), C(2), B(2), A(2)
从上面例子可以中看出,若排序算法不稳定,则排序前后元素B和元素C的相对位置发生了改变。
排序算法稳定性的应用
平时我们之所以不太关注排序算法稳定性的原因是:一般情况下,我们只需要关注一个排序关键字。当我们需要考虑多个关键字排序时,那么排序算法的稳定性就是解决此类问题的牛鼻子。
例如:你有一个学生的成绩列表,你需要把他们的成绩按照降序排名。同时由于班级里学生人数较多,难免出现成绩相同的情况。若两个学生成绩相同则按照其姓名的首字母顺序升序排列。
处理思路
若两名学生成绩不同,则无需考虑其姓名首字母的先后顺序,因为成绩是主要的排序关键字。若两名学生成绩相同,此时姓名首字母的顺序是排序的关键。
若一开始按照姓名的首字母顺序排好序,若排好序的两名相邻的同学其成绩不同,那么我们在接下来按照成绩排序时,打破这种相对顺序是完全合理的。若排好序的两名相邻同学的成绩相同,那么在接下来按照成绩排序中,若排序算法是稳定的,这种相对顺序依然不会发生改变。
因此,解决问题的流程可以阐述为:在不考虑排序算法稳定性的前提下,按照学生姓名的首字母先对数据进行排序。排序完毕后,再以学生成绩为关键字,选择一个稳定的排序算法进行排序。
案例的Java实现
建立承载数据的POJO类
@Data
@AllArgsConstructor
final class Student {
private String name;
private int score;
}
生成一份学生成绩单
final List<Student> students = Arrays.asList(
new Student("Sonny", 100),
new Student("Bryan", 100),
new Student("Jenny", 115),
new Student("Candy", 90),
new Student("Bob", 90),
new Student("Steven", 105),
new Student("James", 105),
new Student("Kim", 105)
);
按照对关键字的要求进行排序
// 插入排序是稳定的排序算法
static <T> void insertionSort(List<T> data, Comparator<T> cmp) {
if (!(data instanceof RandomAccess)) throw new RuntimeException("Unsupported list type");
for (int i = 1, len = data.size(); i < len; i++) {
// 向前寻找插入位置
for (int j = i; j > 0; j--) {
if (cmp.compare(data.get(j), data.get(j-1)) < 0) {
final T temp = data.get(j);
data.set(j, data.get(j-1));
data.set(j-1, temp);
} else {
break;
}
}
}
}
static void sort(List<Student> students) {
// 1. 首先按照姓名首字母升序排列
Collections.sort(students, (a, b) -> a.getName().compareTo(b.getName()));
// 2. 选择一个稳定的排序算法,再按照分数降序排列
insertionSort(students, (a, b) -> Integer.compare(b.getScore(), a.getScore()));
}
获得程序运行后的结果
NAME SCORE
-------------------- -----
Jenny 115
James 105
Kim 105
Steven 105
Bryan 100
Sonny 100
Bob 90
Candy 90