疯狂九大排序及其详解

排序算法是我们最常用的算法,之前听牛客大牛左大神讲解后自己详细的整理了一遍,感觉比以前学的深刻很多。下面共享给大家

九大经典排序算法是面试必备的算法知识。九大算法可以根据时间复杂度、空间复杂度、稳定性有不同的划分。本文先基于时间复杂度对九类经典排序算法进行简要介绍和其代码实现。其中会夹杂稳定性和空间复杂度分析。

首先介绍时间复杂度为。其中包括冒泡排序选择排序插入排序

(1)、冒泡排序很简单,首先对0~N-1个数进行比较,从0开始两两比较,哪个大就放在后面,将最大的沉在最后一个位置。然后对0~N-2进行相同操作,大的放在N-2位置。依次类推直道全部比较完毕。冒泡排序每次只将区间中最大的选出,放入该区间的最后。一共进行n此选举,每一次选举都要遍历一遍所以时间复杂度为下面代码实现:

public int[] bubbleSort(int[] a, int n) {
	for(int i=0;i<n;i++){
        	for(int j=0;j<n-i-1;j++){//比较区间为0~(n-i-1)
        		if(a[j]>a[j+1]){//大的后移交互。   
                  int t= a[j];
                  a[j]=a[j+1];
                  a[j+1]=t;
        		}
        	}
     }
     return a;
}

(2)、选择排序

选择排序也冒泡类似,选择排序是每次从区间内选择一个最小值放在区间最开始的位置,共执行n次,每次遍历整个区间,故时间复杂度为。要问选择排序和冒泡排序有什么最大的区别。二者的区别应该在稳定性上。冒泡排序每次比较,如果找到前一个数大的就进行交换。而选择排序只是记录该区间最大的,然后进行交换。选择排序这种选法会破坏稳定性。举个例子

序列为  8  3 4  8  1 2。从该区间选择最最小的元素1与第一个元素8进行交换,但是原序列中有两个8,这样会导致两个8前后顺序变化。故选择排序为不稳定排序。而冒泡排序为稳定排序。不管如何比较交换两个8的顺序不会变。下面上选择排序代码:

public int[] selectionSort(int[] A, int n) {
        for(int i=0;i<n;i++){
        	int min=i;
        	int temp=A[min];
        	for(int j=i+1;j<n;j++){
        		if(A[j]<temp)
        		{
        			temp=A[j];
        			min=j;
        		}
        	}
        	A[min]=A[i];
        	A[i]=temp;
        }
        return A;
}

(3)、插入排序

插入排序是每次将一个数插入已排好序的数列之中。初始排好序的数列只含有一个元素(1个数无所谓是否排好序的吧),然后将第二个数插入,然后将第三个插入。已排好序的数列不断变长,要插入的数不断后移。这种排序大部分时间耗费在数的移动上。如果原数组为较有序的数组排序速度会更佳。当然插入排序是稳定的排序。下面是代码实现:

public int[] insertionSort(int[] A, int n) {
        int temp,j;
		for(int i=1;i<n;i++){
        	temp=A[i];
        	for(j=i;j>0&&A[j-1]>temp;j--){
        		A[j]=A[j-1];//前一个数后移一位,不用担心覆盖,temp已存储该值
        	}
        	A[j]=temp;
        }
        return A;
}

时间复杂度为,其中包括归并排序、快速排序、堆排序、希尔排序

(1)、归并排序

归并排序首先让数组中的每一个数单独成为长度为1的有序区间,然后将相邻的长度为1的有序区间进行合并,得到最大长度为2的有序区间。然后再将相邻有序区间进行合并得到有序区间为4的区间。依次类推。直到整个区间排序。

public int[] mergeSort(int[] A, int n) {
	int[] c=new int[n];  //辅助数组
	sort(A, 0, n-1,c);
	return A;
}
public void sort(int[] A,int left,int right,int[] temp){
	if(left<right){
		int middle=(right+left)/2;
		sort(A, left, middle,temp);
		sort(A, middle+1, right,temp);
		merge(A, left,middle,right,temp);
	}
}
public void merge(int[] a,int lo,int mid,int hi,int[] c){
	int i = lo, j = mid + 1;
	for (int k = lo; k <= hi; k++)//该区间值赋值给辅助数组
		c[k] = a[k];
	for (int k = lo; k <= hi; k++) {
		if (i > mid)//左部分归并完
			a[k] = c[j++];
		else if (j > hi)//右部分归并完
			a[k] = c[i++];
		else if (c[j]<c[i])//左右未归并完,比大小放入
			a[k] = c[j++];//右边小
		else
			a[k] = c[i++];//左边小
	}
}

(2)、快速排序

随机在数组中选择一个数,将小于等于的数放在该数的左侧,大于的数放在该数的右侧。左右两侧分别递归调用快速排序。划分过程为:首先选择最左侧的位置的数,并存储,从区间最右侧开始选择第一个比该数小的放在最左侧的位置,然后从该位置出发向右选择第一个大于该数的放在右侧刚才小于该数的位置。依次类推直到初始选中的数放入最终位置(就是右侧数比它大,左侧数比它小)。下面是代码

public int[] quickSort(int[] A, int n) {
       qSort(A, 0, n-1);
       return A;
}
public void qSort(int[] A,int left,int right){
	if(left<right){
		int i=left,j=right;
		int aim=A[i];
		while(i<j){
			while(i<j&&A[j]>aim)
			  j--;
		       A[i]=A[j];
			while(i<j&&A[i]<=aim)
			  i++;
			A[j]=A[i];
		}
		A[i]=aim;
		qSort(A, left, i-1);
		qSort(A, i+1,right);
	}
		
}

(3)、堆排序

堆是一个数组,它可以被看成一个近似的完全二叉树,树上的每一个节点对应数组中的一个元素。除最底层外,该树是完全充满的,并且是从左到右填充。堆分为两种大根堆和小根堆。大根堆的性质是除了根节点以外所有节点i都满足:A[parent(i)]>=A[i],也就是说某个节点的值之多与其父节点一样大,因此堆中最大元素存放在根节点处。并且在任一子树中,该子树中所包含的所有节点的值都不大于树根节点的值。小根堆性质与之相反不在介绍。下面讲的堆默认为大根堆。

堆排序首先要构建初始化堆。初始化堆的性质就是任意节点都不大于其父节点。首先将最后一个节点与根节点进行交换,然后调整0~n-2区间堆为大根堆。然后将根节点与n-2位置交换,然后调整0~n-3区间堆为大根堆。依次类推,则数组为递增的序列,该堆也为排好序的堆。

public void heapSort(int[] a,int n){
    	buildMaxHeap(a);//构建初始化堆(满足任意节点不大于父节点的值)
    	for(int i=n-1;i>=1;i--){//堆排序,a[i]存放最大的,然后调整0~i-1为大根堆
    		int t=a[i];
    		a[i]=a[0];
    		a[0]=t;
    		maxHeap(a, 0,i);
    	}
    }
public void buildMaxHeap(int[] a){
    	int size=a.length/2;
    	for(int i=size;i>=0;i--){
    		heapFix(a, i, a.length);
    		//maxHeapFeiDiGui(a, i,a.length);
    	}
}
public void maxHeap(int[] a,int i,int n){//该方法为递归 可改为非递归
    	int l=2*i+1;//左孩子
    	int r=2*i+2;//右孩子
       int largest=i;
     if(l<n&&a[l]>a[i])//左右孩子比父节点大才进行调整,如果该孩子不是叶节点,要调整该子树
    		largest=l;
     if(r<n&&a[r]>a[largest])
    		largest=r;
     if(largest!=i){
    		int t=a[i];
    		a[i]=a[largest];
    		a[largest]=t;
    		maxHeap(a, largest,n);
    }
 }
</pre><pre code_snippet_id="1927145" snippet_file_name="blog_20161013_7_135705" name="code" class="java"><pre code_snippet_id="1927145" snippet_file_name="blog_20161013_7_135705" name="code" class="java">(4)、希尔排序</p><p>希尔排序其实是插入排序的一种改进。根据步长来进行插入排序。步长的选取会应该时间效率。本文初始步长默认选取n/2长度。</p><p>其过程为:假设步长初始为4,数组长度为8,首先从4出发,对0,4进行插入排序,然后是1,5然后是2,6;3,7。然后步长减半为2,对0,2;1,3;0,2,4;1,3,5;0,2,4,6;1,3,5,7进行插入排序。然后步长再减半为1,对0,1,2,3,4,5,6,7进行插入排序。代码如下:

public int[] shellSort(int[] A, int n) {//插入排序的一种改进版
	int h=A.length/2;
    	while(h>=1){
    		for(int i=h;i<A.length;i++){
	    		for(int j=i;j>=h&&A[j]<A[j-h];j-=h){
	    			int t=A[j];
	    			A[j]=A[j-h];
	    			A[j-h]=t;
	    		}	
	    	}
    		h=h/2;
    	}
	return A;
}

时间复杂度为线性,这类算法不是基于比较的排序算法,思想来源于桶排序

主要分为计数排序和基数排序。

(1)、计数排序

计数排序故名思议就是用标志位来记录出现的次数,然后从标志位的最小位依次遍历,输出该数组变是有序递增的序列。如图:

代码的实现过程:首先要对标志位个数的选取术语为桶的选取。也就是先找出该数组中的最大值和最小值。然后桶的个数变为max-min+1个。桶中存放的值为该数组中该桶出现的次数,每个桶对应的值为min+i(第几个桶)。比如数组2 4 3 1 3 1 6,最小值为1,最大值为6,桶的个数为6个,桶0对应的值为min(1)+0(第几个桶)=1,桶0存放的值为2(1出现了两次)。依次类推桶1对应值为2,存放的值为1。桶2对应的值为3,存放的值为1,以此类推等等。

下面上代码

public int[] countingSort(int[] A, int n) {//计数排序
        int min=A[0],max=A[0];
        for(int i=0;i<n;i++){//找最大值与最小值 来决定建桶的个数
        	min=A[i]<min?A[i]:min;
        	max=A[i]<max?max:A[i];
        }
        int[] visi=new int[max-min+1];
        for(int i=0;i<n;i++){//计数
        	visi[A[i]-min]+=1;
        }
        int index=0;
        for(int i=0;i<max-min+1;i++){
        	while((visi[i]--)!=0){
        		A[index++]=i+min;
        	}
        }
        return A;
}

(2)、基数排序

基数排序感觉是一种挺好玩的排序。从例子讲起吧。

比如数组为

189,132,62,13,7,69,4,33,146,104,26,112,52,17,181,132

首先根据各数的个位数字对数组进行排序为

181 132 62 112 52 13213 33 4 104 146 26 7 17 189 69

然后在此基础上根据各数的十位数字进行排序为

4 104 7 112 13 17 26132 132 33 146 52 62 69 181 189

最后在此基础上根据各数的百位数字进行排序为

4 7 13 17 26 33 52 6269 104 112 132 132 146 181 189

排序结束,即为递增有序的数组,是不是很奇妙?关于位数的选取就是看最大的数为几位,就进行几次循环排序。比如该例子中最大的数为189,为3位数字,故进行3次循环,从个位往高位开始。但是数组中有些数不满三位如4,7,13等。这时候如果进行十位数上进行排序,不满的为0,也就是4的十位和百位取为0,7也取为0,0。而13十位取1,百位取0。对了基数排序每次循环必须用稳定排序不能选用不稳定排序,这样是为了避免破坏前一次循环排序位置先后。本文采用的插入排序

下面进行代码的实现。直接上代码

public int getD(int a,int d){//获取该数组第d位上的数,1为个位,2为十位。。。
		if(a>=Math.pow(10, d-1)){
			a=(int)(a/Math.pow(10, d-1));
			return a%10;
		}else return 0;
	}
public int[] radixSort(int[] A, int n) {//基数排序
	 int max=A[0];
	 for(int i=0;i<A.length;i++){
	    max=max>A[i]?max:A[i];
	 }
	 int d=String.valueOf(max).length();
	 for(int i=1;i<=d;i++){
	     sort(A, i,n);
	}
	return A;
}
public void sort(int[] A, int d,int n) {//基数排序所用的插入排序
		int temp,j;
		for(int i=1;i<n;i++){
	          temp=A[i];
	          for(j=i;j>0&&getD(A[j-1],d)>getD(temp,d);j--){
	        	A[j]=A[j-1];//前一个数后移一位,不用担心覆盖,temp已存储该值
	          }
	          A[j]=temp;
	      }
}

总结

时间复杂度:



这是最基础的九大排序算法。没有孰优孰劣之分,只是应用场景不同罢了。可以根据不同场景采用不同的排序算法,所获得的时间复杂度也不同。等以后再举例介绍给大家。

如有错误欢迎指正!








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值