算法笔记2——排序

基础排序

1.选择排序:
    首先,找到数组中最小的那个元素,其次,将他和数组第一个元素交换位置,
    再次,在剩下的元素中找到最小的元素,将他和数组的第二个元素交换位置。
    如此反复,直到将整个数组排序。不断的选择剩余元素的最小值
2.插入排序:(对部分有序数组很有效)
    为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。

	public class Insertion {

		public static void main(String[] args) {
			
			int []arr= {12,16,3,64,-5,69};
			for (int i = 1; i < arr.length; i++) {
				for(int j=i;j>0&&arr[j]<arr[j-1];j--) {
					int temp=arr[j];
					arr[j]=arr[j-1];
					arr[j-1]=temp;
				}
			}
			System.out.println(Arrays.toString(arr));
		}
	}	

    插入排序不会访问索引右侧的元素,而排序不会访问索引左侧的元素。
    对于随机排序数组,两者的运行时间都是平方级别的。

3.希尔排序
    对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点的从数组的一段移动到另外一端。
    希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组进行排序。
    希尔排序的思想是是数组中任意间隔为h的元素是有序的。这样的数组都是h有序数组。换句话说,一个h有序数组就是h个相互独立的有序数
    组编制在一起组成的一个数组。在进行排序时,如果h很大,我们就能将元素移动到很远的地方,为实现更小的h有序创造方便。
    用这种方式,对于任意以1结尾的h序列,我们都够能将数组排序。
    希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的, 这种情况很适合插入排序。

	public class XirSort {

		public static void main(String[] args) {
			
			int []arr= {12,16,3,64,-5,69,25,75,95,-4,-36,52,76,-92,34,83};
			int N=arr.length;
			int h=1;
			while(h<N/3)h=h*3+1;//1,4,13,40,121,364,1093, ...
			while(h>=1)
			{
				for (int i = h; i < N; i++) {
					for (int j = i; j>=h&&arr[j]<arr[j-h]; j-=h) {
						int temp=arr[j];
						arr[j]=arr[j-h];
						arr[j-h]=temp;
					}
				}
				h=h/3;
			}
			System.out.println(Arrays.toString(arr));
		}
	}


高级排序

4.归并排序:要将一个数组排序,可以先将它分成两半分别排序,然后将结果归并起来。
    优点:保证任意长度为N的数组排序所需的时间和NlogN成正比
    缺点:所需的额外空间和N成正比。

原地归并抽象方法:将子数组a[lo...mid]和a[mid+1...hi]归并为一个有序的数组并将结果保存到a[lo...hi]中

从aux[]两边取值,谁小取谁,左边取完了取右边,右边取完了取左边。
该方法将所有元素复制到aux[]中,然后再归并到a[]中。方法在归并时(第二个for循环)进行了四个条件判断:
左半边用尽(取右半边的元素)、右半边用尽(取左半边元素)、右半边的当前元素小于左半边的当前元素(取右半边的元素)
以及右半边的当前元素大于等于左半边的当前元素(取左半边元素)。

	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[lo...hi]复制到aux[lo...hi]
			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(aux[j]<aux[i])	a[k]=aux[j++];
			else					a[k]=aux[i++];
	}

自顶向下的归并排序:分治思想

要对子数组a[lo...hi]进行排序,先将它分为a[lo...mid]和a[mid+1...hi]两部分,
分别通过递归调用将他们单独排序,最后将有序的子数组归并为最终的排序结果。

	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)
		{
			if(hi<=ho)	return;
			int mid=lo+(hi-lo)/2;
			sort(a,lo,mid);		//将左半边排序
			sort(a,mid+1,hi);	//将右半边排序
			merge(a,lo,mid,hi);	//归并
		}
	}

 

自底向上的归并排序:先归并那些微型数组,然后再成对归并得到子数组,如此这般,知道整个数组归并到一起。
首先进行的是两两的归并(把每个元素想象成一个大小为1的数组),然后是四四归并(把两个大小为2的数组归并为大小为4的数组),然后一直下去。。。
在每一轮归并中,最后一次归并的第二个数组可能比第一个子数组要小,如果不是的话所有归并的中两个数组的大小都应该一样。而下一轮会翻倍。
自底向上的归并排序会多次遍历整个数组,根据子数组大小进行两两归并。子数组的大小sz的初始值为1,每次加倍。最后一个子数组的大小只有在数组是sz的偶数倍的时候才会等于sz(否则它会比sz小)。

public class MergeBU
{
	private static Comparable[]aux;		//归并所需的辅助数组
	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));
	}		
}

快速排序

特点:原地排序(只需要一个很小的辅助栈),且长度为N的数组排序所需的时间和NlogN成正比。
快速排序是一种分治的排序方法。
快速排序和归并排序时互补的:归并排序将数组分成两个子数组分别排序,并将有序数组归并以将整个数组排序;而快速排序将数组排序的方式是将两个子数组都有序时整个数组也就有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分为2半;在快速排序中,切分的位置取决于数组的内容。

public class Quick
{
	public static void sort(Comparable[]a)
	{
		StdRandom.shuffle(a);	//将原数组随即打乱,消除对输入的依赖
		sort(a,0,a.length-1);
	}
	public 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);				//将右半部分排序
	}
}	

快速排序递归的将子数组a[lo...hi]排序,先用partition()方法将a[j]放到一个合适的位置,然后再用递归调用将其他位置的元素排序。

切分:
对于某个j,a[j]已经排定;
a[lo]到a[j-1]中的所有元素都不大于a[j];
a[j+1]到a[hi]中的所有元素都不小于a[j]。

要完成这个实现,需要实现切分方法。一般策略是先随意取a[lo]作为切分元素,既那个将会被排定的元素,然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端向左扫描直到找到一个小于等于它的元素。这两个元素显然是没有排定的,交换他们的位置。如此继续,我们可以保证左指针i左侧元素都不大于切分元素,右指针j的右侧元素都不小于切分元素。当两个指针相遇时,我们只需要将切分元素a[lo]和左子数组最右侧的元素(a[j])交换然后返回j即可。

private static int partition(Comparable[]a,int lo,int hi)
{//将数组切分为a[lo...j-1],a[j],a[j+1...hi]
	int i=lo,j=hi+1;	//左右扫描指针
	Comparable v=a[lo];//切分元素
	while(true)
	{
		while(a[++i]<v)	if(i==hi)	break;
		while(a[--j]>v)	if(j==lo)	break;
		if(i>=j)break;
		int temp=a[i];
		a[i]=a[j];
		a[j]=temp;		
	}
	int temp=a[lo];	//将v=a[lo]放到正确的位置上
	a[lo]=a[j];
	a[j]=temp;
	return j;		//返回位置角标
}

这段代码我们按照a[lo]的值切分。当指针i和j相遇时主循环退出。在循环中,a[i]小于v时我们增大i,a[j]大于v时我们减小j,然后交换a[i]和a[j]来保证i左侧的元素都不大于v,j右侧的元素都不小于v。当指针相遇时交换a[lo]和a[j],切分结束。

 

优先队列

是一种抽象的数据类型,最重要的操作就是删除最大元素和插入元素。
堆:二叉堆的数组中,每个元素都要的大于等于另外两个特定的元素。
堆有序:一个二叉树的节点都大于等于它的两个子节点
完全二叉树:只用数组不用指针,具体方法是将二叉树的节点按层级顺序放入到数组中。
在一个堆中,位置k节点的父节点的位置是k/2,而它的两个子节点的位置则分别是2k和2k+1.

优先队列的实现:用长度为N+1的私有数组pq[]来表示一个大小为N的堆,我们不使用pq[0],堆元素放在pq[1]到pq[N]中。

右下至上的堆的有序化(上浮):如果堆的有序状态因为某个节点变得比它的父节点大而被打破,那么就需要交换它和它的父节点来修复堆。

private void swim(int k)
{
	while(k>1&&less(k/2,k))
	{
		exch(k/2,k);
		k=k/2;
	}
}

由上至下的堆有序化(下沉):如果堆的有序状态因为某个节点变得比它的两个子节点小而被打破,那么就需要交换它和它的两个子节点中较大者来修复堆。

private void sink(int k)
{
	while(2*k<=N)
	{
		int j=2*k;
		if(j<N&&less(j,j+k))j++;
		if(!less(k,j))break;
		exch(k,j);
		k=j;
	}
}

基于堆的优先队列

public class MaxPQ<Key extends Comparable<Key>>
{
	private Key[]pq;
	private int N=0;
	
	public MaxPQ(int maxN)
	{
		pq=(Key[])new Comparable[maxN+1];
	}
	public boolean isEmpty()
	{
		return N==0;
	}
	public int size()
	{
		return N;
	}
	public void insert(Key v)
	{
		pq[N++]=v;
		swim(N);
	}
	/*
	从数组顶端删除最大元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置
	*/
	public Key delMAX()
	{
		Key max=pq[1];	//从根节点得到最大元素
		exch(1,N--);	//将其和最后一个节点交换
		pq[N+1]=null;	//防止越界
		sink(1);		//恢复堆的有序性
		return max;		
	}
	private boolean less (int i,int j)
	{
		return pq[i].comparableTo(pq[j])<0;
	}
	private void exch(int i,int j)
	{
		Key t=pq[i];pq[i]=pq[j];pq[j]=t;
	}
	private void swim(int k)
	{
		while(k>1&&less(k/2,k))
		{
			exch(k/2,k);
			k=k/2;
		}
	}
	private void sink(int k)
	{
		while(2*k<=N)
		{
			int j=2*k;
			if(j<N&&less(j,j+k))j++;
			if(!less(k,j))break;
			exch(k,j);
			k=j;
		}
	}
}

堆排序:主要分为两个阶段,堆的构造阶段(将一个任意顺序的数组变为堆有序)和下沉排序阶段(将堆排序)

	public static void sort(Commparable[]a)
	{
		int N=a.length;
		//构造堆
		for(int k=N/2;k>=1;k--)
		{
			sink(a,k,N);
		}
		//下沉排序阶段,将最大值a[1]和a[N]交换并修复堆,
		//每次将最大元素删除,然后放入到数组减小空出的位置。
		while(N>1)
		{
			exch(a,1,N--);
			sink(a,1,N);
		}
	}
	private void sink(Commparable[]a,int k,int N)
	{
		while(2*k<=N)
		{
			int j=2*k;
			if(j<N&&less(a,j,j+k))j++;
			if(!less(a,k,j))break;
			exch(k,j);
			k=j;
		}
	}
	private boolean less (Commparable[]a,int i,int j)
	{
		return a[i].comparableTo(a[j])<0;
	}
	private void exch(Commparable[]a,int i,int j)
	{
		Commparable t=a[i];a[i]=a[j];a[j]=t;
	}

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值