初级算法排序
规则
- less()元素比较
- exch()元素交换位置
排序算法类的模板
public class Example
{
public static void sort(Comparable[] a){
}
private static boolean less(Compara v,Comparable w){
return v.compareTo(w)<0;
}
private static void exch(Comparable[] a,int i,int j){
Comparable t = a[i];a[i] = a[j];a[j]=t;
}
private static void show(Comparable[] a)
{ // 在单行中打印数组
for (int i = 0; i < a.length; i++)
StdOut.print(a[i] + " ");
StdOut.println();
}
public static boolean isSorted(Comparable[] a)
{ // 测试数组元素是否有序
for (int i = 1; i < a.length; i++)
if (less(a[i], a[i-1])) return false;
return true;
}
public static void main(String[]
args)
{ // 从标准输入读取字符串,将它们排序并输出
String[] a = In.readStrings();
sort(a);
assert isSorted(a);
show(a);
}
}
经典排序方法
- 选择排序
- 插入排序
- 希尔排序
- 并归排序
- 快速排序
- 堆排序
选择排序
定义
找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者。对于长度为 N 的数组,选择排序需要大约 N2/2 次比较和 N 次交换
- 特点
1)运行时间和输入无关。有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间一样长!
2)数据移动是最少的。
public class Selection
{
public static void sort(Comparable[] a)
{ // 将a[]按升序排列
int N = a.length; // 数组长度
for (int i = 0; i < N; i++)
{ // 将a[i]和a[i+1..N]中最小的元素交换
int min = i; // 最小元素的索引
for (int j = i+1; j < N; j++)
if (less(a[j], a[min])) min = j;
exch(a, i, min);
}
}
// less()、 exch()、 isSorted()和main()方法见“排序算法类模板”
}
插入排序
对于随机排列的长度为 N 且主键不重复的数组,平均情况下插入排序需要~ N2/4 次比
较以及~ N2/4 次交换。最坏情况下需要~ N2/2 次比较和~ N2/2 次交换,最好情况下需要 N-1
次比较和 0 次交换
public class Insertion
{
public static void sort(Comparable[] a)
{ // 将a[]按升序排列
int N = a.length;
for (int i = 1; i < N; i++)
{ // 将 a[i] 插入到 a[i-1]、 a[i-2]、 a[i-3]...之中
for (int j = i; j > 0 && less(a[j], a[j-1]); j--)
exch(a, j, j-1);
}
}
// less()、 exch()、 isSorted()和main()方法见“排序算法类模板”
}
插入排序对部分有序的数组很有效,而选择排序则不然。
命题 C。插入排序需要的交换操作和数组中倒置的数量相同,需要的比较次数大于等于倒置的
数量,小于等于倒置的数量加上数组的大小再减一。证明。 每次交换都改变了两个顺序颠倒的元素的位置,相当于减少了一对倒置,当倒置数量为
0 时,排序就完成了。每次交换都对应着一次比较,且 1 到 N-1 之间的每个 i 都可能需要一次
额外的比较(在 a[i] 没有达到数组的左端时)。性质 D。对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,
两者之比应该是一个较小的常数。例证。这个结论在过去的半个世纪中已经在许多不同类型的计算机上经过了验证。在 1980 年
本书第 1 版完成之时插入排序就比选择排序快一倍。
希尔排序
基于插入排序的快速的排序算法
对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素。
希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部
进行排序,并最终用插入排序将局部有序的数组排序。希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。这样的数组被称为 h 有序数组。
一个 h 有序数组即一个由 h 个有序子数组组成的数组
public class Shell
{
public static void sort(Comparable[] a)
{ // 将a[]按升序排列
int N = a.length;
int h = 1;
while (h < N/3) h = 3*h + 1; // 1, 4, 13, 40, 121, 364, 1093, ...
while (h >= 1)
{ // 将数组变为h有序
for (int i = h; i < N; i++)
{ // 将a[i]插入到a[i-h], a[i-2*h], a[i-3*h]... 之中
for (int j = i; j >= h && less(a[j], a[j-h]); j -= h)
exch(a, j, j-h);
}
h = h/3;
}
}
// less()、 exch()、 isSorted()和main()方法见“排序算法类模板”
}
归并排序
将两个有序的数组并归成一个更大的有序数组。(递归排序算法)
要将一个数组排序,可以先(递归的)将它分成两半分别排序,然后将结果并归起来。
原地归并
public static void merge(Comparable[] a,int lo,int mid,int hi){
int i = lo, j = mid+1;
for(int k=lo;k<=hi;k++) aux[k]=a[k];
for(int k=lo;k<=hi;k++)
if(i>mid) a[k]=aux[j++];
else if(j>hi) a[k]=aux[i++];
else if(less(aux[j],aux[i])) a[k]=aux[j++];
else a[k]=aux[i++];
}
自顶向下的归并排序
分治思想的典型应用,将大问题分隔成小问题分别解决,然后用所有小问题的答案来解决整个大问题。
public class Merge{
private static Comparable[] aux;
//归并所需的辅助数组
public static void sort(Comparable[] a){
aux = new Comparable[a.length];
//一次性分配空间
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a,int lo,int hi){
//将数组a[lo..hi]排序
if(hi<lo) return;
int mid = lo+(hi-lo)/2;
sort(a,lo,mid);//将左半边排序
sort(a,mid+1,hi);//将右半边排序
merge(a,lo,mid,hi);//归并结果
}
}
对于长度为N的任意数组,自顶向下的归并排序需要1/2NlgN至NlgN次比较。
对于长度为N的任意数组,自顶向下的归并排序最多需要访问数组6NlgN次。可以用归并排序处理数百万甚至更大规模的数组,这是插入排序或者选择排序做不到的。
缺点:辅助数组所使用的的额外空间和N的大小成正比。运行时间还有待改善。
自底向上的归并排序
public class MergeBu{
private.static Comparable[] aux;
public static void sort(Comparable[] a){
//进行lgN次两两归并
int N = a.length;
aux = new Comparable[N];
for(int sz =1;sz<N;sz=sz+sz) //sz子数组大小
for(int lo=0;lo<N-sz;lo+=sz+sz)//lo:子数组索引
merge(a,lo,lo+sz-1,Math.MIN(lo+sz+sz-1,N-1));
}
}
对于长度为N的任意数组,自顶向上的归并排序需要1/2NlgN至NlgN次比较。
当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。归并排序是一种渐进最优的基于比较排序的算法
快速排序
分治的排序算法。将一个数组分成两个子数组,将两部分独立的排序。
快速排序和归并排序是互补的:
归并排序,递归调用发生在处理整个数组之前,一个数组被等分为两半。
快速排序中,递归调用发生在处理整个数组之后,切分的位置取决于数组的内容。
public clas Quick{
public static void sort(Comparable[] a){
StdRanm.shuffle(a);
sot(a,0,a.length-1);
}
private static void sort(Comparable[] a,int lo,int hi){
if(hi<=lo) return;
int j=partiton(a,ho,hi);//切分
sort(a,lo,j-1);//将左半部分a[lo..j-1]排序
sort(a,j+1,hi);//将右半部分a[j+1..hi]排序
}
}
递归调用切分 (原地切分,不使用辅助数组)
1. 任意取a[lo]作为切分元素
2. 从数组左端开始向右扫描直到找到一个大于等于它的元素
3. 从数组有段开始扫描直到找到一个小于等于它的元素。
4. 交换两个数的位置,然后继续。
5. 指针相遇时,只需要将切分元素a[lo]和左子数组最右侧的元素(a[j])交换然后返回j
private static int partition(Comparable[]1,int lo,int hi){
//将数组切分为a[lo..i-1],a[i],a[i+1..hi]
int i = lo,j=hi+1;//左右扫描指针
Comparable v=a[lo];//切分元素
while(true){
//扫描左右,检查扫描是否结束并交换元素
while(less(a[++i],v)) if(i==hi) break;
while(less(v,a[--j])) if(j==lo) break;
if(i>=j) break;
exch(a,i,j);
}
exch(a,lo,j);//将v=a[j]放入正确的位置
return j;//a[lo..j-1]<=a[j]<=a[j+1..hi]达成
}
对于长度为N的任意数组,快速排序需要~2NlgN次比较。及1/6的交换。
快速排序最多需要N²/2次比较,但随机打乱数组能够预防这种情况。
算法改进
切换到插入排序
对于小数组,快速排序比插入排序慢
三取样切分
使用子数组的一小部分元素的中位数来切分数组。
取样大小设为3并用大小居中的元素切分的效果最好。熵最优的排序
大量重复元素的数组,有改进潜力
简单的想法:
将数组切分成三部分,分别对应小于、等于和大于切分元素的数组元素。
荷兰国旗,三向切分的快速排序
等于切分元素时,指针+1,不交换。不是很流行。
public class Qucik3wan{
private static void sort(Comparable[] a,int lo,int hi){
//调用此方法的公有方法sort()
if(hi<=0) return;
int lt=lo,i=lo+1;gt=hi;
Comparable v = a[lo];
while(i<=gt){
int cmp =a[i].compareTo(v);
if (cmp<0) exch(a,lt++,i++);
else if(cmp>0) exch(a,i,gt--);
else i++;
}//现在 a[lo..lt-1]<v=a[lt..gt]<a[gt+1..hi]成立
sort(a,lo,lt-1);
sort(a,gt+1,hi);
}
}