常用排序算法

八大排序算法

资料
来源:【尚硅谷】数据结构与算法(Java数据结构与算法)
课件:数据结构和算法.exe
图解排序

一 冒泡排序

  1. 基本介绍
           冒泡排序( bubble sorting )
  2. 基本思想
           通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
           因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)

代码实现

public void bubbleSort(int[] arr){
	int temp=0;	// 临时变量
	boolean flag=false;	// 表示变量,表示是否进行过交换
	for(int i=0;i<arr.length-1;i++){
		for(int j=0;j<arr.length-1-i;j++){
			// 如果前面的数比后面的数大(逆序),则交换
			if(arr[j]>arr[j+1]){
				temp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=temp;
				flag=true;
			}
		}
		
		// 优化:如果在一趟排序中,一次交换都没有发生过,则退出循环
		if(!flag) break;
		// 重置 flag ,进行下次交换
		flag=false;
	}
}
  1. 性能
    • 时间复杂度为 O(n^2)
    • 8 万个数据用时 16s / 23s
  2. 总结
           第一个 for 循环实现第几趟大的排序,第二个 for 循环实现在一次大的排序中,进行几次比较。第一个 for 循环,如果有 5 个数,则进行 5-1=4 次循环。第二个 for 循环可以将所有的数进行比较,但是比较浪费时间,因此比较次数可以根据比较的趟数依次递减

二 选择排序

  1. 基本介绍
           选择排序( select sorting )也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
  2. 基本思想
           第一次从 arr[0]~arr[n-1] 中选取最小值,与 arr[0] 交换,第二次从 arr[1]~arr[n-1] 中选取最小值,与 arr[1] 交换,第三次从 arr[2]~arr[n-1] 中选取最小值,与 arr[2] 交换,…,第 i 次从 arr[i-2]~arr[n-1] 中选取最小值,与 arr[i-1] 交换,…,第 n-1 次从 arr[n-2]~arr[n-1] 中选取最小值,与 arr[n-2] 交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列

代码实现

public void selectSort(int[] arr){
	int minIndex,temp;
	for(int i=0;i<arr.length-1;i++){
		minIndex=i;
		for(int j=i+1;j<arr.length;j++){
			if(arr[minIndex]>arr[j]){	// 说明假定的最小值,并不是最小
				minIndex=j;	// 重置 minIndex
			}
		}
		
		// 将最小值与待交换位置的值进行交换
		if(minIndex!=i){	// 如果假定待交换位置的值已经是最小的,就不需要交换
			temp=arr[minIndex];
			arr[minIndex]=arr[i];
			arr[i]=temp;
		}
	}
}
  1. 性能
    • 时间复杂度为 O(n^2)
    • 8 万个数据用时 2s / 3s
  2. 总结
           第一个 for 循环假定为待插入的位置,同时假定它为当次循环中最小的值的下标,该 for 循环的次数为 arr.length-1 次。第二个 for 循环在待插入位置的后一位开始一直查找完整个数组,来更新最小值的下标,该 for 循环结束一轮后,将待插入位置与最小值下标的值进行交换

三 插入排序

  1. 基本介绍
           插入排序( insertion sorting)属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
  2. 基本思想
           把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排列码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表

代码实现

public void insertSort(int[] arr){
	int insertVal,insertIndex;
	for(int i=1;i<arr.length;i++){
		// 保存待插入的数
		insertVal=arr[i];
		// 保存待插入的下标
		insertIndex=i-1;
		
		/**
		* 给 insertVal 找到插入的位置
		* 说明:
		* 	1.insertIndex>=0 保证在给 insertVal 找插入位置,不越界
		* 	2.insertVal<arr[insertIndex],说明待插入的数,还没有找到插入位置
		* 	3.就需要将 arr[insertIndex] 后移
		*/
		while(insertIndex>=0 && insertVal<arr[insertIndex]){
			arr[insertIndex+1]=arr[insertIndex];
			insertIndex--;
		}
		
		// 当退出 while 循环时,说明插入的位置找到,insertIndex+1
		// 这里判断是否需要赋值(也可以不用判断)
		if(insertIndex+1!=i){
			arr[insertIndex+1]=insertVal;
		}
	}
}
  1. 性能
    • 8 万个数据用时 4s / 5s
  2. 总结
           该 for 循环代表无序表中的第一个数( for 循环从数组下标 1 开始,一直循环完整个数组中的数据 ),从 1 开始是因为第 0 个位置刚开始就被当成有序表的数据。待插入位置为有序表最后一位,即 i-1 。当 insertIndex 下标的值不符和插入的条件时,就把当前 insertIndex 所指的值赋值给它所在的后一位下标上。
           最后赋值需要 insertIndex+1 ,是因为此时 insertIndex 所指的值小于待插入的值(也可能是由于 insertIndex>=0 这个条件不满足,此时 insertIndex=-1,代表此时待插入数据在有序表中最小,需要插入到 0 这个位置,此时 insertIndex+1=0 ),所以待插入的值需要插入到此时 insertIndex 所指的值的后面(此时 insertIndex+1 下标的值已经保存在 insertIndex+1+1 这个下标所在的位置了)

四 希尔排序

  1. 基本介绍
           希尔排序是希尔( Donald Shell )于 1959 年提出的一种排序算法。希尔排序也是一种 插入排序 ,提示简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
  2. 基本思想
           把记录按下标的一定增量分组,对每组使用直接插入排序算法排序,随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止

代码实现(交换法)

public void shellSort(int[] arr){
	int temp;
	for(int gap=arr.length/2;gap>0;gap/=2){
		for(int i=gap;i<arr.length;i++){
			// 遍历各组中所有的元素(共 gap 组,每组有 arr.length/gap 个元素),步长为 gap
			for(int j=i-gap;j>=0;j-=gap){
				// 如果当前元素大于加上步长后的那个元素,说明交换
				if(arr[j]>arr[j+gap]){
					temp=arr[j];
					arr[j]=arr[j+gap];
					arr[j+gap]=temp;
				}
			}
		}
	}
}
/**
* 自己实现
* 
* @param arr
*/
public void shellSort(int[] arr){
	int temp;
	for(int gap=arr.length/2;gap>0;gap/=2){
		for(int i=0;i<arr.length-gap;i++){
			for(int j=0;j<arr.length-gap;j+=gap){
				if(arr[j]>arr[j+gap]){
					temp=arr[j];
					arr[j]=arr[j+gap];
					arr[j+gap]=temp;
				}
			}
		}
	}
}

代码实现(移位法)

public void shellSort(int[] arr){
	// 增量 gap,并逐步缩小增量
	for(int gap=arr.length/2;gap>0;gap/=2){
		// 从第 gap 个元素,逐个对其所在的组进行直接插入排序
		for(int i=gap;i<arr.length;i++){
			int insertIndex=i;
			int insertVal=arr[i];
			if(arr[insertIndex]<arr[insertIndex-gap]){
				while(insertIndex-gap>=0 && insertVal<arr[insertIndex-gap]){
					// 移动
					arr[insertIndex]=arr[insertIndex-gap];
					insertIndex-=gap;
				}
				// 当退出 while 循环后,就给 insertVal 找到了插入的位置
				arr[insertIndex]=insertVal;
			}
		}
	}
}
/**
* 自己实现
* 
* @param arr
*/
public void shellSort(int[] arr){
	for(int gap=arr.length/2;gap>0;gap/=2){
		for(int i=gap;i<arr.length;i++){
			int insertIndex=i-gap;
			int insertVal=arr[i];
			// 移位
			while(insertIndex>=0 && arr[insertIndex]>insertVal){
				arr[insertIndex+gap]=arr[insertIndex];
				insertIndex-=gap;
			}
			arr[insertIndex+gap]=insertVal;
		}
	}
}
  1. 性能
    • 交换法
      • 8 万个数据用时 17s
    • 移位法
      • 8 万个数据用时 0s / 1s
      • 80 万个数据用时 1s
      • 800 万个数据用时 4s
  2. 总结
           根据步长分组,通过算法将每一组实现有序(交换法相当于冒泡排序,移位法相当于插入排序),实现有序后再缩小步长,当步长为 1 时,相当于整个数组了,此时再次进行排序后即为最终答案

五 快速排序

  1. 基本介绍
           快速排序( Quicksort )是对冒泡排序的一种改进
  2. 基本思想
           通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

代码实现

public void quickSort(int[] arr,int left,int right){
	int l=left;	// 左下标
	int r=right; // 右下标
	// int mid=(left+right)/2;	// 中间下标	不能使用,会出现排序错误
	// pivot 中轴值
	int pivot=arr[(left+right)/2];
	int temp; //临时变量,作为交换时使用
	// while 循环的目的是让比 pivot 值小的放到左边,比 pivot 值大的放到右边
	while(l<r){
		// 在 pivot 的左边一直找,找到大于等于 pivot 值的数,才退出
		while(arr[l]<pivot) l++;
		// 在 pivot 的右边一直找,找到小于等于 pivot 值的数,才退出
		while(arr[r]>pivot) r--;
		// 如果 l>=r 说明 pivot 左右两边边的值,已经按照左边全部是小于等于 pivot 值的数,
		// 右边全部是大于等于 pivot 值的数
		if(l>=r) break;
		
		// 交换
		temp=arr[l];
		arr[l]=arr[r];
		arr[r]=temp;
		
		// 如果交换完后,发现这个 arr[l]==pivot 值 相等 r--,前移
		if(arr[l]==pivot) r--;
		// 如果交换完后,发现这个 arr[r]==pivot 值 相等 l++,后移
		if(arr[r]==pivot) l++;
	}
	
	// 如果 l==r ,必须 l++,r--,否则会出现栈溢出
	if(l==r){
		l++;
		r--;
	}
	
	// 向左递归
	if(left<r)	quickSort(arr,left,r);
	// 向右递归
	if(right>l)	quickSort(arr,l,right);
}
  1. 性能
    • 8 万个数据用时 0s / 1s
    • 80 万个数据用时 0s
    • 800 万个数据用时 2s / 4s
  2. 总结

六 归并排序

  1. 基本介绍
           归并排序( MERGE-SORT )是利用归并的思想实现的排序方法,该算法采用经典的分治( divide-and-conquer )策略(分治法将问题分( divide )成一些小的问题然后递归求解,而治( conquer )的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之)
  2. 基本思想
    在这里插入图片描述
    在这里插入图片描述

代码实现

// 分+和方法
public void mergeSort(int[] arr,int left,int right,int[] temp){
	if(left<right){
		int mid=(left+right)/2;	// 中间索引
		// 向左递归进行分解
		mergeSort(arr,left,mid,temp);
		// 向右递归进行分解
		mergeSort(arr,mid+1,right,temp);
		// 合并
		merge(arr,left,mid,right,temp);
	}
}

// 合并的方法
/**
 *
 * @param arr   排序的原始数组
 * @param left  左边有序序列的初始索引
 * @param mid   中间索引
 * @param right 右边索引
 * @param temp  做中转的数组
 */
public static void merge(int[] arr,int left,int mid,int right,int[] temp){
	int i=left;	//初始化 i ,左边有序序列的初始索引
	int j=mid+1;	// 初始化 j ,右边有序序列的初始索引
	int t=0;	// 指向 temp 数组的当前索引
	
	// (一)
	// 先把左右两边(有序)的数据按照规则填充到 temp 数组
	// 直到左右两边的有序序列,有一边处理完毕为止
	while(i<=mid && j<=right){// 继续
		// 如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
		// 即将左边的当前元素,填充到 temp 数组
		// 然后 t++,i++
		if(arr[i]<=arr[j]){
			temp[t]=arr[i];
			t++;
			i++;
		}else{// 反之,将右边有序序列的当前元素,填充到 temp 数组
			temp[t]=arr[j];
			t++;
			j++;
		}
	}
	
	// (二)
	// 把有剩余数据的一边的数据依次全部填充到 temp
	while(i<=mid){// 左边的有序序列还有剩余的元素,就全部填充到 temp
		temp[t]=arr[i];
		t++;
		i++;
	}
	while(j<=right){// 右边的有序序列还有剩余的元素,就全部填充到 temp
		temp[t]=arr[j];
		t++;
		j++;
	}
	
	// (三)
	// 将 temp 数组的元素拷贝到 arr
	// 注意:并不是每次都拷贝所有
	// 第一次合并:tempLeft=0,right=1,第二次合并:tempLeft=2,right=3
	// 第三次合并:tempLeft=0,right=3......
	// 最后一次合并:tempLeft=0,right=arr.length-1
	t=0;
	int tempLeft=left;
	while(tempLeft<=right){
		arr[tempLeft]=temp[t];
		t++;
		tempLeft++;
	}
}
  1. 性能
    • 8 万个数据用时 0s / 1s
    • 80 万个数据用时 1s
    • 800 万个数据用时 3s / 4s
  2. 总结

七 基数排序

  1. 基本介绍
    • 基数排序( radix sort )属于“分配式排序”( distributio sort ),又称“桶子法”( bucket sort )或 bin sort ,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
    • 基数排序法属于稳定性的排序,基数排序法是效率高的稳定性排序法
    • 基数排序( Radix Sort )是桶排序的扩展
    • 基数排序是 1887 年郝尔曼.何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较
  2. 基本思想
           将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行依次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列

代码实现

public void radixSort(int[] arr){
	// 得到数组中最大数的位数
	int max=arr[0];	// 假设第一数就是最大数
	for(int i=1;i<arr.length;i++){
		if(arr[i]>max) max=arr[i];
	}
	// 得到最大数是几位数
	int maxLength=(max+"").length();
	
	// 定义一个二维数组,表示 10 个桶,每个桶就是一个一维数组
	// 说明
	// 1.二维数组包含 10 个一维数组
	// 2.为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为 arr.length
	// 3.很明确,基数排序是使用空间换时间的经典算法
	int[][] bucket=new int[10][arr.length];
	
	// 为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入
	// 的数据个数,可以这样理解
	// 比如:bucketElementCounts[0] 记录的就是 bucket[0] 桶的放入数据个数
	int[] bucketElementCounts=new int[10];
	
	// 这里我们使用循环将代码处理,依次处理各个数的个位、十位、百位......
	for(int i=0,n=1;i<maxLength;i++,n*=10){
		// 针对每个元素的对应位进行排序处理,第一次是个位,第二次是十位,第三次是百位...
		for(int j=0;j<arr.length;j++){
			// 取出每个元素的对应位的值
			int digitOfElement=arr[j]/n % 10;
			// 放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]]=arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		int index=0;
		// 遍历每一个桶,并将桶中的数据,放入到原数组
		for(int k=0;k<bucketElementCounts.length;k++){
			// 如果桶中,有数据,我们才放入到原数组
			if(bucketElementCounts[k]!=0){
				// 循环该桶即第 k 个桶(即第 k 个一维数组),放入
				for(int l=0;l<bucketElementCounts[k];l++){
					// 取出元素放入到 arr
					arr[index++]=bucket[k][l];
				}
			}
			// 处理完后,需要将每个 bucketElementCounts[k]=0!!!!!!
			bucketElementCounts[k]=0;
		}
	}
}
  1. 性能
    • 8 万个数据用时 0s / 1s
    • 80 万个数据用时 0s
    • 800 万个数据用时 1s
    • 8000 万个数据报错 OutOfMemoryError
  2. 总结
           根据各个数据指定位数的大小将各个数据放入到合适的桶中,全部放完后,从第 0 个桶开始依次取出数据到原数组,当一个桶中有多个数据时,根据先放入先取出的原则来进行转移
           整个大的循环次数是根据数组中最大值的位数来确定的
  3. 补充
    • 基数排序是对传统桶排序的扩展,速度很快
    • 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成 OutOfMemoryError
    • 技术排序是稳定的 [注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的,否则称为不稳定的]
    • 有负数的数组,我们不用基数排序来进行排序,如果要支持负数,参考:链接

八 堆排序

  1. 基本介绍
    • 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为 O(nlogn),它也是不稳定排序
    • 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系
    • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
    • 大顶堆举例说明:
      在这里插入图片描述
    • 小顶堆举例说明:
      在这里插入图片描述
    • 一般升序采用大顶堆,降序采用小顶堆
  2. 基本思想
    • 将待排序序列构造成一个大顶堆
    • 此时,整个序列的最大值就是堆顶的根结点
    • 将其与末尾元素进行交换,此时末尾就为最大值
    • 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次大值。如此反复执行,便能得到一个有序序列了
      可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了

代码实现

public void heapSort(int[] arr){
	int temp;
	
	// 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
	for(int i=arr.length/2-1;i>=0;i--){
		adjustHeap(arr,i,arr.length);
	}
	
	// 1.将堆顶元素与末尾元素交换,将最大元素“沉”到数组末端
	// 2.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
	for(int i=arr.length-1;i>0;i--){
		// 交换
		temp=arr[i];
		arr[i]=arr[0];
		arr[0]=temp;
		adjustHeap(arr,0,i);
	}
}

// 讲一个数组(二叉树),调整成一个大顶堆
/**
 * 功能:完成将以 i 对应的非叶子结点的树调整成大顶堆
 * 举例:int[] arr={4,6,8,5,9} => i=1 => adjustHeap => 得到{4,9,8,5,6}
 * 		如果我们再次调用 adjustHeap 传入的是 i=0 => 得到{9,6,8,5,4}
 * 
 * @param arr	待调整的数组
 * @param i		表示非叶子结点在数组中索引
 * @param length 表示对多少个元素继续调整,length 是在逐渐的减少
 */
 public static void adjustHeap(int arr[],int i,int length){
 	int temp=arr[i]; // 先取出当前元素的值,保存在临时变量
 	// 开始调整
 	// 说明:k=i*2+1,k是 i 结点的左子结点
 	for(int k=i*2+1;k<length;k=k*2+1){
 		if(k+1<length && arr[k]<arr[k+1]){// 说明左子结点的值小于右子结点的值
 			k++; // k指向右子结点
 		}
 		if(arr[k]>temp){// 如果子结点大于父结点
 			arr[i]=arr[k]; // 把较大的值赋给当前结点
 			i=k; // i 指向 k ,继续进行循环比较 !!!
 		}else{
 			break; // !!!
 		}
 	}
 	// 当 for 循环结束后,我们已经将以 i 为父结点的树的最大值,放在了最顶部(局部)
 	arr[i]=temp; // 将 temp 值放到调整后的位置
 }
  1. 性能
    • 8 万个数据用时 0s / 1s
    • 80 万个数据用时 0s
    • 800 万个数据用时 4s / 5s
  2. 总结

九 常用排序算法对比

排序算法平均时间复杂度最好情况最坏情况空间复杂度排序方式稳定性
冒泡排序O(n²)O(n)O(n²)O(1)In-place稳定
选择排序O(n²)O(n²)O(n²)O(1)In-place不稳定
插入排序O(n²)O(n)O(n²)O(1)In-place稳定
希尔排序O(n log n)O(n log² n)O(n log² n)O(1)In-place不稳定
归并排序O(n log n)O(n log n)O(n log n)O(n)Out-place稳定
快速排序O(n log n)O(n log n)O(n²)O(log n)In-place不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)In-place不稳定
计数排序O(n+k)O(n+k)O(n+k)O(k)Out-place稳定
桶排序O(n+k)O(n+k)O(n²)O(n+k)Out-place稳定
基数排序O(n+k)O(n+k)O(n+k)O(n+k)Out-place稳定

相关术语解释:

  1. 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面
  2. 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面
  3. 内排序:所有排序操作都在内存中完成
  4. 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
  5. 时间复杂度:一个算法执行所耗费的时间
  6. 空间复杂度:运行完一个程序所需内存的大小
  7. n:数据规模
  8. k:“桶”的个数
  9. In-place:不占用额外内存
  10. Out-place:占用额外内存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值