晋南讲堂之各类排序算法精讲(附经典Java源码)

  在本文中将介绍我们平时用到的各种排序算法,并详细介绍它们的排序过程。并在最后附带所有这些排序算法的时间复杂度和空间复杂度。

1. 插入排序

1.1 插入排序–直接插入排序

  插入排序的思想是:每一轮都把一个元素,按照其关键字的大小插入到它前面已经排序的子序列中,使得插入后的子序列仍是排序的,直到所有的元素插入为止。直接插入排序的思想如下:从第1个元素开始(Java数组从0开始),每次都和前面已经排序好的子序列去比较,将其放到适当的位置,直到最后一个放好。关键代码如下:

//直接插入排序
public class straightInsertionSort {

	public static void main(String[] args) {
		int[] arr ={32,26,87,72,26,17,100,10,3,6};
		System.out.print("未排序的原序列为:");
		打印(arr);
		StraightInsertionSort(arr);
	}
	
	public static void StraightInsertionSort(int[] arr){
		for(int i=1;i<arr.length;i++){//从第1个元素开始(Java中数组从0开始),扫描n-1次
			int temp=arr[i],j;//把第i个待插入的元素拿出来,后面会用来和前面已经排序好的子序列进行比较
			for(j=i-1;j>=0&&temp<arr[j];j--)//从前面已经排序好的子序列里,从后向前比较
				arr[j+1]=arr[j];//如果temp比arr[j]小,就将当前元素后移,直到temp>=arr[j]
			arr[j+1]=temp;//此时temp需要放到arr[j]后面,所以是arr[j+1]=temp
			System.out.print("第"+i+"趟排序结果为:");
			打印(arr);	
		}
	}
	
	public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
		for(int a:arr)
			System.out.print(a+"\t");
		System.out.println();
	}

}

最后的排序结果如下:
在这里插入图片描述
直接插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1),直接插入排序算法稳定。

1.2 插入排序–希尔排序

  希尔排序(shell sort)又称为缩小增量排序,它是分组的直接插入排序。希尔排序将一个数据序列按一定的距离进行分组,这段距离称为增量。
假设数组为 a 0 , a 1 , a 2 , . . . , a n − 2 , a n − 1 a_0,a_1,a_2,...,a_{n-2},a_{n-1} a0,a1,a2,...,an2,an1,设增量 δ \delta δ为3,则在该增量下数据分组为: { a 0 , a 0 + δ , a 0 + 2 δ , a 0 + 3 δ , . . . . } \{a_0,a_{0+\delta},a_{0+2\delta},a_{0+3\delta},....\} {a0,a0+δ,a0+2δ,a0+3δ,....} { a 1 , a 1 + δ , a 1 + 2 δ , a 1 + 3 δ , . . . . } \{a_1,a_{1+\delta},a_{1+2\delta},a_{1+3\delta},....\} {a1,a1+δ,a1+2δ,a1+3δ,....} { a 2 , a 2 + δ , a 2 + 2 δ , a 2 + 3 δ , . . . . } \{a_2,a_{2+\delta},a_{2+2\delta},a_{2+3\delta},....\} {a2,a2+δ,a2+2δ,a2+3δ,....}共3组数据,对这3组数据都进行直接插入排序。由此可看出增量是几就可以分出几组。在一个组内采用直接插入排序算法进行排序。增量的初值一般设置为数组长度的一半,以后每趟增量逐渐缩小,随着增量的减少,组数也减少,组内元素个数增加,整个数组趋近于有序。最后缩小为1,即这时只有一组,囊括了整个数组,再进行最后一次直接插入排序即可。增量的变化规律有多种方案,在本文将初始增量设为数组的一半,以后增量每次都减半。
  希尔排序的增量减半的源码如下:

//希尔排序
public class ShellSort {
	
	public static void main(String[] args) {
		int[] arr ={32,26,87,72,26,17,100,10,3,6};
		System.out.print("未排序的原序列为-------------:");
		打印(arr);
		shellSort(arr);
	}
	
	public static void shellSort(int[] arr){
		for(int delta=arr.length/2;delta>0;delta/=2){
			for(int i=delta;i<arr.length;i++){//i每次加1的意思会迷惑很多人,这个意思是同时分多个组并行地直接插入排序,i每增加1,就会切换到另一组
				int temp=arr[i],j;//将当前值赋给temp
				for(j=i-delta;j>=0&&temp<arr[j];j-=delta)//每次向前移动delta,寻找同一组的数据进行比较
					arr[j+delta]=arr[j];
				arr[j+delta]=temp;
				System.out.print("增量为"+delta+"时"+"第"+(i%delta+1)+"组的第"+(i/delta)+"次插入排序结果为:");
				打印(arr);	
			}
		}
	}
	
	public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
		for(int a:arr)
			System.out.print(a+"\t");
		System.out.println();
	}

}

观察如上代码,发现只是将直接插入排序每次增量为1的地方改变为增量delta,然后在最外层增加一层控制增量delta变化的for循环即可。将每次发生的插入排序都打印,并打印出组别和排序次数,看起来会更直观一些,读者只需要认真分析如下结果,就会一目了然:
在这里插入图片描述
  希尔排序的时间复杂度分析比较复杂,它取决于如何选取增量序列。它的空间复杂度是 O ( 1 ) O(1) O(1),算法不稳定。

2. 交换排序

  交换排序有冒泡排序和快速排序。

2.1 交换排序–冒泡排序

  冒泡排序的思想是比较相邻的两个元素,如果反序,则交换。如果按照升序排序,每一趟扫描的数据子序列中最大的值将会交换到最后的位置,就像是在冒泡泡。冒泡排序的子序列变化规律如下:
第1趟扫描序列: { a 0 , a 1 , . . . , a n − 2 } \{a_0,a_1,...,a_{n-2}\} {a0,a1,...,an2}
第2趟扫描序列: { a 0 , a 1 , . . . , a n − 3 } \{a_0,a_1,...,a_{n-3}\} {a0,a1,...,an3}
第3趟扫描序列: { a 0 , a 1 , . . . , a n − 4 } \{a_0,a_1,...,a_{n-4}\} {a0,a1,...,an4}

第n-2趟扫描序列: { a 0 , a 1 } \{a_0,a_1\} {a0,a1}
第n-1趟扫描序列: { a 0 } \{a_0\} {a0}
  为了避免后期无效的扫描(比如一个近乎有序的序列根本不需要n-1次扫描),增加一个是否产生交换的标记变量,如果本次扫描未发生交换,则证明数组已经有序,可以终止了。冒泡排序的源码如下:

//冒泡排序
public class BubbleSort {
	
	public static void main(String[] args) {
		int[] arr ={32,26,87,72,26,17,100,10,3,6};
		System.out.print("未排序的原序列为:");
		打印(arr);
		bubbleSort(arr);
	}
	
	public static void bubbleSort(int[] arr){
		boolean exchange=true;//首先将其置为true,便于for循环的正常执行
		for(int i=1;i<arr.length&&exchange;i++){//发生交换时再进行下一轮,最多发生n-1轮扫描
			exchange=false;
			for(int j=0;j<arr.length-i;j++){//该for循环走完一次发生了一趟比较、交换
				if(arr[j]>arr[j+1]){//反序就交换
					int temp=arr[j];
					arr[j]=arr[j+1];
					arr[j+1]=temp;
					exchange=true;//发生交换将交换标志置为true
				}
			}
			System.out.print("第"+i+"趟排序结果为:");
			打印(arr);
		}	
	}
	
	public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
		for(int a:arr)
			System.out.print(a+"\t");
		System.out.println();
	}

}

在这里插入图片描述
当一个序列近乎有序时,有了交换标志不需要再扫描n-1次:
在这里插入图片描述
则增加了交换标志,可减少不必要的扫描。冒泡排序算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),它需要一个辅助空间用于交换两个元素,空间复杂度为 O ( 1 ) O(1) O(1),冒泡排序算法是稳定的。

2.2 交换排序–快速排序

  快速排序是一种分区交换排序算法。用到了分治法的思想。快速排序(quick sort)的基本思想是:在序列中选择一个值作为比较的基准值,每趟从序列的两端开始交替进行,将小于基准值的元素交换到序列前端,将大于基准值得元素交换到序列后端,介于两者之间的位置即为基准值的位置。同时序列被划分为两个子序列,再对子序列用同样的方法进行排序,直到子序列长度为1,完成排序。
  一般选取第一个元素作为基准值,这样前面的元素就空了下来,这时需要从后面从后往前找,找到比基准值小的,就放到前面空出的位置,这样后面的位置就空出来一个,再从前面找比基准值大的,这样依次往复,直到前后交汇,交汇点即为基准值得位置。基准值将序列一分为二,它不再参与后续两个子序列的操作。快速排序的源码如下:

//快速排序
public class QuickSort {
	static int num=0;
	public static void main(String[] args) {
		int[] arr ={38,26,97,19,66,1,5,49};
		System.out.print("未排序的原序列为:");
		打印(arr);
		quickSort(arr,0,arr.length-1);
	}
	
	public static void  quickSort(int[] arr,int start,int end){
		if(start<end){//首先得是个有效的序列,start>end不是序列,start=end只有一个元素,没必要排序
			int i=start,j=end;
			int temp=arr[i];//将序列的第一个值作为基准值
			while(i!=j){//当i==j时停止循环
				while(i<j&&temp<=arr[j])//从后往前找,如果i<j,而且基准值小于序列后端的arr[j],就向前移动
					j--;
				if(i<j)
					arr[i++]=arr[j];//如果此时i仍然小于j,说明找到了一个元素比基准值小的,将其放置到前端空出的位置,然后i向后移动一位
				while(i<j&&arr[i]<=temp)//这个时候后端的位置空出了一个,需要从前往后找比基准值大的元素
					i++;
				if(i<j)
					arr[j--]=arr[i];//找到了之后将这个较大的元素放置到后面空出的 位置
			}
			arr[i]=temp;//这时的i就是基准值得最终位置
			System.out.print("第"+(++num)+"趟排序结果为:");
			打印(arr);	
			quickSort(arr,start,j-1);//前端的子序列再排序
			quickSort(arr,i+1,end);	//后端的子序列再排序
		}
	}
	
	public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
		for(int a:arr)
			System.out.print(a+"\t");
		System.out.println();
	}

}

在这里插入图片描述
快速排序的平均时间复杂度为 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n),空间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),该算法是不稳定的。

3. 选择排序

  选择排序有直接选择排序和堆排序两种

3.1 选择排序–直接选择排序

  直接选择排序(straight select sort)的基本思想是:按升序排序,第一趟从n个元素里找出最小的元素放到最前面,下一趟从n-1个元素里找出最小元素放在前面,以此类推,经过n-1次扫描,完成排序。源码如下:

//直接选择排序
public class StraightSelectSort {
	
	public static void main(String[] args) {
		int[] arr ={32,26,87,72,26,17,100,10,3,6};
		int[] arr1 ={32,26,87,72,26,17,100,10,3,6};
		System.out.print("未排序的原序列为:");
		打印(arr);
		straightSelectSort1(arr);
		straightSelectSort2(arr1);
	}
	
	public static void straightSelectSort1(int[] arr){//这是一种交换次数比较多的一种写法
		int num=0;//用来记录交换次数,可不写
		for(int i=0;i<arr.length-1;i++){//从i=0开始,逐步去确定arr[0],arr[1],...arr[n-2]的值
			for(int j=i+1;j<arr.length;j++)//从i的后面开始查找,查找比arr[i]小的元素
				if(arr[j]<arr[i]){//如果存在,则直接交换arr[i]和arr[j]的值,这样的交换次数会很多,影响效率
					int temp=arr[i];
					arr[i]=arr[j];
					arr[j]=temp;
					num++;//记录交换次数,可不写
				}
			System.out.print("第"+(i+1)+"趟排序结果为:");
			打印(arr);
		}
		System.out.println("交换次数为:"+num);
	}
	
	public static void straightSelectSort2(int[] arr){//这是一种只记录下标的方式,可以大大减少交换次数
		int num=0;//用来记录交换次数,可不写
		for(int i=0;i<arr.length-1;i++){
			int min=i;//记录当前位置下标
			for(int j=i+1;j<arr.length;j++)
				if(arr[j]<arr[min])//如果找到比arr[min]更小的值,则记录其下标
					min=j;
			if(min!=i){//上述for循环执行完毕后,已经找到当前序列的最小值的下标,根据下标进行交换
				int temp=arr[i];
				arr[i]=arr[min];
				arr[min]=temp;
				num++;//记录交换次数,可不写
			}//这样最多只产生n-1次交换
			System.out.print("第"+(i+1)+"趟排序结果为:");
			打印(arr);
		}
		System.out.println("交换次数为:"+num);
	}
	
	public static void 打印(int[] arr){//Java中的方法名完全可以用汉字,但笔者不建议这么做
		for(int a:arr)
			System.out.print(a+"\t");
		System.out.println();
	}
}

从以下的运行结果可明显看出,记录下标的方式可明显减少交换次数,提高运行效率。
在这里插入图片描述
直接选择排序时间复杂度为 O ( n 2 ) O(n^2) O(n2)。空间复杂度为 O ( 1 ) O(1) O(1)。算法不稳定。

3.1 选择排序–堆排序

4. 归并排序

5. 总结

思想排序算法时间复杂度最好情况最坏情况空间复制度稳定性
插入直接插入排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
插入希尔排序 O ( n ( l o g 2 n ) 2 ) O(n(log_2n)^2) O(n(log2n)2) O ( 1 ) O(1) O(1)不稳定
交换冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
交换快速排序 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( l o g 2 n ) O(log_2n) O(log2n)不稳定
选择直接选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定
选择堆排序 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( 1 ) O(1) O(1)不稳定
归并归并排序 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n) O ( n ) O(n) O(n)稳定
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值