数据结构与算法(九)——排序算法

排序算法

排序也称排序算法,排序是将一组数据,按照指定的顺序进行排列的过程

  1. 内部排序:将需要处理的所有数据都家长赛到内部存储器中进行排序
  2. 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序

在这里插入图片描述

冒泡排序

基本思想

通过对待排序的序列从前往后,依次比较相邻的两个元素的值,如果发现逆序则交换,使的最大(降序则最小)的元素逐渐从前移到后部,像水底的气泡逐渐向上冒;每一轮排序结束,就保证了当前无序的序列中的最大的元素已经到了序列尾部

代码实现

/**
 * 冒泡排序
 * @author laowa
 *
 */
public class BubbleSort {
	public static void main(String args[]) {
		int arr[]= {3,9,-1,10,-2};
		bubbleSort(arr);
		System.out.println("最终排序结果为");
		print(arr);
	}
	
	/**
	 * 冒泡排序
	 * @param arr 待排序的数组
	 */
	private static void bubbleSort(int[] arr) {
		//使用一个标志量来优化算法,当某趟冒泡结束后,没有发生数字的交换,说明当前任意两个相邻的数都是有序的,则该数组已有序
		boolean isSorted = true;
		//临时变量用户交换前后变量
		int temp;
		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]) {
					//当前后数据逆序,则进行交换
					//标志量改为false,表示当前趟的冒泡发生了交换
					isSorted=false;
					temp=arr[j];
					arr[j]=arr[j+1];
					arr[j+1]=temp;
				}
			}
			//如果标志量没有被改变,即序列已有序,直接返回
			if(isSorted) {
				return;
			}
			isSorted=true;
			System.out.printf("第%d轮排序的结果为\n",i+1);
			print(arr);
		}
	}
	
	/**
	 * 打印数组
	 * @param arr 待打印的数组
	 */
	private static void print(int []arr) {
		for(int i:arr) {
			System.out.print(i+" ");
		}
		System.out.println();
	}
}

速度测试

在这里插入图片描述
代码中使用了循环嵌套,冒泡的时间复杂度为O(n2),在十万级的数据量下,冒泡排序耗时高达16秒

选择排序

基本思想

选择排序也属于内部排序,它的基本思想是:在arr[n]数组中,第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换。。。进行n-1次,得到一个有序序列

选择排序每一轮排序下来,会得到无序序列中的最小值,这点和冒泡类似,但是选择排序不会每一次比较之后都进行交换,而是一轮排序只进行一次交换,所以效率要高许多

代码实现

/***
 * 选择排序
 * @author laowa
 *
 */
public class SelectSort {

	public static void main(String[] args) {
		int arr[]= {101,34,119,1};
		selectSort(arr);
		System.out.println("最终排序结果为");
		print(arr);
		timeTest();

	}
	
	/**
	 * 排序时间检查
	 */
	private static void timeTest() {
		int count = 100000;
		int arr[] = new int[count];
		for(int i=0;i<count;i++) {
			arr[i]=new Random().nextInt(count);
		}
		long before = System.currentTimeMillis();
		selectSort(arr);
		long after = System.currentTimeMillis();
		System.out.printf("排序%d个数用时%d毫秒", count,after-before);
	}

	/**
	 * 选择排序
	 * @param arr 待排数组
	 */
	private static void selectSort(int[] arr) {
		//存储当前轮找到的最小值
		int min;
		//当前轮找到的最小值的索引
		int minIndex;
		for(int i=0;i<arr.length-1;i++) {
			//初始化最小值为当前轮的第一个元素
			min = arr[i];
			minIndex = i;
			//从i+1开始,因为第i个已经取得了,依次找最小值
			for(int j=i+1;j<arr.length;j++) {
				//依次将无序列中的数与最小值比较,取最小值
				if(min>arr[j]) {
					min = arr[j];
					minIndex = j;
				}
			}
			//将最小值和当前轮的1个元素交换
			arr[minIndex]=arr[i];
			arr[i]=min;
		}
	}
	
	/**
	 * 打印数组
	 * @param arr 待打印的数组
	 */
	private static void print(int []arr) {
		for(int i:arr) {
			System.out.print(i+" ");
		}
		System.out.println();
	}
}

速度测试

在这里插入图片描述
代码使用了循环嵌套,时间复杂度为O(n2),但是选择排序每一轮排序只进行一次交换,省去了大量的交换时间,所以比冒泡排序用户要低很多,在十万级的数据下耗时约两秒

插入排序

基本思想

把n个待排序的元素看成一个有序表和一个无序表,开始时有序表只包含一个元素,无序表包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码一次与有序表中元素的排序码进行比较,将他插入到有序表中的适当位置,使之成为新的有序表

代码实现

/***
 * 插入排序
 * @author laowa
 *
 */
public class InsertSort {

	public static void main(String[] args) {
		int []arr= {101,34,119,1,-1,89};
		insertSort(arr);
		System.out.println("插入排序之后的结果为");
		print(arr);

	}
	
	/**
	 * 插入排序
	 * @param arr 待排数组
	 */
	private static void insertSort(int []arr) {
		//当前需要插入的值
		int insertVal;
		//表示插值的位置
		int insertIndex;
		//最初以第一个元素作为有序序列,所以从i=1开始遍历
		for(int i=1;i<arr.length;i++) {
			//当前遍历到的值即需要插入的值
			insertVal=arr[i];
			//从当前值的前一个位置开始,往前遍历
			insertIndex=i-1;
			
			//for循环形式
			//从i-1,即有序序列的最后一个元素开始往前遍历
			for(insertIndex=i-1;insertIndex>=0;insertIndex--) {
				//如果遍历到的元素比待插的值大,说明要插在这个元素前面,将这个元素向后移动
				if(arr[insertIndex]>insertVal) {
					arr[insertIndex+1]=arr[insertIndex];
				}else {
					//否则表示这个值要插的位置已经找到了,跳出循环,将待插值插在这个值的后面
					break;
				}
			}
			arr[insertIndex+1]=insertVal;
			
			//while循环形式
			//一直往前查找,直到找到第一个位置或者需要插入的值大于某个位置的值
			while(insertIndex>=0&&insertVal<arr[insertIndex]) {
				//将当前位置的值往后移动
				arr[insertIndex+1]=arr[insertIndex];
				//位置向前移动
				insertIndex--;
			}
			//当while循环结束后,表示都找到了当前值需要插入的位置
			//要么当前的insertIndex=-1跳出,那么将值插在0位置上
			//要么当前arr[inserIndex]<=insertVal那么将这个值插在arr[insertIndex]的后面
			arr[insertIndex+1]=insertVal;
		}
	}
	
	/**
	 * 打印数组
	 * @param arr 待打印的数组
	 */
	private static void print(int []arr) {
		for(int i:arr) {
			System.out.print(i+" ");
		}
		System.out.println();
	}

}

速度测试

在这里插入图片描述
插入排序也是双重循环,时间复杂度为O(n2),因为每次内层循环都会有多次赋值操作,相对于冒泡的交换要少,但相对于选择的一个循环一次要多,十万级的数量用户约四秒

希尔排序

插入排序的缺点分析

假设这样一个数组arr={2,3,4,5,6,1},这里经过4四轮排序之后轮到了1(最小),那么接下来这轮排序过程是:

  1. {2,3,4,5,6,6}
  2. {2,3,4,5,5,6}
  3. {2,3,4,4,5,6}
  4. {2,3,3,4,5,6}
  5. {2,2,3,4,5,6}
  6. {1,2,3,4,5,6}

因为当前要插入的数是最小的一个数,所以在有序列从后往前遍历的时候会将有序列所有元素都后移一次,影响效率(当需要插入的数是较小的数的时候,后移次数明显增多,对效率有影响

希尔排序介绍

希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也成为缩小增量排序

基本思想

将记录按照下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法终止
在这里插入图片描述

代码实现

/***
 * 希尔排序
 * @author laowa
 *
 */
public class ShellSort {

	public static void main(String[] args) {
		int[] arr= {8,9,1,7,2,3,5,4,6,0};
		shellSort(arr);
		print(arr);
//		timeTest();
	}
	
	/**
	 * 排序时间检查
	 */
	private static void timeTest() {
		int count = 10000000;
		int arr[] = new int[count];
		for(int i=0;i<count;i++) {
			arr[i]=new Random().nextInt(count);
		}
		long before = System.currentTimeMillis();
		shellSort(arr);
		long after = System.currentTimeMillis();
		System.out.printf("排序%d个数用时%d毫秒", count,after-before);
	}
	
	/**
	 * 希尔排序
	 * @param arr 待排数组
	 */
	private static void shellSort(int []arr) {
		int step=arr.length/2;//表示当前的增量步长,初始化为length/2
		int temp;//临时变量,在交换法中,用于辅助两个值的交换;在移位法中,用于保存当前需要插入的值
		//开始排序,直到步长小于1退出
		while(step>=1) {
			//从步长的位置开始,因为将第一位看作了有序序列,遍历每一组后面的无序序列
			for(int i=step;i<arr.length;i++) {
				//交换法,交换法中,待插的值会在有序序列中从后到前不断的于逆序的数交换,每次交换有三次赋值操作
				//从有序序列的最后一位开始往前遍历,将无序序列的第一位插入到有序序列中;第一次循环时,arr[j+step]就是arr[i]即无序序列的第一位
				for(int j=i-step;j>=0;j-=step) {
					//当前位比后一位大,则两个数交换位置
					//arr[i]位置待插入的数,会经过不断的交换,一直到前一个数比它小或序列第一
					if(arr[j]>arr[j+step]) {
						temp = arr[j+step];
						arr[j+step]=arr[j];
						arr[j]=temp;
					}else {
						//这里一定要加上break,表示如果当前的值比后面值小则不在向前交换,因为待插值已经确定好位置了,前面的序列都是有序的,不用遍历了
						break;
					}
				}
//				//移位法,移位法中,出现逆序只会将当前数往后移动一位,每次移位只有一次赋值操作
//				int j=i-step;
//				temp = arr[i];//将待插值存入临时变量中
//				//从最后一位往前遍历,一直到当前位小于待插值或已经到了序列头部
//				while(j>=0&&arr[j]>temp) {
//					//将当前位往后移动一位
//					arr[j+step]=arr[j];
//					//当前位前移
//					j-=step;
//				}
//				//循环结束后,当前位置就是待插值的前一位,将待插值插在后一位即可
//				arr[j+step]=temp;
			}
			step=step/2;
		}
	}
	
	/**
	 * 打印数组
	 * @param arr 待打印的数组
	 */
	private static void print(int []arr) {
		for(int i:arr) {
			System.out.print(i+" ");
		}
		System.out.println();
	}

}

时间测试

在这里插入图片描述
希尔排序在十万级数据用时仅29毫秒,在千万级数据用时也只有3秒左右

快速排序

基本思想

通过一趟排序,将要排的数据分割成独立的两部分,其中一部分数据都比另外一部分的所有数据都小,然后再对每一部分按照相同的规则进行排序,直到每一部分元素的个数为1 ;即每一趟排序下来,指定数左边的部分都比该数小,右边的部分都比该数大

代码实现

/***
 * 快速排序
 * @author laowa
 *
 */
public class QuickSort {

	public static void main(String[] args) {
//		int []arr= {-9,78,0,23,-567,70,70,70,70,70,70,70,780};
//		quickSort(arr,0,arr.length-1);
//		print(arr);
		timeTest();
	}
	
	/**
	 * 排序时间检查
	 */
	private static void timeTest() {
		int count = 10000000;
		int arr[] = new int[count];
		for(int i=0;i<count;i++) {
			arr[i]=new Random().nextInt(count);
		}
		long before = System.currentTimeMillis();
		quickSort(arr,0,arr.length-1);
		long after = System.currentTimeMillis();
		System.out.printf("排序%d个数用时%d毫秒", count,after-before);
	}
	
	/**
	 * 快速排序
	 * @param arr 待排序列
	 */
	private static void quickSort(int []arr,int left,int right) {
		//创建变量l存储当前需要进行排序的序列最左
		int l = left;
		//创建变量r存储当前需要排序的序列最右
		int r = right;
		//临时变量,用于两个数进行交换
		int temp;
		//支点数,每一次递归都会根据这个支点元素,让小于它的在他左边,大于他的在他右边
		int pivot = arr[(left+right)/2];
		//当左边的指针超过了右边的指针,表示左边没有了比右边大的数
		while(l<r) {
			//从left开始找,一直到找到和支点相等或大于支点的数
			while(arr[l]<pivot) {
				l++;
			}
			//从right向左找,一直找到和支点相等或小于支点的数
			while(arr[r]>pivot) {
				r--;
			}
			//如果此时左边的指针已经和右边指针相同(同时指向了支点),或者左边的指针超过了右边,表示已经达到目的,直接break
			if(l>=r) {
				break;
			}
			//将l和r指向的元素进行交换,如果其中有一个是支点元素,则表示支点元素某一边存在了与支点逆序的数,将支点与该数交换
			temp = arr[l];
			arr[l]=arr[r];
			arr[r]=temp;
			//1.将当前满足条件的值省去可以省去同一值的重复比较
			//2.出现重复的数据的时候,不进行移位会导致arr[l]==arr[r]==pivot造成死循环
			//如果左指针指向的数已经满足条件,则指向下一位,(如果当前指向的值和支点相等,他和支点的前后顺序是无所谓的,所以也可以当作满足条件)
			if(arr[l]<=pivot) {
				l++;
			}
			//如果右指针指向的数已经满足条件,则指向下一位,(如果当前指向的值和支点相等,他和支点的前后顺序是无所谓的,所以也可以当作满足条件)
			if(arr[r]>=pivot) {
				r--;
			}
		}
		//如果左右指针相等,则两者都往远处移动一位,放置下一次递归重复,造成栈溢出
		if(l==r) {
			l++;
			r--;
		}
		//如果left>=r说明左递归已经到最左边了,则不用继续递归
		if(left<r) {
			quickSort(arr,left,r);
		}
		//如果right<=l说明右递归已经到最右边了,则不用继续递归
		if(right>l) {
			quickSort(arr,l,right);
		}
	}
	
	/**
	 * 打印数组
	 * @param arr 待打印的数组
	 */
	private static void print(int []arr) {
		for(int i:arr) {
			System.out.print(i+" ");
		}
		System.out.println();
	}

}

速度测试

在这里插入图片描述
千万级的数据在快速排序下只用到了不到2秒

归并排序

基本思想

归并排序采取了分治策略,先将序列分割成最小的子序列,然后子序列之间按照顺序进行合并,最后合并成有序序列
在这里插入图片描述

代码实现

/***
 * 归并排序
 * @author laowa
 *
 */
public class MergeSort {

	public static void main(String[] args) {
//		int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 };
//		int temp[] = new int[arr.length];
//		mergeSort(arr,0,arr.length-1,temp);
//		print(arr);
		timeTest();
	}
	
	/**
	 * 排序时间检查
	 */
	private static void timeTest() {
		int count = 10000000;
		int arr[] = new int[count];
		for(int i=0;i<count;i++) {
			arr[i]=new Random().nextInt(count);
		}
		long before = System.currentTimeMillis();
		mergeSort(arr,0,arr.length-1,new int[arr.length]);
		long after = System.currentTimeMillis();
		System.out.printf("排序%d个数用时%d毫秒", count,after-before);
	}

	/**
	 * 归并排序,将数组进行分解与合并
	 * @param arr 待排序数组
	 * @param left 待分解部分的最左边索引
	 * @param right 待分解部分的最右边索引
	 * @param temp 辅助合并的数组
	 */
	private static void mergeSort(int[] arr,int left,int right,int []temp) {
		//当最左边小于最右边的时候进行分解与合并
		//当递归到最左边或最右边只有一个元素时,不能在拆分了,此时left==right不会再进行分解与合并
		if(left<right) {
			//取当前待分解部分的中间,将它分开
			int mid=(left+right)/2;
			//取左边~中间向左递归分解
			mergeSort(arr,left,mid,temp);
			//取中间~右边向右递归分解
			mergeSort(arr,mid+1,right,temp);
			//将当前部分合并
			//合并的执行是在递归分解的下面,所以合并会在所有递归分解结束后进行,最后只剩一个数的时候方法不会继续递归,然后从栈顶往栈底依次从2,4...个元素开始合并
			merge(arr,left,right,mid,temp);
		}
	}

	/**
	 * 合并,left~mid是分解后的左边部分,mid~right是分解后的右边部分,这里就是依次从左右两边按顺序取数放到temp中
	 * 
	 * @param arr
	 *            待排数组
	 * @param left
	 *            左边有序序列的初始值,左边部分的第一个位置
	 * @param right
	 *            最右边索引
	 * @param mid
	 *            中间索引,左边部分最后一个位置
	 * @param temp
	 *            辅助合并数组
	 */
	private static void merge(int[] arr, int left, int right, int mid, int[] temp) {
		//左边序列第一个数索引
		int i = left;
		//右边序列第一个数索引
		int j = mid+1;
		//辅助数组当前待插部分索引
		int t = 0;
		//当左边元素取完或者右边元素取完,表示比较结束,左边和右边的序列都是有序的,剩余部分直接放入temp中即可
		while (i <= mid && j <= right) {
			//如果左边更小,则将左边数插入temp中,左边索引后移
			if (arr[i] < arr[j]) {
				temp[t] = arr[i];
				i++;
			} else {
				//否则将右边数插入temp中,右边索引后移
				temp[t] = arr[j];
				j++;
			}
			t++;
		}
		//将左边剩余元素插入temp中
		while (i <= mid) {
			temp[t] = arr[i];
			i++;
			t++;
		}
		//将右边剩余元素插入temp中
		while (j <= right) {
			temp[t] = arr[j];
			j++;
			t++;
		}

		//将当前合并部分复制到原始数组中,原始数组该部分九有序了
		t = 0;//指向temp数组
		int tempLeft = left;//指向原始数组left~right就是当前部分在原始数组中的片段
		while (tempLeft <= right) {
			arr[tempLeft] = temp[t];
			tempLeft++;
			t++;
		}
	}

	/**
	 * 打印数组
	 * 
	 * @param arr
	 *            待打印的数组
	 */
	private static void print(int[] arr) {
		for (int i : arr) {
			System.out.print(i + " ");
		}
		System.out.println();
	}

}

速度测试

在这里插入图片描述
归并排序在千万级数据量下,时间在两秒内

基数排序

基本介绍

  1. 基数排序属于“分配式排序”,又称“桶子法”,它是通过键值的各个位的值,将要排序的元素分配至某些桶中,达到排序的作用
  2. 基数排序属于稳定型的排序,且效率高,但耗费空间大
  3. 基数排序是桶排序的扩展

基本思想

将所有待比较数值统一位同样长度,数位较短的位置数字补0,然后从最低位开始,依次进行依次排序,从最低位到最高位排序完成后,就变成一个有序序列

代码实现

/***
 * 基数排序
 * 
 * @author laowa
 *
 */
public class RadixSort {

	public static void main(String[] args) {
//		int arr[] = { 53, 3, 542, 748, 14, 214 };
//		radixSort(arr);
//		print(arr);
		timeTest();

	}
	
	/**
	 * 排序时间检查
	 */
	private static void timeTest() {
		int count = 10000000;
		int arr[] = new int[count];
		for(int i=0;i<count;i++) {
			arr[i]=new Random().nextInt(count);
		}
		long before = System.currentTimeMillis();
		radixSort(arr);
		long after = System.currentTimeMillis();
		System.out.printf("排序%d个数用时%d毫秒", count,after-before);
	}

	private static void radixSort(int arr[]) {
		// 使用一个二维数组来模拟十个桶,二维数组的行号是每个桶的编号,二维数组的列是用来存储每一轮放入同种元素的一维数组
		int[][] bucket = new int[10][arr.length];
		//用一个一维数组记录当前轮次的排序,存入到每个桶中的数据数量
		//这个一维数组的下标表示二维数组bucket的行
		int[] bucketElementCount = new int[10];
		//开始遍历位数
		for (int i = 1; i <= maxLength(arr); i*=10) {
			// 开始遍历数组
			for (int j = 0; j < arr.length; j++) {
				// 获得当前位的数字
				int digitOfElement = arr[j]/i % 10;
				//将当前数字放在对应的digitOfElement的桶中,根据bucketElementCount来获得当前应该放的位置
				//因为bucketElementCount存的是目标桶存放的个数,所以bucketElementCount存的值就可以当作当前需要插入的下标来使用
				//例如,bucket[0]中存了一个元素,那么bucketElementCount[0]=1;接下来下一个元素要存的位置也正是bucket[0][1]
				bucket[digitOfElement][bucketElementCount[digitOfElement]] = arr[j];
				//存放之后,将当前桶数据个数+1
				bucketElementCount[digitOfElement]++;
			}
			
			//将桶中的数据复制到原数组中
			int index=0;
			//外层循环遍历存储每个同种数字个数的数组,将每个桶中的数据依次复制给原序列
			for(int k=0;k<bucketElementCount.length;k++) {
				//从0到当前轮次当前桶的数量个数,进行循环,将这一轮放入这个桶的数据复制到序列
				for(int l=0;l<bucketElementCount[k];l++) {
					arr[index]=bucket[k][l];
					index++;
				}
				//复制结束后,将个数清零,为下一轮排序作准备
				//桶中的数据是脏的,获取桶中的有效数据是通过bucketElementCount来进行的
				bucketElementCount[k]=0;
			}
			
		}
		
		

	}

	/**
	 * 获取最长数字数量级
	 * @param arr
	 * @return
	 */
	private static double maxLength(int arr[]) {
		int res = 0;
		int current;
		for (int i : arr) {
			current = String.valueOf(i).length();
			res = res > current ? res : current;
		}
		return Math.pow(10, res);
	}
	
	/**
	 * 打印数组
	 * @param arr 待打印的数组
	 */
	private static void print(int []arr) {
		for(int i:arr) {
			System.out.print(i+" ");
		}
		System.out.println();
	}

}

速度测试

在这里插入图片描述
排序千万级数据用时约6秒,但是花费了极大量的内存

排序算法对比

在这里插入图片描述

  1. 稳定性:对于两个大小相同的数a,b如果排序前后两者相对位置不变,则表示稳定
  2. 内排序与外排序:所有操作都在内存中进行,为内排序;由于数据量过大,部分在内存中,部分在磁盘中进行
  3. 时间复杂度:一个算法执行所耗费的时间
  4. n:数据规模
  5. In-place:不占用额外内存
  6. Out-place:占用额外内存
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值