第六章 排序算法

本章源码:https://github.com/name365/Java-Data-structure

排序算法介绍和分类

排序也称排序算法 (Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。

  • 排序的分类:

    • 内部排序:指将需要处理的所有数据都加载
      到内部存储器中进行排序。
    • 外部排序:数据量过大,无法全部加载到内
      存中,需要借助外部存储进行排序。
  • 常见的排序算法分类(如图):

算法的时间复杂度与空间复杂度

时间复杂度

度量一个程序(算法)执行时间的两种方法

  1. 事后统计的方法
    这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快

  2. 事前估算的方法
    通过分析某个算法的时间复杂度来判断哪个算法更优.

  • 时间频度

时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。[举例说明如下]

  • 举例说明-基本案例
public class Sort {

	//比如计算1-100所有数字之和, 设计如下两种算法:
	public static void main(String[] args) {
		//方法一:
		int total = 0;
		int end = 100;
		for(int i = 1;i <= end;i++){
			total += i;
		}
		System.out.println("total = " + total);	//T(n)=n+1;
		
		//方法二:
		int total2 = 0;
		int end2 = 100;
		total2 = (1 + end2)*end2/2;
		System.out.println("total2 = " + total2); //T(n)=1
	}
}
  • 举例说明-忽略常数项
  T(n)=2n+20 T(n)=2*n T(n)=(3n+10) T(n)=(3n)
1 22 2 13 3
2 24 4 16 6
5 30 10 25 15
8 36 16 34 24
15 50 30 55 45
30 80 60 100 90
100 220 200 310 300
300 620 600 910 900

结论: 
1. 2n+202n 随着n 变大,执行曲线无限接近, 20可以忽略
2. 3n+103n 随着n 变大,执行曲线无限接近, 10可以忽略
  • 举例说明-忽略低次项
  T(n)=2n^2+3n+10 T(n)=(2n^2) T(n)=(n^2+5n+20) T(n)=(n^2)
1 15 2 26 1
2 24 8 34 4
5 75 50 70 25
8 162 128 124 64
15 505 450 320 225
30 1900 1800 1070 900
100 20310 20000 10520 10000

结论: 
1. 2n^2+3n+102n^2 随着n 变大, 执行曲线无限接近, 可以忽略 3n+10
2. n^2+5n+20 和 n^2 随着n 变大,执行曲线无限接近, 可以忽略 5n+20
  • 举例说明-忽略系数
  T(n)=(3n^2+2n) T(n)=(5n^2+7n) T(n)=(n^3+5n) T(n)=(6n^3+4n)
1 5 12 6 10
2 16 34 18 56
5 85 160 150 770
8 208 376 552 3104
15 705 1230 3450 20310
30 2760 4710 27150 162120
100 30200 50700 1000500 6000400

结论: 
1. 随着n值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明  这种情况下, 53可以忽略。
2. 而n^3+5n 和 6n^3+4n  ,执行曲线分离,说明多少次方式关键
  1. 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。

  2. T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。

  3. 计算时间复杂度的方法:

  • 用常数1代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1

  • 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²

  • 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)

常见的时间复杂度

1)常数阶O(1)

2)对数阶O(log2n)

3)线性阶O(n)

4)线性对数阶O(nlog2n)

5)平方阶O(n^2)

6)立方阶O(n^3)

7)k次方阶O(n^k)

8)指数阶O(2^n)
  • 说明
  • 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

  • 从图中可见,我们应该尽可能避免使用指数阶的算法。
  • 常数阶O(1)

无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1) 。

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

  • 对数阶O(log2n)

说明:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n) 。

  • 线性阶O(n)

说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。

  • 线性对数阶O(nlogN)

说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。

  • 平方阶O(n²)

说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n * n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m * n)。

  • 立方阶O(n³)K次方阶O(n^k)

说明:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。

  • 平均时间复杂度和最坏时间复杂度

1)平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。

2)最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。

3)平均时间复杂度和最坏时间复杂度是否一致,和算法有关(如下表)。

排序法 平均时间 最差情形 稳定度 额外空间 备注
冒泡 O(n2) O(n2) 稳定 O(1) n小时较好
交换 O(n2) O(n2) 不稳定 O(1) n小时较好
选择 O(n2) O(n2) 不稳定 O(1) n小时较好
插入 O(n2) O(n2) 稳定 O(1) 大部分已排序时较好
基数 O(logRB) O(logRB 稳定 O(n) B是真数(0-9),R是基数(个十百)
Shell O(nlogn) O(ns)1<s<2 不稳定 O(1) s是所选分组
快速 O(nlogn) O(n2) 不稳定 O(nlogn) n大时较好
归并 O(nlogn) O(nlogn) 稳定 O(1) n大时较好
O(nlogn) O(nlogn) 不稳定 O(1) n大时较好

空间复杂度

  • 基本简介

1)类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。

2)空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。

3)在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。

冒泡排序

基本介绍

冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。

优化:
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,
因此要在排序过程中设置一个标志flag判断元素是否进行过交换。
从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)

图解冒泡排序算法的过程

具体的案例如下来说明冒泡排序。将五个无序的数:3, 9, -1, 10, -2使用冒泡排序法将其排成一个的有序数列。






冒泡排序规则:
(1) 一共进行 数组的大小-1 次 大的循环
(2) 每一趟排序的次数在逐渐的减少
(3) 如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。这就是算法的优化。

源自网络的冒泡排序动图(仅供参考理解):

排序过程代码实现

import java.util.Arrays;

public class BubbleSort {

	public static void main(String[] args) {
		int arr[] = {3, 9, -1, 10, -2};
		
		//第一趟排序,将最大的数排在最后
		int temp = 0;	//创建临时变量,用于数据交换
		for(int i = 0;i < arr.length - 1;i++){
			//如果, 前一个数 > 后一个数
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第一趟排序后:");
		System.out.println(Arrays.toString(arr));
		
		//第二趟排序,将第二大的数排在倒数第二位
		for(int i = 0;i < arr.length - 1 - 1;i++){
			//如果, 前一个数 > 后一个数
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第二趟排序后:");
		System.out.println(Arrays.toString(arr));
		
		//第三趟排序,将第三大的数排在倒数第三位
		for(int i = 0;i < arr.length - 1 - 2;i++){
			//如果, 前一个数 > 后一个数
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第三趟排序后:");
		System.out.println(Arrays.toString(arr));
		
		//第四趟排序,将第四大的数排在倒数第四位
		for(int i = 0;i < arr.length - 1 - 3;i++){
			//如果, 前一个数 > 后一个数
			if(arr[i] > arr[i+1]){
				temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
		System.out.print("第四趟排序后:");
		System.out.println(Arrays.toString(arr));
	}
}
  • 对前面的代码进行优化:
import java.util.Arrays;

public class BubbleSort {

	public static void main(String[] args) {
		int arr[] = { 3, 9, -1, 10, -2 };

		// 第一趟排序,将最大的数排在最后
		// 冒泡排序 的时间复杂度 O(n^2)
		int temp = 0; // 创建临时变量,用于数据交换
		for (int j = 0; j < arr.length - 1; j++) {
			for (int i = 0; i < arr.length - 1 - j; i++) {
				// 如果, 前一个数 > 后一个数
				if (arr[i] > arr[i + 1]) {
					temp = arr[i];
					arr[i] = arr[i + 1];
					arr[i + 1] = temp;
				}
			}
			System.out.print("第" + (j + 1) + "趟排序后:");
			System.out.println(Arrays.toString(arr));
		}
    }
}
  • 对前面的代码进行进一步优化:
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class BubbleSort2 {

	public static void main(String[] args) {
		int arr[] = { 3, 9, -1, 10, 20 };
		
		System.out.print("排序前的数组:");
		System.out.println(Arrays.toString(arr));
		
		//测试一下冒泡排序的速度:O(n^2),给80000个的数据,测试
// int[] arr = new int[80000];
// for(int i = 0;i < 80000;i++){
// arr[i] = (int)(Math.random() * 80000); //自动生成[0,80000)之间的随机数
// }
// System.out.print("排序前的数组:");
// System.out.println(Arrays.toString(arr));
		
		//排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);

		//测试冒泡排序:
		bubbleSort(arr);
		
		//输出排序后的数组
		System.out.print("排序后的数组:");
		System.out.println(Arrays.toString(arr));
		
		//排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);

	}

	// 将前面的冒泡排序封装成一个方法
	public static void bubbleSort(int[] arr) {
		// 冒泡排序 的时间复杂度 O(n^2)
		int temp = 0; // 创建临时变量,用于数据交换
		boolean falg = false; // 标识变量,表示是否进行过交换
		for (int j = 0; j < arr.length - 1; j++) {
			for (int i = 0; i < arr.length - 1 - j; i++) {
				// 如果, 前一个数 > 后一个数
				if (arr[i] > arr[i + 1]) {
					falg = true;
					temp = arr[i];
					arr[i] = arr[i + 1];
					arr[i + 1] = temp;
				}
			}

			if (falg == false) { // 在一趟排序中,一次交换都没有,直接退出
				break;
			} else {
				falg = false; // 重置falg,方便进行二次判断
			}
		}
	}
}

选择排序

基本介绍

选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

  • 选择排序思想:
选择排序(select sorting)也是一种简单的排序方法。
它的基本思想是:
	第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换;
	第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换;
	第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换;
	…
    第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换;
	…
    第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换,
	总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

选择排序思路分析图:


选择排序的思路图解

说明:
1. 选择排序一共有 数组大小 - 1 轮排序
2.1轮排序,又是一个循环, 循环的规则(见下面代码)
	2.1 先假定当前这个数是最小数
	2.2 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
	2.3 当遍历到数组的最后时,就得到本轮最小数和下标
	2.4 交换 [见下面代码]

选择排序应用实例

有一群牛 , 颜值分别是 101, 34, 119, 1 请使用选择排序从低到高进行排序[101, 34, 119, 1]

算法规律推导过程代码:

import java.util.Arrays;

public class SelectSort {

	public static void main(String[] args) {
		int[] arr = {101, 34, 119, 1};
		System.out.print("排序前:");
		System.out.println(Arrays.toString(arr));
		
		selectSort(arr);	//调用排序
	}

	// 选择排序
	public static void selectSort(int[] arr) {		
		// 逐步推导过程
		// 第1轮
		// 原始数组: 101, 34, 119, 1
		// 第一轮排序: 1, 34, 119, 101
		// 算法思维:先简单--》再复杂,即复杂的算法拆分为简单的问题,再逐步解决
		
		// 第一轮
		int minIndex = 0;
		int min = arr[0];	//假设某个为最小值
		for(int j = 0 + 1;j < arr.length;j++){
			if(min > arr[j]){	//说明假定的最小值,并不是最小
				min = arr[j];	//重置最小值
				minIndex = j;	//重置minIndex
			}
		}
		
		//将最小值放在arr[0],即交换
		if(minIndex != 0){
			arr[minIndex] = arr[0];
			arr[0] = min;
		}
		
		System.out.print("第一轮后:");
		System.out.println(Arrays.toString(arr));	//第一轮后:[1, 34, 119, 101]
		
		//第二轮
		minIndex = 1;
		min = arr[1];	//假设某个为最小值
		for(int j = 1 + 1;j < arr.length;j++){
			if(min > arr[j]){	//说明假定的最小值,并不是最小
				min = arr[j];	//重置最小值
				minIndex = j;	//重置minIndex
			}
		}
		
		//将最小值放在arr[1],即交换
		if(minIndex != 1){
			arr[minIndex] = arr[1];
			arr[1] = min;
		}
		
		System.out.print("第二轮后:");
		System.out.println(Arrays.toString(arr));	//[1, 34, 119, 101]
		
		//第三轮
		minIndex = 2;
		min = arr[2];	//假设某个为最小值
		for(int j = 1 + 2;j < arr.length;j++){
			if(min > arr[j]){	//说明假定的最小值,并不是最小
				min = arr[j];	//重置最小值
				minIndex = j;	//重置minIndex
			}
		}
		
		//将最小值放在arr[2],即交换
		if(minIndex != 2){
			arr[minIndex] = arr[2];
			arr[2] = min;
		}
		
		System.out.print("第三轮后:");
		System.out.println(Arrays.toString(arr));	//[1, 34, 101, 119]
	}

}

推导过程的代码优化:

import java.util.Arrays;

public class SelectSort {

	public static void main(String[] args) {
		int[] arr = { 101, 34, 119, 1 };
		System.out.print("排序前:");
		System.out.println(Arrays.toString(arr));

		selectSort(arr); // 调用排序
		
		System.out.print("排序后:");
		System.out.println(Arrays.toString(arr));
	}

	// 选择排序
	public static void selectSort(int[] arr) {

		// 在推导过程中,通过规律直接循环解决
		// 选择排序的时间复杂度O(n^2)
		for (int i = 0; i < arr.length - 1; i++) {
			int minIndex = i;
			int min = arr[i]; // 假设某个为最小值
			for (int j = i + 1; j < arr.length; j++) {
				if (min > arr[j]) { // 说明假定的最小值,并不是最小
					min = arr[j]; // 重置最小值
					minIndex = j; // 重置minIndex
				}
			}

			// 将最小值放在arr[i],即交换
			if (minIndex != i) {
				arr[minIndex] = arr[i];
				arr[i] = min;
			}

			System.out.print("第" + (i+1) + "轮后:");
			System.out.println(Arrays.toString(arr));
		}
    }
}

选择排序算法速度测试:

import java.text.SimpleDateFormat;
import java.util.Date;
//选择排序算法速度测试
public class SelectSort2 {

	public static void main(String[] args) {
		//测试一下冒泡排序的速度:O(n^2),给80000个的数据,测试
		int[] arr = new int[80000];
		for(int i = 0;i < 80000;i++){
			arr[i] = (int)(Math.random() * 80000);	//自动生成[0,80000)之间的随机数
		}
		
		//排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);

		selectSort(arr); // 调用排序
		
		//排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);
	}

	// 选择排序
	public static void selectSort(int[] arr) {

		// 在推导过程中,通过规律直接循环解决
		// 选择排序的时间复杂度O(n^2)
		for (int i = 0; i < arr.length - 1; i++) {
			int minIndex = i;
			int min = arr[i]; // 假设某个为最小值
			for (int j = i + 1; j < arr.length; j++) {
				if (min > arr[j]) { // 说明假定的最小值,并不是最小
					min = arr[j]; // 重置最小值
					minIndex = j; // 重置minIndex
				}
			}

			// 将最小值放在arr[i],即交换
			if (minIndex != i) {
				arr[minIndex] = arr[i];
				arr[i] = min;
			}

		}
	}
}

插入排序

基本介绍

插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

插入排序法思想

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

插入排序思路图


插入排序应用实例

有一群小牛,考试成绩分别是101,34,119,1请从小到大排序

算法规律推导过程代码:

import java.util.Arrays;

public class InsertSort {

	public static void main(String[] args) {
		int[] arr = {101, 34, 119, 1};
		
		inserSort(arr);
		
	}
	//插入排序
	public static void inserSort(int[] arr){
		//使用逐步推到的过程
		
		//第1轮{101, 34, 119, 1}; =》 {34, 101, 119, 1};
		
		//定义待插入的数
		int insertVal = arr[1];
		int insertIndex = 1 - 1;	//即arr[1]的前面这个数的下标
		
		//给insertVal 找到插入位置
		//说明:
		//1.insertVal >= 0 保证在找到相应位置时,不会越界
		//2.insertVal < arr[insertIndex] 待插入的数未找到合适位置
		//3.就需要将 arr[insertIndex] 后移
		while(insertIndex >= 0 && insertVal < arr[insertIndex]){
			arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
			insertIndex--;
		}
		//当退出while循环时,说明找到要插入的位置, insertIndex + 1
		arr[insertIndex + 1] = insertVal;
		
		System.out.print("第1轮插入:");
		System.out.println(Arrays.toString(arr));
		
		// 第2轮
		insertVal = arr[2];
		insertIndex = 2 - 1;	//即arr[2]的前面这个数的下标
		
		while(insertIndex >= 0 && insertVal < arr[insertIndex]){
			arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
			insertIndex--;
		}
		//当退出while循环时,说明找到要插入的位置, insertIndex + 1
		arr[insertIndex + 1] = insertVal;
		
		System.out.print("第2轮插入:");
		System.out.println(Arrays.toString(arr));
		
		//第3轮
		insertVal = arr[3];
		insertIndex = 3 - 1;	//即arr[3]的前面这个数的下标
		
		while(insertIndex >= 0 && insertVal < arr[insertIndex]){
			arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
			insertIndex--;
		}
		//当退出while循环时,说明找到要插入的位置, insertIndex + 1
		arr[insertIndex + 1] = insertVal;
		
		System.out.print("第3轮插入:");
		System.out.println(Arrays.toString(arr));
	}
}

推导过程的代码优化:

import java.util.Arrays;

public class InsertSort {

	public static void main(String[] args) {
		int[] arr = {101, 34, 119, 1,-1,89,25};
		
		inserSort(arr);
		
	}
	//插入排序
	public static void inserSort(int[] arr){
		
		//代码的简化
				
		for(int i = 1;i < arr.length;i++){
			int insertVal = arr[i];
			int insertIndex = i - 1;	//即arr[i]的前面这个数的下标
			//给insertVal 找到插入位置
			//说明:
			//1.insertVal >= 0 保证在找到相应位置时,不会越界
			//2.insertVal < arr[insertIndex] 待插入的数未找到合适位置
			//3.就需要将 arr[insertIndex] 后移
			while(insertIndex >= 0 && insertVal < arr[insertIndex]){
				arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
				insertIndex--;
			}
			//当退出while循环时,说明找到要插入的位置, insertIndex + 1
			arr[insertIndex + 1] = insertVal;
			
			System.out.print("第"+ i +"轮插入:");
			System.out.println(Arrays.toString(arr));
		}
    }
}

插入排序算法速度测试:

import java.text.SimpleDateFormat;
import java.util.Date;
//时间测试
public class InsertSort2 {

	public static void main(String[] args) {
		//测试一下插入排序的速度:O(n^2),给80000个的数据,测试
		int[] arr = new int[80000];
		for(int i = 0;i < 80000;i++){
			arr[i] = (int)(Math.random() * 80000);	//自动生成[0,80000)之间的随机数
		}
		
		//排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);	//2020-05-29 11:31:53
		
		inserSort(arr);	//调用插入排序
		
		//排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);	//2020-05-29 11:31:54 
	}
	//插入排序
	public static void inserSort(int[] arr){
		int insertVal = 0;
		int insertIndex = 0;
		for(int i = 1;i < arr.length;i++){
			insertVal = arr[i];
			insertIndex = i - 1;	//即arr[i]的前面这个数的下标
			//给insertVal 找到插入位置
			//说明:
			//1.insertVal >= 0 保证在找到相应位置时,不会越界
			//2.insertVal < arr[insertIndex] 待插入的数未找到合适位置
			//3.就需要将 arr[insertIndex] 后移
			while(insertIndex >= 0 && insertVal < arr[insertIndex]){	//从小到大排序
// while(insertIndex >= 0 && insertVal > arr[insertIndex]){ //从大到小排序
				arr[insertIndex + 1] = arr[insertIndex];	// arr[insertIndex]
				insertIndex--;
			}
			//当退出while循环时,说明找到要插入的位置, insertIndex + 1
			//算法的优化:判断是否需要赋值
			if(insertVal + 1 == i){	//满足则没必要执行
				arr[insertIndex + 1] = insertVal;
			}
		}
	}
}

希尔排序

基本介绍

针对于上述简单插入排序,它存在一些问题:

我们看简单的插入排序可能存在的问题. 
数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小), 这样的过程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
针对这个问题,引入另一种方法:    

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。

  • 希尔排序法基本思想
希尔排序是把记录按下标的一定增量分组,
对每组使用直接插入排序算法排序;
随着增量逐渐减少,每组包含的关键词越来越多,
当增量减至1时,整个文件恰被分成一组,算法便终止。
  • 希尔排序法的示意图


源自网络的图片:

希尔排序应用实例

有一群小牛, 考试成绩分别是 {8,9,1,7,2,3,5,4,6,0} 请从小到大排序. 请分别使用

1)希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度.

2)希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度.

算法规律推导过程代码:

import java.util.Arrays;

public class ShellSort {

	public static void main(String[] args) {
		int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
		
		shellSort(arr);	//调用排序
	}

	// 逐步推导的方式——希尔排序
	public static void shellSort(int[] arr) {
		int temp = 0;
		// 第1轮排序: 将10个数据分为5组
		for (int i = 5; i < arr.length; i++) {
			// 遍历各组中所有的元素(共5组,每组两个元素),步长是5
			for (int j = i - 5; j >= 0; j -= 5) {
				// 如果当前的元素大于加上步长后的元素,即交换
				if (arr[j] > arr[j + 5]) {
					temp = arr[j];
					arr[j] = arr[j + 5];
					arr[j + 5] = temp;
				}
			}
		}
		System.out.println("第1轮排序:" + Arrays.toString(arr));
		
		//第2轮排序,将10个数据分成了 5/2 = 2组
		for (int i = 2; i < arr.length; i++) {
			// 遍历各组中所有的元素(共2组,每组5个元素),步长是2
			for (int j = i - 2; j >= 0; j -= 2) {
				// 如果当前的元素大于加上步长后的元素,即交换
				if (arr[j] > arr[j + 2]) {
					temp = arr[j];
					arr[j] = arr[j + 2];
					arr[j + 2] = temp;
				}
			}
		}
		System.out.println("第2轮排序:" + Arrays.toString(arr));
		
		//第3轮排序,将10个数据分成了 2/2 = 1组
		for (int i = 1; i < arr.length; i++) {
			// 遍历各组中所有的元素(共1组,每组10个元素),步长是1
			for (int j = i - 1; j >= 0; j -= 1) {
				// 如果当前的元素大于加上步长后的元素,即交换
				if (arr[j] > arr[j + 1]) {
					temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}
		}
		System.out.println("第3轮排序:" + Arrays.toString(arr));
		
	}
}

推导过程的代码优化(希尔排序[交换式]):

import java.util.Arrays;

public class ShellSort {

	public static void main(String[] args) {
		int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
		
		shellSort(arr);	//调用排序
	}

	// 逐步推导的方式——希尔排序
	public static void shellSort(int[] arr) {
		
		//使用循环处理
		int temp = 0;
		int count = 0;	//统计排序次数
		for(int num = arr.length / 2;num > 0;num /= 2){
			for (int i = num; i < arr.length; i++) {
				// 遍历各组中所有的元素(共num组,每组?个元素),步长是num
				for (int j = i - num; j >= 0; j -= num) {
					// 如果当前的元素大于加上步长后的元素,即交换
					if (arr[j] > arr[j + num]) {
						temp = arr[j];
						arr[j] = arr[j + num];
						arr[j + num] = temp;
					}
				}
			}
			System.out.println("第"+ (++count) +"轮排序:" + Arrays.toString(arr));
		}
    }
}

希尔排序[移位式]算法实现

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class ShellSort2 {

	public static void main(String[] args) {
// int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };

		 int[] arr = new int[80000];
		 for(int i = 0;i < 80000;i++){
		 arr[i] = (int)(Math.random() * 80000); //自动生成[0,80000)之间的随机数
		 }

		// 排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);

// shellSort(arr); // 调用[交换式]排序
		shellSort2(arr); // 调用[移位式]排序

		// 排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);
		
	}

    //[交换式]排序
	public static void shellSort(int[] arr) {

		// 使用循环处理
		int temp = 0;
		int count = 0; // 统计排序次数
		for (int num = arr.length / 2; num > 0; num /= 2) {
			for (int i = num; i < arr.length; i++) {
				// 遍历各组中所有的元素(共num组,每组?个元素),步长是num
				for (int j = i - num; j >= 0; j -= num) {
					// 如果当前的元素大于加上步长后的元素,即交换
					if (arr[j] > arr[j + num]) {
						temp = arr[j];
						arr[j] = arr[j + num];
						arr[j + num] = temp;
					}
				}
			}
			System.out.println("第" + (++count) + "轮排序:" + Arrays.toString(arr));
		}
	}

	// 对交换式的希尔排序进行优化->移位法
	public static void shellSort2(int[] arr) {

		for (int num = arr.length / 2; num > 0; num /= 2) {
			// 从num个元素,逐个对其所在的组进行直接插入排序
			for (int i = num; i < arr.length; i++) {
				int j = i;
				int temp = arr[j];
				if (arr[j] < arr[j - num]) {
					while (j - num >= 0 && temp < arr[j - num]) {
						//移动
						arr[j] = arr[j - num];
						j -= num;
					}
					//当退出while循环后,就为temp找到相应的位置
					arr[j] = temp;
				}
			}
		}
	}
}

快速排序

基本介绍

快速排序(Quicksort)是对冒泡排序的一种改进

基本思想是:

通过一趟排序将要排序的数据分割成独立的两部分,
其中一部分的所有数据都比另外一部分的所有数据都要小,
然后再按此方法对这两部分数据分别进行快速排序,
整个排序过程可以递归进行,以此达到整个数据变成有序序列.

快速排序法示意图

快速排序法思路分析图:

源自网络的图片:

快速排序应用实例

对 [-9,78,0,23,-567,70] 进行从小到大的排序,要求使用快速排序法。【测试8w和800w】

说明[验证分析]:
如果取消左右递归,结果是  -9 -567 0 23 78 70
如果取消右递归,结果是  -567 -9 0 23 78 70
如果取消左递归,结果是  -9 -567 0 23 70 78

算法规律推导过程代码:

import java.util.Arrays;

public class QuackSort {

	public static void main(String[] args) {
		int[] arr = { -9, 78, 0, 23, -567, 70 };

		quackSort(arr, 0, arr.length - 1);

		System.out.println("arr排序的结果是:" + Arrays.toString(arr));
	}

	//
	public static void quackSort(int[] arr, int left, int right) {
		int l = left; // 左索引
		int r = right; // 右索引
		int pivot = arr[(left + right) / 2]; // pivot 中轴
		int temp = 0; // 临时变量,作为交换时使用
		// while循环的目的:让,比pivot 值小的放到左边,比pivot 值大的放到右边
		while (l < r) {
			// 在pivot左边一直找,找到一个大于等于pivot的值,才退出
			while (arr[l] < pivot) {
				l += 1;
			}
			// 在pivot右边一直找,找到一个小于等于pivot的值,才退出
			while (arr[r] > pivot) {
				r -= 1;
			}
			// 如果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 -= 1;
			}
			// 如果交换完后,发现这个arr[r] == pivot值,相等 l++, 后移
			if (arr[r] == pivot) {
				l += 1;
			}
		}
		//如果 l == r, 必须l++, r--, 否则为出现栈溢出
		if(l == r){
			l += 1;
			r -= 1;
		}
		//向左递归
		if(left < r){
			quackSort(arr, left, r);
		}
		//向右递归
		if(right > l){
			quackSort(arr, l, right);
		}
	}
}

快速排序算法速度测试:

import java.text.SimpleDateFormat;
import java.util.Date;
//测试快排的执行速度
public class QuackSort2 {

	public static void main(String[] args) {
		
		int[] arr = new int[8000000];
		for(int i = 0;i < 8000000;i++){
			arr[i] = (int)(Math.random() * 8000000); //自动生成[0,8000000)之间的随机数
		}

		// 排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);

		quackSort(arr, 0, arr.length - 1);
		
		// 排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);
	}

	//快速排序
	public static void quackSort(int[] arr, int left, int right) {
		int l = left; // 左索引
		int r = right; // 右索引
		int pivot = arr[(left + right) / 2]; // pivot 中轴
		int temp = 0; // 临时变量,作为交换时使用
		// while循环的目的:让,比pivot 值小的放到左边,比pivot 值大的放到右边
		while (l < r) {
			// 在pivot左边一直找,找到一个大于等于pivot的值,才退出
			while (arr[l] < pivot) {
				l += 1;
			}
			// 在pivot右边一直找,找到一个小于等于pivot的值,才退出
			while (arr[r] > pivot) {
				r -= 1;
			}
			// 如果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 -= 1;
			}
			// 如果交换完后,发现这个arr[r] == pivot值,相等 l++, 后移
			if (arr[r] == pivot) {
				l += 1;
			}
		}
		//如果 l == r, 必须l++, r--, 否则为出现栈溢出
		if(l == r){
			l += 1;
			r -= 1;
		}
		//向左递归
		if(left < r){
			quackSort(arr, left, r);
		}
		//向右递归
		if(right > l){
			quackSort(arr, l, right);
		}
	}
}

归并排序

基本介绍

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

归并排序思想示意图1-基本思想:

说明:

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程。

归并排序思想示意图2-合并相邻有序子序列:

再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤:

源自网络的图片:

归并排序应用实例

给你一个数组, val arr = Array(9,8,7,6,5,4,3,2,1),请使用归并排序完成排序。

归并排序算法代码实现:

import java.util.Arrays;

public class MergetSort {

	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);
 		
 		System.out.println("归并排序的结果:" + Arrays.toString(arr));
	}
	
	//分+和的方法
	public static void mergeSort(int[] arr,int left,int right,int[] tem){
		if(left < right){
			int mid = (left + right) / 2;	//中间索引
			//向左进行递归
			mergeSort(arr, left, mid, tem);
			//向右递归
			mergeSort(arr, mid + 1, right, tem);
			//合并
			marge(arr, left, mid, right, tem);
		}
	}

	//合并的方法
	/** * * @Description * @author subei * @date 2020年5月29日下午5:46:23 * @param arr 排序的原始数组 * @param left 左边有序序列的初始索引 * @param mid 中间索引 * @param right 右边索引 * @param temp 做中转的数组 */
	public static void marge(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 += 1;
				i += 1;
			} else {	//反之,将右边有序序列的当前元素,填充到temp数组
				temp[t] = arr[j];
				t += 1;
				j += 1;
			}
		}
		
		//二、
		//把有剩余数据的一边的数据依次全部填充到temp
		while(i <= mid){	//左边的有序序列还有剩余的元素,就全部填充到temp
			temp[t] = arr[i];
			t += 1;
			i += 1;
		}
		while(j <= right){	//右边的有序序列还有剩余的元素,就全部填充到temp
			temp[t] = arr[j];
			t += 1;
			j += 1;
		}
		
		//三、
		//将temp数组的元素拷贝到arr,注意,并不是每次都拷贝所有数据
		t = 0;
		int tempLeft = left;
		//第一次合并 tempLeft = 0 , right = 1 
		//第二次合并 tempLeft = 2 right = 3 
		//第三次合并 tempLeft = 0 right=3
		//最后一次合并 tempLeft = 0 right = 7
		System.out.println("tempLeft = " + tempLeft + ", right = " + right);
		while(tempLeft <= right){
			arr[tempLeft] = temp[t];
			t += 1;
			tempLeft += 1; 
		}
	}
}

归并排序算法速度测试:

import java.text.SimpleDateFormat;
import java.util.Date;

public class MergetSort {

	public static void main(String[] args) {
		
		int[] arr = new int[80000];
		for(int i = 0;i < 80000;i++){
			arr[i] = (int)(Math.random() * 80000); //自动生成[0,80000)之间的随机数
		}

		// 排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);
		
		int temp[] = new int[arr.length]; //归并排序需要一个额外空间
		mergeSort(arr, 0, arr.length - 1, temp);
 				
		// 排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);
	}
	
	//分+和的方法
	public static void mergeSort(int[] arr,int left,int right,int[] tem){
		if(left < right){
			int mid = (left + right) / 2;	//中间索引
			//向左进行递归
			mergeSort(arr, left, mid, tem);
			//向右递归
			mergeSort(arr, mid + 1, right, tem);
			//合并
			marge(arr, left, mid, right, tem);
		}
	}

	//合并的方法
	/** * * @Description * @author subei * @date 2020年5月29日下午5:46:23 * @param arr 排序的原始数组 * @param left 左边有序序列的初始索引 * @param mid 中间索引 * @param right 右边索引 * @param temp 做中转的数组 */
	public static void marge(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 += 1;
				i += 1;
			} else {	//反之,将右边有序序列的当前元素,填充到temp数组
				temp[t] = arr[j];
				t += 1;
				j += 1;
			}
		}
		
		//二、
		//把有剩余数据的一边的数据依次全部填充到temp
		while(i <= mid){	//左边的有序序列还有剩余的元素,就全部填充到temp
			temp[t] = arr[i];
			t += 1;
			i += 1;
		}
		while(j <= right){	//右边的有序序列还有剩余的元素,就全部填充到temp
			temp[t] = arr[j];
			t += 1;
			j += 1;
		}
		
		//三、
		//将temp数组的元素拷贝到arr,注意,并不是每次都拷贝所有数据
		t = 0;
		int tempLeft = left;
		//第一次合并 tempLeft = 0 , right = 1 
		//第二次合并 tempLeft = 2 right = 3 
		//第三次合并 tempLeft = 0 right=3
		//最后一次合并 tempLeft = 0 right = 7
		while(tempLeft <= right){
			arr[tempLeft] = temp[t];
			t += 1;
			tempLeft += 1; 
		}
	}
}

基数排序

基本介绍

  1. 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用

  2. 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法

  3. 基数排序(Radix Sort)是桶排序的扩展

  4. 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序基本思想:

将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤。    

将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序。



源自网络的图片:

基数排序应用实例

将数组 {53, 3, 542, 748, 14, 214 } 使用基数排序, 进行升序排序。

算法规律推导过程代码:

import java.util.Arrays;

public class RadixSort {
	
	public static void main(String[] args) {
		int[] arr = {53, 3, 542, 748, 14, 214 };
		radSort(arr);	//调用基数排序
	}
	
	//基数排序方法
	public static void radSort(int[] arr){
		
		//定义一个二维数组,表示10个桶,每个桶即为一个一维数组
		//说明:
		//1.二维数组包含10个一维数组
		//2.为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
		//3.需要明确:基数排序是使用空间换时间的经典算法
		int[][] bucket = new int[10][arr.length];
		
		//为了记录每个桶中,实际存放了多少个数据,需要定义一个一维数组来记录各个桶的每次放入的数据个数
		//可以这里理解
		//比如:bucketNums[0] , 记录的就是 bucket[0] 桶的放入数据个数
		int[] bucketNums = new int[10];
		
		//第1轮排序(对每个元素的个位进行排序)
		for(int j = 0;j < arr.length;j++){
			//取出每个元素的个位的值
			int digt = arr[j] % 10;	// 748 % 10 => 8
			//放入到对应的桶中
			bucket[digt][bucketNums[digt]] = arr[j];
			bucketNums[digt]++;
		}
		//按照这个桶的顺序(将一维数组的下标依次取出数据,放入原来数组)
		int index = 0;
		//遍历每一个桶,并将桶中的数据,放入原数组中
		for(int k = 0;k < bucketNums.length;k++){
			//如果桶中有数据,才放入原数组
			if(bucketNums[k] != 0) {
				//循环该桶,即第k个桶(即第k个一维数组), 放入数据
				for(int p = 0;p < bucketNums[k];p++){
					//取出元素放入到arr中
					arr[index++] = bucket[k][p];
				}
			}
			//第1轮处理后,需要将每个 bucketNums[k] = 0 !
			bucketNums[k] = 0;	//重置为0
		}
		System.out.println("第1轮排序:" + Arrays.toString(arr));
		
		//第2轮排序(对每个元素的十位进行排序)
		for(int j = 0;j < arr.length;j++){
			//取出每个元素的十位的值
			int digt = arr[j] / 10 % 10;	// 748 / 10 => 74 % 10 => 4
			//放入到对应的桶中
			bucket[digt][bucketNums[digt]] = arr[j];
			bucketNums[digt]++;
		}
		//按照这个桶的顺序(将一维数组的下标依次取出数据,放入原来数组)
		index = 0;
		//遍历每一个桶,并将桶中的数据,放入原数组中
		for(int k = 0;k < bucketNums.length;k++){
			//如果桶中有数据,才放入原数组
			if(bucketNums[k] != 0) {
				//循环该桶,即第k个桶(即第k个一维数组), 放入数据
				for(int p = 0;p < bucketNums[k];p++){
					//取出元素放入到arr中
					arr[index++] = bucket[k][p];
				}
			}
			//第2轮处理后,需要将每个 bucketNums[k] = 0 !
			bucketNums[k] = 0;	//重置为0
		}
		System.out.println("第2轮排序:" + Arrays.toString(arr));
		
		//第3轮排序(对每个元素的百位进行排序)
		for(int j = 0;j < arr.length;j++){
			//取出每个元素的百位的值
			int digt = arr[j] / 100;	// 748 / 100 => 7
			//放入到对应的桶中
			bucket[digt][bucketNums[digt]] = arr[j];
			bucketNums[digt]++;
		}
		//按照这个桶的顺序(将一维数组的下标依次取出数据,放入原来数组)
		index = 0;
		//遍历每一个桶,并将桶中的数据,放入原数组中
		for(int k = 0;k < bucketNums.length;k++){
			//如果桶中有数据,才放入原数组
			if(bucketNums[k] != 0) {
				//循环该桶,即第k个桶(即第k个一维数组), 放入数据
				for(int p = 0;p < bucketNums[k];p++){
					//取出元素放入到arr中
					arr[index++] = bucket[k][p];
				}
			}
			//第3轮处理后,需要将每个 bucketNums[k] = 0 !
			bucketNums[k] = 0;	//重置为0
		}
		System.out.println("第3轮排序:" + Arrays.toString(arr));
		
	}
}

算法的进一步优化:

import java.util.Arrays;

public class RadixSort {
	
	public static void main(String[] args) {
		int[] arr = {53, 3, 542, 748, 14, 214 };
		
		radSort(arr);	//调用基数排序
	}
	
	//基数排序方法
	public static void radSort(int[] arr){
		
		//1.得到数组中的最大的位数
		int max = arr[0];	//假设第一个数即为最大数
		for(int i = 1;i < arr.length;i++){
			if(arr[i] > max){
				max = arr[i];
			}
		}
		//得到最大数是几位数
		int maxLen = (max + "").length();
		
		//定义一个二维数组,表示10个桶,每个桶即为一个一维数组
		//说明:
		//1.二维数组包含10个一维数组
		//2.为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
		//3.需要明确:基数排序是使用空间换时间的经典算法
		int[][] bucket = new int[10][arr.length];
		
		//为了记录每个桶中,实际存放了多少个数据,需要定义一个一维数组来记录各个桶的每次放入的数据个数
		//可以这里理解
		//比如:bucketNums[0] , 记录的就是 bucket[0] 桶的放入数据个数
		int[] bucketNums = new int[10];
		
		for(int i = 0,n = 1;i < maxLen;i++,n *= 10){
			//对每个元素的各位进行排序,第一次是个位,第二次是十位,第三次是百位.
			for(int j = 0;j < arr.length;j++){
				//取出每个元素的对应位的值
				int digt = arr[j] / n % 10;	// 748 / 1 % 10 => 8
				//放入到对应的桶中
				bucket[digt][bucketNums[digt]] = arr[j];
				bucketNums[digt]++;
			}
			//按照这个桶的顺序(将一维数组的下标依次取出数据,放入原来数组)
			int index = 0;
			//遍历每一个桶,并将桶中的数据,放入原数组中
			for(int k = 0;k < bucketNums.length;k++){
				//如果桶中有数据,才放入原数组
				if(bucketNums[k] != 0) {
					//循环该桶,即第k个桶(即第k个一维数组), 放入数据
					for(int p = 0;p < bucketNums[k];p++){
						//取出元素放入到arr中
						arr[index++] = bucket[k][p];
					}
				}
				//第i+1轮处理后,需要将每个 bucketNums[k] = 0 !
				bucketNums[k] = 0;	//重置为0
			}
			System.out.println("第"+ (i+1) +"轮排序:" + Arrays.toString(arr));
		}
    }
}

算法速度测试:

import java.text.SimpleDateFormat;
import java.util.Date;

public class RadixSort2 {
	
	public static void main(String[] args) {
		
		int[] arr = new int[8000000];
		for(int i = 0;i < 8000000;i++){
			arr[i] = (int)(Math.random() * 8000000); //自动生成[0,8000000)之间的随机数
		}

		// 排序前的时间:
		Date data = new Date();
		SimpleDateFormat simt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS = simt.format(data);
		System.out.println("排序前的时间是:" + dateS);
		
		radSort(arr);	//调用基数排序
		
		// 排序后的时间:
		Date data2 = new Date();
		SimpleDateFormat simt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateS2 = simt2.format(data2);
		System.out.println("排序后的时间是:" + dateS2);
		
	}
	
	//基数排序方法
	public static void radSort(int[] arr){
		
		//1.得到数组中的最大的位数
		int max = arr[0];	//假设第一个数即为最大数
		for(int i = 1;i < arr.length;i++){
			if(arr[i] > max){
				max = arr[i];
			}
		}
		//得到最大数是几位数
		int maxLen = (max + "").length();
		
		//定义一个二维数组,表示10个桶,每个桶即为一个一维数组
		//说明:
		//1.二维数组包含10个一维数组
		//2.为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
		//3.需要明确:基数排序是使用空间换时间的经典算法
		int[][] bucket = new int[10][arr.length];
		
		//为了记录每个桶中,实际存放了多少个数据,需要定义一个一维数组来记录各个桶的每次放入的数据个数
		//可以这里理解
		//比如:bucketNums[0] , 记录的就是 bucket[0] 桶的放入数据个数
		int[] bucketNums = new int[10];
		
		for(int i = 0,n = 1;i < maxLen;i++,n *= 10){
			//对每个元素的各位进行排序,第一次是个位,第二次是十位,第三次是百位.
			for(int j = 0;j < arr.length;j++){
				//取出每个元素的对应位的值
				int digt = arr[j] / n % 10;	// 748 / 1 % 10 => 8
				//放入到对应的桶中
				bucket[digt][bucketNums[digt]] = arr[j];
				bucketNums[digt]++;
			}
			//按照这个桶的顺序(将一维数组的下标依次取出数据,放入原来数组)
			int index = 0;
			//遍历每一个桶,并将桶中的数据,放入原数组中
			for(int k = 0;k < bucketNums.length;k++){
				//如果桶中有数据,才放入原数组
				if(bucketNums[k] != 0) {
					//循环该桶,即第k个桶(即第k个一维数组), 放入数据
					for(int p = 0;p < bucketNums[k];p++){
						//取出元素放入到arr中
						arr[index++] = bucket[k][p];
					}
				}
				//第i+1轮处理后,需要将每个 bucketNums[k] = 0 !
				bucketNums[k] = 0;	//重置为0
			}
// System.out.println("第"+ (i+1) +"轮排序:" + Arrays.toString(arr));
		}
	}
}

基数排序算法注意事项

  • 基数排序是对传统桶排序的扩展,速度很快.
  • 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成OutOfMemoryError。
  • 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
  • 有负数的数组,我们不用基数排序来进行排序,如果要支持负数,参考:https://code.i-harness.com/zh-CN/q/e98fa9

堆排序(在二叉树部分)

常用排序算法的总结

一张排序算法的比较图

相关术语解释:

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度:一个算法执行所耗费的时间。
  • 空间复杂度:运行完一个程序所需内存的大小。
  • n:数据规模
  • k:“桶”的个数
  • In-place:不占用额外内存
  • Out-place:占用额外内存

本章思维的导图