排序算法比较及其应用
Posted 我是老邱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法比较及其应用相关的知识,希望对你有一定的参考价值。
一、将各种数据排序
只要实现了Comparable接口的数据类型就可以被排序。
但要使算法能够灵活地用不同字段进行排序,则是后续需要考虑的问题。
1、指针排序
在Java中,指针操作是隐式的,排序算法操作的总是数据引用,而不是数据本身。
2、键不可变
如果在排序后,用例还可以改变键值,那么数组很可能就不是有序的了。类似,优先队列也会乱套。
Java中,可以用不可变数据类型作为键来避免这个问题,如String,Integer,Double和File都是不可变的。
3、廉价交换
使用引用的另一个好处是算法不必移动整个元素,对于元素大键值小的数组来说将会省下很多操作成本。
因为比较只需要访问元素的一小部分,在整个排序过程中,大部分不会被访问到。
因此对于任意大小的元素,使用引用,使得一般情况下交换和比较的成本几乎相同。
如果键值很长,那么交换的成本甚至会低于比较的成本。
4、多种排序方法
实际应用中,用户希望根据情况对一组对象按照不同的方式进行排序。
Java的Comparator接口允许在一个类中实现多种排序方法。通过将Comparator接口对象传递给sort方法,再传递给less方法即可。
Comparator接口实现举例如下:
public class WhoOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { //add code return 0; } }
sort举例如下:
public static void sort(Object[] a, Comparator c) { int N = a.length; for(int i = 1; i < N; i++) for(int j = i; j > 0 && less(c, a[j], a[j-1]); j--) exch(a, j, j-1); } private static boolean less(Comparator c, Object v, Object w) { return c.compare(v, w) < 0; } private static void exch(Object[] a, int i, int j) { Object t = a[i]; a[i] = a[j]; a[j] = t; }
Comparator接口允许为任意数据定义任意多种排序方法。用Comparator替代Comparable接口可以更好地将数据类型的定义和两个该数据类型对象应该如何比较的定义区分开来。
比如,相对字符串数组a,进行忽略大小写排序,可以使用String类中的CASE_INSENSITIVE_ORDER比较器Comparator,并传递给sort函数:
Insertion.sort(a, String.CASE_INSENSITIVE_ORDER)
5、多键
在实际应用中,一个元素的多个属性都可能被用作排序的键。
要实现这种灵活性,Comparator接口正合适,我们可以在数据类型中定义多种比较器(Comparator)。
例如:
public class Transaction implements Comparable<Transaction> { private final String who; // customer private final Date when; // date private final double amount; // amount /** * Compares two transactions by customer name. */ public static class WhoOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { return v.who.compareTo(w.who); } } /** * Compares two transactions by date. */ public static class WhenOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { return v.when.compareTo(w.when); } } /** * Compares two transactions by amount. */ public static class HowMuchOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { return Double.compare(v.amount, w.amount); } } }
这样定义之后,想让Transaction按照时间排序,可以调用
Insertion.sort(a, new Transaction.WhenOrder())
6、稳定性
如果一个排序算法能够保留数组中重复元素的相对位置的话,则这个排序算法使稳定的。
插入排序和归并排序是稳定的,选择排序、希尔排序、快速排序和堆排序则不是稳定的。
二、排序算法的对比
1、插入排序
稳定、原地排序
时间复杂度:最坏~N2/2、最好~N、平均~N2/4
空间复杂度:1
交换次数:最坏~N2/2、最好0、平均~N2/4
注:取决于输入元素的排列情况
http://www.cnblogs.com/songdechiu/p/6610515.html
2、选择排序
不稳定、原地排序
时间复杂度:N2(~N2/2)
空间复杂度:1
交换次数:N
http://www.cnblogs.com/songdechiu/p/6609896.html
3、希尔排序
不稳定、原地排序
时间复杂度:达不到平方级别、最坏和N3/2成正比(未知)
空间复杂度:1
http://www.cnblogs.com/songdechiu/p/6611340.html
4、快速排序
不稳定、原地排序
时间复杂度:最好~NlogN、最坏~N2、平均1.39NlogN
空间复杂度:最好logN,最坏N,平均logN
运行效率由概率提供保证
http://www.cnblogs.com/songdechiu/p/6629539.html
5、三向切分快速排序
不稳定、原地排序
时间复杂度:最好N、最坏~N2、平均~NlogN
空间复杂度:最好1、最坏N、平均logN
运行效率由概率提供保证
http://www.cnblogs.com/songdechiu/p/6629539.html
6、归并排序
稳定、非原地排序
时间复杂度:1/2NlgN至NlgN
空间复杂度:N
数组访问次数:6NlgN
http://www.cnblogs.com/songdechiu/p/6607341.html
http://www.cnblogs.com/songdechiu/p/6607720.html
7、堆排序
不稳定、原地排序
时间复杂度:2NlogN + 2N(最坏)
空间复杂度:1
数组元素交换次数:NlogN + N(最坏)
http://www.cnblogs.com/songdechiu/p/6736502.html
结论:快速排序是最快的通用排序算法,因为其内循环指令少,而且还能利用缓存(顺序访问数据),其增长数量级为~cNlogN。特别是使用三切分之后,快排对某些特殊输入其复杂度变为线性级别。
实际应用中,大多数情况下,快速排序是最好的选择。
但是如果稳定性很重要而且空间又不是问题,那么归并排序是最好的。
三、问题规约
1、找出重复元素
首先将数组排序,然后遍历有序的数组,记录连续出现的重复元素即可。
2、排列
一个排列就是一组N个整数的数组,其中0到N-1的每个数都只出现一次。
两个排列之间的Kendall tau距离就是两组排列中的逆序数对。如0 3 1 6 2 5 4和1 0 3 6 4 2 5的Kendall tau距离是4,因为0-1,3-1,2-4,5-4这四对数字的相对顺序不同。
Kendall tau距离就是逆序数对的数量。
实现:http://algs4.cs.princeton.edu/25applications/KendallTau.java.html
public class KendallTau { // return Kendall tau distance between two permutations public static long distance(int[] a, int[] b) { if (a.length != b.length) { throw new IllegalArgumentException("Array dimensions disagree"); } int n = a.length; int[] ainv = new int[n]; for (int i = 0; i < n; i++) ainv[a[i]] = i; Integer[] bnew = new Integer[n]; for (int i = 0; i < n; i++) bnew[i] = ainv[b[i]]; return Inversions.count(bnew); }
}
求两个排列a和b的逆序数对跟以前归并排序中求一个未排好序的排列相对排好序的排列的逆序数对有点不同。
归并排序中是默认跟标准序列(排好序)即0 1 2 3 4 5 6进行比较,这里是a跟b进行比较,我们以b为标准序列。
所以我们要转换成一个序列跟标准序列之间的逆序数对,再将该序列传给归并排序中的求解方法。
以方便对上述代码进行解释,做出以下假设
a:0 3 1 6 2 5 4
b:1 0 3 6 4 2 5
其中将a[i]称为key,i称为index;b[j]也为key,j为index。
上述代码的主要思路是转化为index序列之间的比较,以b为标准序列,其index序列为0 1 2 3 4 5 6,再求出b[i](0<= i <= 6)在数组a中相应的index序列。
ainv是a的逆数组,即存储着数组a的key所在的index,即ainv[k] = i代表着a[i] = k。
当key为b[0] = 1时,其索引为ainv[b[0]]即ainv[1] = 2,以此类推最后求出a相对于b的索引序列为2 0 1 3 6 4 5。
最后将这个相对索引序列2 0 1 3 6 4 5传给归并排序中的求解方法即可,求出为4。
以下为归并排序中求逆序数对的方法:
http://algs4.cs.princeton.edu/22mergesort/Inversions.java.html
public class Inversions { // do not instantiate private Inversions() { } // merge and count private static long merge(int[] a, int[] aux, int lo, int mid, int hi) { long inversions = 0; // copy to aux[] for (int k = lo; k <= hi; k++) { aux[k] = a[k]; } // merge back to a[] int i = lo, j = mid+1; for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; else if (aux[j] < aux[i]) { a[k] = aux[j++]; inversions += (mid - i + 1); } else a[k] = aux[i++]; } return inversions; } // return the number of inversions in the subarray b[lo..hi] // side effect b[lo..hi] is rearranged in ascending order private static long count(int[] a, int[] b, int[] aux, int lo, int hi) { long inversions = 0; if (hi <= lo) return 0; int mid = lo + (hi - lo) / 2; inversions += count(a, b, aux, lo, mid); inversions += count(a, b, aux, mid+1, hi); inversions += merge(b, aux, lo, mid, hi); assert inversions == brute(a, lo, hi); return inversions; } /** * Returns the number of inversions in the integer array. * The argument array is not modified. * @param a the array * @return the number of inversions in the array. An inversion is a pair of * indicies {@code i} and {@code j} such that {@code i < j} * and {@code a[i]} > {@code a[j]}. */ public static long count(int[] a) { int[] b = new int[a.length]; int[] aux = new int[a.length]; for (int i = 0; i < a.length; i++) b[i] = a[i]; long inversions = count(a, b, aux, 0, a.length - 1); return inversions; } }
3、优先队列规约
两个规约为优先队列操作问题得例子:
TopM,在输入流中找到前M个最大的元素。
MultiWay,将M个有序输入流归并为一个有序输入流。
4、中位数和顺序统计
找到一组数中的第k小的元素(中位数是特殊情况)。
可以规约为快排中的切分方法。
public static <Key extends Comparable<Key>> Key select(Key[] a, int k) { if (k < 0 || k >= a.length) { throw new IndexOutOfBoundsException("Selected element out of bounds"); } StdRandom.shuffle(a); int lo = 0, hi = a.length - 1; while (hi > lo) { int i = partition(a, lo, hi); if (i > k) hi = i - 1; else if (i < k) lo = i + 1; else return a[i]; } return a[lo]; }
平均上,基于切分的选择算法的运行时间是线性级别的。
四、排序应用
1、商业计算
一般大量的信息都被存储在大型数据库中,能够按照多个键排序以提供搜索效率。
可以说没有线性对数级别的排序算法就没法将这些海量的数据进行排序,进一步处理这些数据也会变得极端困难,甚至是不可能的。
2、信息搜索
有序的信息可以确保我们可以用经典的二分查找法来进行高效的搜索。
3、运筹学
(1)排序算法在调度中的应用
问题:假设我们需要完成N个任务,第j个任务需要耗时tj秒,我么需要在完成任务的同时,将每个任务的平均完成时间最小化。
思路:只要将任务按照处理时间升序排列就可以达到目标。
实现:因此按照最短优先原则,将任务按耗时排序,或者插入最小优先队列中,即可完成目标。
(2)负载均衡问题
问题:假设我们有M个相同的处理器以及N个任务,目标是在尽可能短的时间内在这些处理器上完成所有任务。
思路:这个问题是NP-困难的,因此实际上不可能算出最优的方法,一种较优调度方法是最大优先。将任务按照耗时降序排列,将每个任务依次分配给当前可用的处理器。
实现:先逆序排序这些任务,然后为M个处理器维护一个优先队列,每个元素的优先级就是对应处理器上运行的任务耗时之和。每一步都删去优先级最低的处理器,将下个任务分配这个处理器,然后重新插入优先队列。
4、数值计算
一些数值计算算法采用优先队列和排序来控制计算中的精确度。
数值积分的一个方法,就是使用优先队列来存储一组小间隔中每段的近似精度。积分的过程就是删除精确度最低的间隔,并分为两段,然后将两段都重新加入优先队列,直到达到预期的精度。
5、事件驱动模型
第6章
6、组合搜索
优先队列。A*算法。
7、Prim算法和Dijkstra算法
优先队列在这两个算法中扮演了重要的角色。
8、Kruskal算法
算法的运行时间是由排序所需的时间所决定的。
9、霍夫曼压缩
这是数据压缩中的一个经典算法。
算法处理的数据中每个元素都有一个小整数作为权重,处理的过程就是将权重最小的两个元素归并为一个新的元素,权重相加得到新元素的权重。
使用优先队列可以立即实现这个算法。
其他的几种数据压缩算法也是基于排序的。
10、字符串处理
字符串算法常常依赖于排序算法(常用特殊的字符串排序算法)。
以上是关于排序算法比较及其应用的主要内容,如果未能解决你的问题,请参考以下文章