第一部分:归并排序
这个排序分为排序操作sort和归并操作merge。
对于归并,书中采用了一种原地归并的方法,即merge(a, lo, mid, hi)会将a[lo..mid]和a[mid+1..hi]归并成一个有序数组并将结果存放在a[lo..hi]中。虽然叫原地归并,但是还是用了一个辅助数组aux,将a复制到aux中,用 i 和 j 分别指向aux的左半部分和右半部分,将排序的结果移回a中。
自顶向下的归并排序
递归实现的归并排序。
从顶部不断向下对半划分,直到划分到只有一个元素,开始归并,也就是说开始一层层返回了。总是先将左半边归并了再归并右半边的,轨迹是从左到右。
缺点:递归实现的算法对于小规模问题不是很适合,因为递归会使小规模问题中方法调用过于频繁。所以可以结合插入排序处理小规模数组的优点,划分成小规模问题后用插入排序。
自底向上的归并排序
循环实现的归并排序。
将整个待排序的数组想象成n个大小为1的小数组,先两两归并,再四四归并,再八八归并,以此类推。每轮过后,每个排序的子数组大小会翻倍。
优点:遍历整个序列且不用递归。
两种归并排序比较:
- 数组长度为2的幂时,两种方式用的比较次数和数组访问次数正好相同,只是顺序不同而已;其他时候会有所不同。
- 自底向上比较适合链表组织的数据,这样只用重新组织链表链接就可以做到将链表原地排序,不用创建任何新结点
- 链表原地排序使用自底向下的归并排序
复杂度分析
研究复杂度:
- Upper bound:上限,只有部分算法可以做到,证明解决这个问题有多困难
- Lower bound:下限,所有算法成本保证的限制,没有算法可以做到更好
- Optimal Algorithm:我们一直要找的最优算法,证明上下届相同的算法
对于排序:
- 计算模型:decision tree,因为局限于实现了Comparable接口,所以只能通过比较来得到信息
- 成本模型:比较的次数
- Upper bound:可以保证排序完成的时间,N lg N
- Lower bound:基于比较的排序算法的最低下限一定是N lg N
- Optimal Algorithm:归并排序
Comparator接口
为什么要使用Comparator接口?
因为该接口允许为任意数据类型定义多种排序方法,用该接口代替Comparable接口可以更好地将数据类型的定义和两个该类型的对象应该如何比较的定义区分开。
对于多键数组,即一个对象的多种元素都可以用来当作排序的键的数组,Comparator接口使得可以在其中选择排序的标准。
为了可以使用Comparator:
- import java.util.Comparator;
- 定义一个(或嵌套)的类,这个类实现了Comparator接口
- 实现一个compare()方法,要求和Comparable一样,必须是全序关系
- 使用Object而不是Comparable
- 将Comparator传递给sort()和less()并在less()中使用它
例子:使用了Comparator的插入排序:
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; // 调用指定的compare方法
}
private static void exch(Object[] a, int i, int j)
{ Object t = a[i]; a[i] = a[j] ; a[j] = t; }
和Comparable接口区分:
- Comparable接口:
- 必须将compareTo()函数的定义放到数据类型中
- 使用自然序排序
- Comparator接口:
- 可以在后面再定义比较操作,即在数据类型的外面
- 使用alternate order(替换的顺序,即指定的顺序)来排序
稳定性
定义:如果一个算法可以保留重复元素的相对位置那就是稳定的。
不稳定算法的共同特征:长距离移动,不是一次将元素移动一个位置,而是可以将元素移动到很远的地方,这样可能会越过相同元素。
第二部分:快速排序
快速排序
和归并的不同:
- 归并是将数组分成两个子数组分别排序,然后将有序的两个子数组归并成一个有序的大数组;而快排是当两个子数组都有序时整个数组自然就有序了。
- 归并递归调用发生在处理整个数组之前;快排的递归调用发生在处理整个数组之后。
- 归并划分数组是对半分;快排划分数组取决于数组的内容。
快排步骤:
- 随机将所有元素打乱,这一点非常重要,因为不打乱不能保证不出现最坏的情况,这样做是为了保证性能良好
- 切分
- 递归对每一部分切分排序
优点:
- 复杂度O(N lg N)
- 原地排序
算法改进:
- 跟归并一样,对于小规模数组,快排比插入慢。所以可以在划分成小规模数组后就调用插入排序。
- 三取样切分,注意和三向切分区分。使用子数组的一小部分元素的中位数来切分数组会切分得更好,代价是要计算中位数。最好的取样大小是3,然后用居中的元素切分效果最好。
Selection问题
目标:给定一组n个元素,找到其中第k小的元素(kth smallest),比如k=0就找最小的,k=N-1就找最大的,k=N/2找中位数
对于这个问题:
- Upper bound:N lg N,直接排序后再寻找第k个元素
- Lower bound:N,总要查看一遍所有元素以免有漏的吧
k = 1,2,3这种小数时,upper bound会变成N,因为只用多遍历几遍数组,找到最小/第二小的元素就行。k和N成正比,k=1就为N,k=2就为2N
解决办法:用快排解决
划分数组后:
- a[j]已经就位
- 右边没有比a[j]更大的,左边没有比a[j]更小的
在划分的两个子数组中的一个重复,取决于j,如果a[k]在j的左边,将hi设为j-1;如果a[k]在j的右边,将lo设为j+1。当j=k的时候停止。
public static Comparable select(Comparable[] a, int k)
{
StdRandom.shuffle(a); // 随机化数组很重要
int lo = 0, hi = a.length-1;
while (hi > lo)
{
int j = partition(a, lo, hi);
if (j < k) lo = j + 1;
else if (j > k) hi = j - 1;
else return a[k];
}
return a[k];
}
复杂度:
平均耗时:O(n)
最坏耗时: 1/2N2 1 / 2 N 2
重复的keys
对于有大量重复key的数组,标准的归并和快排并不够好:
- 归并永远要用1/2 N lg N ~ N lg N的时间
- 快排会耗时 N2 N 2 ,只要不停地在重复key上划分
所以提出对快排的改进:三向切分的快速排序,用来处理大量重复元素,只用线性时间
大致的思路是:
从左到右遍历数组一次,维护一个指针lt使得a[lo .. lt-1]中的元素小于划分元素v,另一个指针gt使得a[gt+1 .. hi]中的元素都大于v,还有一个指针 i 使得a[lt .. i-1]中的元素都等于v,a[i .. gt]中的元素还没有确定。一开始 i 和lo相等,如果
- a[i]小于v,将a[lt]和a[i]交换,lt 和 i 加一
- a[i]大于v,将a[gt]和a[i]交换,gt减一
- a[i]等于v,i加一
直到a[lt .. gt]中的元素都等于v为止,一轮结束,在a[lo .. lt-1]和a[gt+1 .. hi]中递归进行下一轮,直到结束。
系统排序方法
java中,一般对原始数据类型使用(三向切分的)快速排序;出于稳定性和n logn的性能保证,对引用类型使用归并排序。