归并、快排和堆排算法

*代码的实现基于算法排序模板方法

1  归并排序

特点:

  • (优点):能够保证将任意长度为N的数组排序所需时间和NlogN成正比。
  • (缺点):所需额外空间与N成正比。

归并排序是算法设计中分治思想的典型应用。

原地归并方法:

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++)//将a[]复制到aux[]
		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++];//右边当前元素>左边当前元素(取左边元素)
}
  • 对于长度为N的任意数组,自顶向下和自底向上的归并排序需要1/2*NlgN至NlgN次比较。
  • 对于长度为N的任意数组,自顶向下和自底向上的归并排序最多访问数组6*NlgN次。
  • 没有任何基于比较的算法能够保证使用少于lg(N!)~NlgN次比较将长度为N的数组排序。
  • 归并排序是一种渐进最优的基于比较排序的算法。

自顶向下的归并排序:

public class Merge {
	private static Comparable[] aux;    //归并方法需要的辅助数组
	public static void sort(Comparable[] a){
		aux = new Comparable[a.length];
		sort(a,0,a.length-1);
	}
	public static void sort(Comparable[] a,int lo,int 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);//归并
	}
	//less()、exch()、isSorted()、main()方法见“排序算法模板”
}

算法改进:

  • 对小规模子数组使用插入排序。
  • 测试数组是否已经有序。
  • 不将元素复制到辅助数组。

自底向上的归并排序:

public static void sort(Comparable[] a){
		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));
}

自底向上的归并排序算法适合用链表组织的数据。

算法改进:

快速归并:按降序将a[]的后半部分复制到aux[],然后将其归并回a[],这样可以去掉循环中检测某半边是否用尽的代码。

次线性的额外空间:用大小M将数组分为N/M块,可以实现算法使需要的额外空间减少到max(M,N/M):

  1. 每个块用选择排序排序
  2. 块之间归并排序排序

2  快速排序

  • 将长度为N的无重复数组排序,快速排序平均需要~2*NlgN次比较(以及1/6的交换)。
  • 快速排序最多需要N^2/2次比较,但随机打乱数组能预防这种情况。

归并排序和希尔排序一般都比快速排序慢,其原因就在它们还在内循环中移动数据;快速排序的另一个速度优势在于它的比较次数很少。

快排特点:

  • 原地排序(只需要一个很小的辅助栈)
  • 将长度为N的数组排序所系时间和NlgN成正比。
  • 快排的内循环比大多数排序算法都要短小,这意味着无论在理论上还是实际中都要更快。
  • 归并排序和希尔排序一般都比快排慢,其原因就是它们还在内循环中移动数据。
  • 主要缺点是非常脆弱,实现时要非常小心才能避免低劣的性能。

切分方法:

切分满足下面三个条件:

  • 对于某个j,a[j]已经排定
  • a[lo]到a[j-1]中的所有元素都不大于a[j]
  • a[j+1]到a[hi]中的所有元素都不小于a[j]
private static int partition(Comparable[] a,int lo,int 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);    //最后将切分元素交换到正确的位置
		return j;
}

快排实现:

快速排序的实现需要注意几个细节:

  • 原地切分。避免使用辅助数组,减小数组复制之间的开销。
  • 别越界。如果切分元素是数组中最大或最小的元素,要特别小心别让扫描指针跑出数组边界。
  • 保持随机性。
  • 处理切分元素值有重复的情况。糟糕的处理可能会使算法运行时间变为平方级别。
  • 终止递归。
public static void sort(Comparable[] a){
	sort(a,0,a.length-1);
}

private static void sort(Comparable[] a,int lo,int hi){
	if(hi<=lo)return ;    //终止递归
	int j = partition(a,lo,hi);    //切分
	sort(a,lo,j-1);    //递归左半部分排序
	sort(a,j+1,hi);    //递归右半部分排序
}

算法改进

  • 切换到插入排序
  • 三取样切分
  • 熵最优排序

3  三向快速排序

含有大量重复元素的数组,快速排序还有巨大的改进空间。一个经典的算法是Dijkstra的“三向切分的快速排序”。它从左到右遍历数组,设有三个指针lt,i,gt。使a[lo...lt-1]中的元素都小于v,a[gt+1...hi]中的元素都大于v,a[lt...i-1]元素都等于v,a[i...gt]元素未定。

  1. a[i]小于v, 将a[lt]和a[i]交换,将lt和i加一;
  2. a[i]大于v, 将a[gt]和a[i]交换,将gt减一;
  3. a[i]等于v, 将i加一。

对于包含大量相同元素的数组,它将排序时间线性对数级别降到了线性级别。

代码实现

private static void sort(Comparable[] a,int lo,int hi){
	if(hi<=lo)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++;    //等于,放中间
	}
    //只递归左右小于和大于V的部分,中间等于V的部分不需要递归
	sort(a,lo,lt-1);
	sort(a,gt+1,hi);
}

算法改进:

  • 哨兵:可以通过设置哨兵来去掉while中的边界检查。由于切分元素本身就是一个哨兵,左侧边界检查是多余的;可以将数组中的最大元素放置在a[length-1]中来去掉右部检查。注意:在处理内部数组中,右子数组最左侧元素可以成为左子数组的哨兵。
  • 非递归的快速排序:可以使用一个循环来将弹出栈的切分并将结果子数组重新压栈来实现非递归快排。注意:先将较大子数组压栈可以保证栈中最多只会有lgN个元素。
  • 快速三向切分:可以讲相等的元素放在数组两边而不是中间实现快速三向切分。

4  堆排序

基础知识:

了解堆排序之前要先了解堆,数据结构--堆的构造和实现

堆排序可以分为两个阶段:

  1. 构造堆。将原始数组重新组织安排进一个堆中
  2. 下沉排序。从堆中按递减顺序取出所有元素并得到排序结果

用下沉操作由N个元素构造堆只需少于2N次比较以及少于N次交换。

将N个元素排序,堆排序只需少于(2NlgN+2N)次比较以及一半次数的交换。2N来字堆的构造。

堆排特点:

  • 唯一的能够同时最优地利用空间和时间的方法。
  • 无法利用缓存。数组元素很少和相邻的元素直接比较,因此缓存未命中的次数远远高于其他排序算法。
  • 能够在插入操作和删除最大元素操作混合的动态场景中保证对数级别的运行时间。

堆排实现:

  • 代码中堆是用数组实现的,数组a[0]弃之不用,堆顶元素存在a[1]中。
  • 最先构造的堆是最大堆(元素越大越靠近堆顶),然后通过循环交换a[1]和a[N--]元素,使大元素沉到数组底部,并修复堆。如此循环直到堆为空,则实现堆的数组中元素已经排好序了。
  • 下面代码是堆排序主要算法,具体堆的实现可以参考数据结构----堆。
public static void sort(Comparable[] a){
	int N = a.length;
	for(int k=N/2;k>=1;k--)//构造堆
		sink(a,k,N);//由上至下的堆有序化(下沉)的实现
	while(N>1){
		exch(a,1,N--);//交换
		sink(a,1,N);//修复堆
	}
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值