从上课划水到校招备考,我对排序的第二次理解

排序算法基本每个学语言的人都会写好几次。。在回顾排序算法的时候发现了上课划水时的一些错误理解。

1.旧的错误:在高效排序中局部使用了低效的‘冒泡’

2.新的发现:希尔排序与堆排序的‘相似’之处

3.旧的错误:堆排序的initHeap()的错误思路与正确思路

4.旧的错误:错误的并归排序写法——为了节省空间,让并归排序在同一个表中进行——结果每个小数组仍然要使用简单排序

5.旧的错误:错误的快排算法写法——取值思路错误,导致又用了简单排序

6.稳定性的作用:第一次理解到稳定性到底有什么意义

 

基础:

冒泡:

复杂度O(n^2),稳定,

冒泡排序很简单,思路也很有趣,但是性能比较低,所以不要写冒泡排序

无序数组:985211369

冒泡1次:  89 5211369-->859 211369-->8529 11369-->~~~~852113699

冒泡2次:  58 2113699-->528 113699-->5218 13699-->~~~~521136899

这里说一下冒泡是因为后面会多次提到,很可能在其他高效排序中一不小心就用到了低性能的冒泡

选择:

复杂度O(n^2),不稳定

选择排序是大部分小伙计上手就会用的排序,遍历找min,交换,思路清晰,代码简洁  省略

 

直接插入排序:

复杂度O(n^2),稳定

直接插入是理解希尔排序的基础。也是三个基础排序中性能最高的,所以即便写不出快排等,也要用直接插入排序

无序数组:  int[] arr=new int[] {5,1,0,7,2,14,17,10,11,13}

数组看成两个表,前有序,后无序

排序第一趟:1和5交换 得{1,5,0,7,2,14,17,10,11,13}

排序第二趟:0和5交换,再0和1交换,得{0,1,5,7,2,14,17,10,11,13}

嗯,上述第二趟比较的说明是错误的,这也是我最开始写直接插入排序时候的错误。。就是——“写成了冒泡”

按照上面的说法,代码是:

static void fakeISort(int[] arr) {
	for(int i=1;i<arr.length;i++) {
		int j=i;    //j初始指向无序表表头
		while(j>0&&arr[j]<arr[j-1]) {
			int t=arr[j];
			arr[j]=arr[j-1];
			arr[j-1]=t;
			--j;
		}
	}
}

看其中交换的操作,比如  453,交换一次 435,交换两次345,,实际把这部分写成了冒泡算法

实际上应该写出的代码是:

static void ISort(int[] arr) {
	for(int i=1;i<arr.length;i++) {
	    int val=arr[i];
	    int j=i;
	    while(j>0&&val<arr[j-1]){
	        arr[j]=arr[j-1];
	        j--;
	    }arr[j]=val;
	}
}

由于在一个数往前移动的过程中,冒泡的思路深入人心,,总是在无意间会用到,甚至包括后面会出现的希尔排序,堆排序。切记不要再写出‘冒泡’风格的直接插入排序算法了、

 

进阶:

SheelSort  希尔排序   第一个复杂度超越n^2的排序算法

复杂度:希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。   [百度百科]

分析一波比较次数

凭空分析:

直接插入排序从arr[1]开始到arr[n-1]结束,对于每一个值,向前对比直到前者小于自身为止

希尔排序共K*increment+Y趟  (具体看取值)

第1趟从arr[increment]到arr[k*increment],对于每一个值,向前跳跃式对比直到前者小于自身为止

~~~~

第K*increment趟(此时increment=1),从arr[1]到arr[n-1],对于每一个值,向前对比直到前者小于自身为止

凭空分析结论:数学不好,,不好说   不过可以发现其实希尔排序的最后一次对比等于直接插入排序,但此时数组已经基本有序!————(所以还可以得出结论,数组基本有序的情况有利于直接插入排序效率)

 

实例分析:假设有数组  5 1 17 7 2 14 17 10 11 13

(希尔排序的增量值取1/3)所以外层排序了3次

希尔排序代码如下:

static void fakeSheelSort(int[] arr) {
	int inc=arr.length;
	do {
		inc=inc/3+1;  
//增量inc意味着待排序的arr[index]将与arr[index-inc*k]对比,而不是对比arr[index-1*k]

		for(int i=inc;i<arr.length;i++) {
			int j=i-inc,I=i;
			while(j>=0&&arr[j]>arr[i]) {  //以下是错误的写法,即冒泡
				int t=arr[j];
				arr[j]=arr[i];
				arr[i]=t;
				j-=inc;i-=inc;
			}i=I;
		}
	}while(inc>1);
}

//正确的写法只是将里面的for循环改动一下: 即冒泡改成直接插入
for(int i=inc;i<arr.length;i++) {
	int temp=arr[i];
	int j=i-inc;
	while(j>=0&&temp<arr[j]) {
		arr[j+inc]=arr[j];
		j-=inc;
	}arr[j+inc]=temp;
}

 

堆排序

说道堆排序不得不说一下二叉树。。我本来打算先复习一下二叉树再来看堆排序的,后来觉得有了堆排序的引入,写二叉树可能会更简单。

不过开始之前,对于二叉树至少要有这些了解:

根节点、子节点的定义。层序遍历的概念。

当对二叉树层序遍历,设根节点为arr[1],则有对于节点arr[i],其左孩子节点为arr[2*i],右孩子为arr[2*i+1]

如图所示

算法思想:

如图所示的两棵二叉树被称为大顶堆/小顶堆

容易看出,大顶堆根节点大于子节点,小顶堆反之

如果说我们可以把待排数组构建成大顶堆(这里一直用大顶堆举例),然后将根节点去掉(因为根节点最大,直接放在数组最后一位,对最大值的排序结束),剩余部分再次构建大顶堆,重复上述动作直到最后一个根节点被移除

(。天晓得这种结构|思路是怎么想出来的~~)

现在过程比较明确:

1.构建大顶堆  initHeap()

2.将大顶堆根节点移出作为最大值,重建(修复)大顶堆

难点在于如何构建大顶堆

这里的理性推理就省略了。。我当时没看书的时候自己想出了一种正确但绕了弯子的思路。。这种东西看一遍书基本不会忘的

从二叉树最后一个(按层序遍历)非叶子结点向前循环
对于每一个节点node,将以该节点为根节点的子树,调整至大顶堆
调整方法为,如果node的子节点中存在比note更大的值,让node与更大那一个子节点‘交换’,以此类推

图示: 假设有数组  5 1 17 7 2 14 17 10 11 13

首先如上左图,层序倒数第一个分支节点为第三层第二个节点②,调整以②为根的树为大顶堆。接着调整以⑦为根的树

然后如上右图,二层第二个节点(17)为根的树无需调整。接着调整(1)为根的树,(1)与右孩子(13)交换,然后再调整其子树

后续步骤省略

 

准备工作完成,下面是某一种代码实现——代码没有这么长,这中间另外写了错误的冒泡写法

public class HeapSort {
	public static void heapSort(int[] arr) {
		//数组arr没有做特殊处理,所以arr[0]左孩子arr[1],右孩子arr[2]
		//一般的,节点arr[i]的左孩子为arr[2*i+1],右孩子为arr[2*i+2]
		
		int last=(arr.length-1)/2;   //寻找层序倒数第一个分支节点
		initHeap(arr,last,arr.length);  //初始化堆
		
		//排序--重建堆--排序~~
		int len=arr.length-1;
		while(len>0) {
			int max=arr[0];
			arr[0]=arr[len];
			arr[len--]=max;
			initHeap2(arr,0,len+1);
		}
	}
	//构建大顶堆。参数:数组,最后一个待排序的分支节点下标,数组长度
	//初次新建大顶堆所有分支节点都需要排序。后续还原大顶堆仅仅根节点需要排序
	private static void initHeap(int[] arr, int last,int len) {
		int i=last;
		while(last>=0) {
			i=last;
			while(i*2+2<len) {  //‘冒泡’排序写法——疯狂交换
				int left=arr[2*i+1],right=arr[2*i+2];
				if(arr[i]>=left&&arr[i]>=right)
					break;
				else {
					if(left>right) {
							arr[2*i+1]=arr[i];
							arr[i]=left;
							i=i*2+1;
					}else {
							arr[2*i+2]=arr[i];
							arr[i]=right;
							i=i*2+2;
					}
				}
			}
			if(i*2+1<len&&arr[i*2+1]>arr[i]) {
				int t=arr[i*2+1];
				arr[i*2+1]=arr[i];
				arr[i]=t;
			}--last;
		}	
	}
	private static void initHeap2(int[] arr, int last,int len) {
		int i=last;
		while(last>=0) {
			i=last;
			int key=arr[i];  //正确写法,类似于希尔排序(或直接插入)
			while(i*2+2<len) {
				int left=arr[2*i+1],right=arr[2*i+2];
				if(key>=left&&key>=right) {
					arr[i]=key;
					break;
				}else {  //将孩子中较大者向上转,i指向该较大值
					int bigger=right>left?i*2+2:i*2+1;
					arr[i]=arr[bigger];
					i=bigger;
				}
			}
			if(i*2+1<len&&arr[i*2+1]>key) {
				arr[i]=arr[i*2+1];
				i=i*2+1;
			}arr[i]=key;
			--last;
		}	
	}
	public static void main(String[] args) {  //测试
		int[] arr=Helper.randomArr(10, 100);
		Helper.show(arr);
		heapSort(arr);
		Helper.show(arr);
	}
}

 

想没想过堆排序其实也有希尔排序的影子  (只是个人理解。)

希尔排序中,每个元素arr[index]会与前面的0个或多个arr[index-inc*k]进行比较。堆排序中,每个arr[index]会与其左右孩子进行比较,即与arr[index*2+1]/arr[index*2+2]比较,通过比较结果的不同进行下一步。

它们都是将低效排序的相邻对比转换成了高效的间隔对比,不过希尔排序是线性增加的间隔,堆排序是平方级增长

 

归并排序(仅二路)

堆排序借助了二叉树的性质,很高效,但是这种结构在未产生之前是很难想到的。下面要说的归并排序就是一种思路比较清晰的算法

假设待排数组arr有n个值,那我们可以将待排数组看成n个有序子集合,如下所示

#   #   #   #   #   #   #   #   #   #   #  #   #   #   #   #     //有序子集合,每个集合长度不超过1

# #   # #   # #   # #   # #   # #   # #   # #       //相邻两个子集合进行归并排序得到新的子集合

# # # #   # # # #   # # # #   # # # #  

# # # # # # # #   # # # # # # # #  

# # # # # # # # # # # # # # # #   //直至最后排序结束

综上,归并排序的核心在于:将两个有序子集合合并成一个有序子集合

原始的归并排序吧上述集合看成真的集合,,每次合并都要耗费新的空间(空间换时间),主要原因是局部排序也会耗费时间,举个例子:

子集A:1 4 7    子集B:2 5 8

如果采用真的数组C来保存,写法是  C[k++]=Max(A[i],B[j]); 

如果在同一张表上处理,则  序列  1 4 7——2 5 8 还要进行排序,又会用到简单排序算法。。那就不是归并排序了

下面是错误的代码实现——错误的原因在于我们在局部排序中使用了直接插入排序,而不是归并排序要求的空间换时间

public class MSort {
	static void merge(int[] A,int art,int aend,int bend) {
		//说明,A为待合并数组,其中A[art,aend]有序且A[aend+1,bend]有序
		if(bend>=A.length)bend=A.length-1;
		for(int i=aend+1;i<=bend;i++) {
			while(A[i]<A[i-1]) {
				int key=A[i];
				int j=i-1;
				while(j>=0&&A[j]>key)
					A[j+1]=A[j--];
				A[++j]=key;
			}
		}
	}
	static void mSort(int[] arr) {
		for(int i=1;i<arr.length;i*=2) {//i代表子数组长度
			for(int j=0;j<arr.length;j+=2*i) {//将数组分成多份,每份长度为i(除去最后一份)
				System.out.print("排序arr["+j+","+(j+i-1)+"],arr["+(j+i)+","+(j+2*i-1)+"]");
				merge(arr,j,j+i-1,j+2*i-1);
			}System.out.println();
		}
	}
	public static void main(String[] args) {
		int[] arr=Helper.randomArr(10, 100);
		Helper.show(arr);
		mSort(arr);
		Helper.show(arr);
	}
}

 

一种正确思路的代码如下:

static int[] sort(int[] arr, int len) {
	if(len==1)
		return arr;
	if(len==2) {
		if(arr[0]>arr[1]) {
			int t=arr[0];arr[0]=arr[1];arr[1]=t;
		}return arr;
	}
	int[] a=new int[len/2],b=new int[len-len/2];
	int ac=0,bc=0;
	for(int i=0;i<len;i++)
		if(i<len/2)
			a[ac++]=arr[i];
		else
			b[bc++]=arr[i];
	a=sort(a,len/2);b=sort(b,len-len/2);
	System.out.print("A:");Helper.show(a);
	System.out.print("B:");Helper.show(b);
	int[]c=new int[len];ac=0;bc=0;
	for(int i=0;i<len;i++)
		if(bc==b.length||ac<a.length&&a[ac]<b[bc])
			c[i]=a[ac++];
		else c[i]=b[bc++];
	return c;
}
Main:    arr=sort(arr,arr.length);

 

 

快排:

快排其实对比堆排序更好实现,主要是要理解思想

思路:通过一趟排序,将待排记录分割成独立的两部分,其中一部分关键字均比另一部分小,然后分别对每一部分继续分割,直至有序。

更通俗的解释:从待排序列中找到这样一个x,通过调整使得他比左边的数都要大,同时比右边的数都要小。此时x左边的整个部分与x右边的整个部分已经有序,再对左右两部分进行同样的操作即可。

static int partSort(int[] arr,int start,int end) { 
//对[start,end]整体排序,并返回关键字坐标(下次排序的分界)
	int key=arr[start];  //取第一个作为关键字
	int low=start,high=end;
	while(low<high) {
		while(high>low&&arr[high]>=key)  //Q1 Q2
			high--;     //如果右边存在不满足平衡条件的值,让他交换到另一边
		swap(arr,low,high);
		while(low<high&&arr[low]<=key)
			low++;
		swap(arr,low,high);
	}return low;
}

static void fSort(int[] arr,int start,int end) {
	int key1=partSort(arr,start,end);
	if(key1<end)
		fSort(arr,key1+1,end);
	if(key1>start)
		fSort(arr,start,key1-1);
}

//上述写法也可以合并.理解起来更费劲
public static void fastSort(int[] arr,int a,int b){
	if(a>=b)
		return;
	int oa=a,ob=b;
	while(a<b){
		while(a<b&&arr[b]>=arr[a])
			--b;
		int t=arr[a];arr[a]=arr[b];arr[b]=t;
		while(a<b&&arr[a]<=arr[b])
			++a;
		t=arr[b];arr[b]=arr[a];arr[a]=t;
	}
	if(oa<a-1)
		fastSort(arr,oa,a-1);
	if(ob>a+1)
		fastSort(arr,a+1,ob);	
}
Main:  fastSort(arr,0,arr.length-1);

如代码注释所示,两个问题

Q1:为什么要先while(high)后while(low)?

A:因为我们所取的关键字是第一个,对比的方式是当 相等于关键字 则不排序。如果这里写成先while(low)那么我们数组靠前的 那几个等于关键字key的数字 将不会被排序——比如说数组 5 1 2 4 9. key=5 由于arr[low(0)]==5,low++,直接跳过了第一个数字5的排序。。  相应的,如果我们所取的关键字是最后一个,那么就需要先while(low)后while(high)  

Q2:为什么是arr[high]>=key的时候 high--    (为什么arr[low]<=key的时候 low++)  ——为什么要取等

A:如果不取等号,假设有数组  5 5,那么arr[0]和arr[1]会一直交换,直到超时。。

 

 

稳定性

假设现在有一个二维数组arr[k][2]  要求按照arr[i][0]降序排列,如果arr[i][0]相等则保持原顺序不变

实例: Arrays.sort(arr,(a,b)->b[0]-a[0]);     //如果不了解lambda或者不了解comparator自己百度先

这个排序的结果会是错误的,他改变了arr[i][0]相等的元素的先后顺序

另外——mysql中order by采用的算法也是不稳定排序,这会导致出现问题——比如在使用limit做的分页里面出现当前页最后一个数据和下一页第一个数据重复(比较的关键字一样,在两次排序中前后的相对位置变化了),这个是我在其他博文上看到的,不过忘记了是哪一个网址。。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值