归并排序
归并排序的思想是把数组一分为二,然后再不断将小数组递归地一分为二下去,经过一系列排序再将它们合并起来。
1 | private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) { |
归并排序可用于大量数据的排序,对于 million 和 billion 级别的数据,插入排序难以完成的任务归并排序可能几分钟就完成了。
对于 N 个元素,归并排序最多需要 NlgN 次比较和 6NlgN 次对数组的访问,并且要使用 N 个空间的辅助数组。
自底向上的归并排序
我们将归并排序的过程倒过来看,先将数组分为 2 个元素并将所有组排序,再分为 4 个元素并将所有组排序,… ,直到完成排序。
1 | public static void sort(Comparable[] a) { |
这是一个完全符合工业标准的代码,除了需要额外的存储空间。时间复杂度为 O(NlogN)。
排序规则
我们可以实现 Comparator 接口来为排序算法编写不同的排序规则,以插入排序为例:
1 | public static void sort(Object[] a, Comparator comparator) { |
1 | public class Student { |
然后可以这样使用排序:
1 | Arrays.sort(a, Student.BY_NAME); |
使用 Comparator 接口来替代 Comparable 接口的优点就是它支持待排序元素的多种排序规则。
快速排序
快速排序广泛运用于系统排序和其他应用中。它也是一个递归过程,与归并排序不同的是,它先进行操作然后再递归,而不是归并排序先进性递归然后再进行 merge。
算法的思想是先对数组随机打乱,然后每次都把第一个元素放到合适的位置,这个位置左边的元素都比它小,右边的元素都比它大,再将两侧的元素递归操作。
1 | private static int partition(Comparable[] a, int lo, int hi) { |
事实证明,快速排序比归并排序还要快,他最少需要 NlgN 次比较,最多需要 1/2 N^2 次。对于 N 个元素,快速排序平均需要 1.39NlgN 次比较,不过因为不需要过多的元素的移动,所以实际上它更快一些。其中,随机打乱是为了避免最坏的情况。
在空间使用上,它不需要额外的空间,所以是常数级别的。
案例
快速排序的一个案例是找一个数组中第 k 大的数。
1 | public static Comparable select(Comparable[] a, int k) { |
这个解法的时间复杂度是线性的,不过有论文表明它的常数很大,所以在实践中效果不是特别好。
多个相同键值
很多时候排序的目的是将相同键值的元素排到一起,处理这种问题不同的排序方法的效率也不同。
归并排序需要 1/2 NlgN 至 NlgN 次比较。
快速排序将达到 N^2 除非 partition 过程停止的键值和结果键值相等,所以需要更好的算法实现.
比较好的一种算法是 Dijkstra 三向切分,它将数组分成了三个部分,是 Dijkstra 的荷兰国旗问题引发的一个思考,即使用三种不同的主键对数组进行排序。
1 | private static void sort(Comparable[] a, int lo, int hi) { |
对于包含大量重复元素的数组,它将排序时间从线性对数级降低到了线性级别。
系统中的排序
Java 内置了一种排序方法——Arrays.sort(),这个方法使用两种排序方式共同实现。如果排序的是基本数据类型,就使用快速排序;如果排序的是对象,就使用归并排序。
因为对于基本类型来说快速排序会使用更少的空间,而且更快;而归并排序能保证 NlogN 的时间复杂度,而且更加稳定。
在视频的最后,老爷子强调对于不同的应用,要考虑的问题太多了,比如说并行、稳定等等,所以几乎每个重要的系统排序都有一个特定的高效算法,而且目前还有很多算法需要改进。
最后附上前面提到过的排序方法的总结:
编程作业:模式识别
给 n 个不同的点,找出所连线段,每条线段至少包括四个点。
首先补充完成 Point 类,这部分主要是练习使用 Comparable 和 Comparator 制定排序规则,具体的排序规则文档中有详细的描述。
1 | public class Point implements Comparable<Point> { |
然后根据给出的点求所能组成的线段,线段只包含四个点,由两端的点表示,这个方法是暴力方法,4次方的时间复杂度。
1 | public class BruteCollinearPoints { |
然后实现高效算法,这里就需要使用前面提到的比较规则,先使用快排将点集排序,取最小的点跟其他点的斜率比,如果达到四个点及以上斜率相同,则记录到数组中。
1 | public class FastCollinearPoints { |
这次作业目前只拿了88分,应该对于大规模的数据仍有不足。
对了不得不说这门课的 PA 真的有趣: