文章目录


1.排序算法的介绍和分类

  • 排序也称​​排序算法 (Sort Algorithm)​​,排序是将一组数据,依指定的顺序进行排列的过程。
  • 排序的分类
  • ​内部排序​​​:指将需要处理的所有数据都加载到​​内存中​​进行排序。
  • ​外部排序​​​:数据量过大,无法全部加载到内存中,需要借助​​外部存储​​ (文件等)进行排序。
  • 常见的排序算法分类

彻底搞懂排序算法,看这篇就够了_合并排序

2.算法的时间复杂度和空间复杂度

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

  • 事后统计的方法
  • 这种方法可行,但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要​​实际运行该程序​​​;二是所得时间的统计量依赖于​​计算机的硬件​​​、​​软件​​​等环境因素, 这种方式,要在​​同一台计算机​​​的​​相同状态​​下运行,才能比较那个算法速度更快。
  • 事前估算的方法
  • 通过分析某个算法的​​时间复杂度​​来判断哪个算法更优

时间频度

  • 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。​​一个算法中的语句执行次数称为语句频度或时间频度​​,记为T(n)。

时间频度的举例说明

public class Sort {

//比如计算1-100所有数字之和, 设计如下两种算法:
public static void main(String[] args) {
//方法一: 使用for循环计算
int total = 0;
int end = 100;
for(int i = 1;i <= end;i++){
total += i;
}
System.out.println("total = " + total); //T(n)=n+1;加1是因为最后还要判断i是不是小于等于end

//方法二: 直接进行计算
int total2 = 0;
int end2 = 100;
total2 = (1 + end2)*end2/2;
System.out.println("total2 = " + total2); //T(n)=1
}

}

2.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

彻底搞懂排序算法,看这篇就够了_插入排序_02

结论:
1. 2n+20 2*n 随着 n 变大,执行曲线无限接近, 20可以忽略
2. 3n+10 3n 随着 n 变大,执行曲线无限接近, 10可以忽略
  • 举例说明 -​​忽略低次项​

T(n)=2*n^2+3n+10

T(n)=(2*n^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

彻底搞懂排序算法,看这篇就够了_算法_03

结论: 
1. 2*n^2+3n+10 2*n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10
2. n^2+5n+20 n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 5n+20
  • 举例说明 -​​忽略系数​

T(n)=(3*n^2+2n)

T(n)=(5*n^2+7n)

T(n)=(n^3+5n)

T(n)=(6*n^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

彻底搞懂排序算法,看这篇就够了_合并排序_04

结论: 
1. 随着n值变大,5*n^2+7n 3*n^2 + 2n ,执行曲线重合, 说明这种情况下, 5和3可以忽略。
2. n^3+5n 6n^3+4n ,执行曲线分离,说明忽略系数不适用于 n^k(k>=3)的情况。

2.2 时间复杂度的计算

  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)=3n²+7n​​+6​​​ => T(n)=3n²+7n​​+1​
  • 修改后的运行次数函数中,​​只保留最高阶项​​​ T(n)=3n²​​+7n+1​​ => T(n) = 3n²
  • 去除最高阶项的系数 T(n) =​​3​​​n² =>​​O(n²)​

2.3 常见的时间复杂度

1.常数阶O(1)

2.对数阶O(log2n):2是底数,n是真数

3.线性阶O(n)

4.线性对数阶O(nlog2n)

5.平方阶O(n^2)

6.立方阶O(n^3)

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

8.指数阶O(2^n):从图中可见,我们应该尽量避免使用指数阶的算法

说明:
常见的算法时间复杂度由小到大依次为:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3) O(n^k) <O(2^n)

彻底搞懂排序算法,看这篇就够了_排序算法_05

2.3.1 常数阶O( 1 )

  • 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1) 。
public static void main(String[] args) {
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
}
  • 上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有​​几万几十万行​​,都可以用O(1)来表示它的时间复杂度。

2.3.2 对数阶O( log2n )

  • 如果
  • 彻底搞懂排序算法,看这篇就够了_算法_06

  • ,即ax次方等于Na>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作
  • 彻底搞懂排序算法,看这篇就够了_算法_07

  • 。其中,a叫做 对数的​​底数​​,N叫做 ​​真数​​,x叫做​​以a为底N的对数​​。
public static void main(String[] args) {
int i = 1;
while (i < n) {
i = i * 2;
}
}
  • 在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环​​x次​​​之后,i 就​​大于 n​​​ 了,此时这个循环就退出了,也就是说​​2 的 x 次方等于 n​​​,那么​​x = log2n​​​也就是说当循环​​log2n 次以后​​​,这个代码就结束了。因此这个代码的时间复杂度为:​​O(log2n)​​​ 。​​O(log2n)​​​ 的这个2 实际上是根据代码变化的,​​i = i * 3​​​ ,则是​​O(log3n)​​ 。

2.3.3 线性阶O( n )

public static void main(String[] args) {
int j = 0;
for (int i = 1;i <= n;i++) {
j = i;
j++;
}
System.out.println("j = " + j);
}
  • 这段代码,for循环里面的代码会​​执行n遍​​​,因此它消耗的时间是​​随着n的变化而变化的​​​,因此这类代码都可以用​​O(n)​​来表示它的时间复杂度。

2.3.4 线性对数阶O( nlogN )

public static void main(String[] args) {
int i = 0;
for (int m = 1;m <= n;m++) {
i = 1;
while (i < n) {
i = i * 2;
}
}
System.out.println("i = " + i);
}
  • 线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。

2.3.5 平方阶O( n² )

public static void main(String[] args) {
int j = 0;
for (int x = 1;x <= n;x++) {
for (int i = 1; i < n; i++) {
j = i;
j++;
}
}
System.out.println("j = " + j);
}
  • 平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是​​O(n²)​​​,这段代码其实就是嵌套了​​2层n循环​​,它的时间复杂度就是 O(n * n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m * n)。

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

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

2.4 算法的平均时间复杂度和最坏时间复杂度

  • ​平均时间复杂度​​​是指所有可能的输入实例均以​​等概率​​出现的情况下,该算法的运行时间。
  • 最坏情况下的时间复杂度称​​最坏时间复杂度​​​。​​一般讨论的时间复杂度均是最坏情况下的时间复杂度​​。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
  • 平均时间复杂度和最坏时间复杂度是否一致,​​和算法有关​​(如下表)。

排序法

平均时间

最差情形

稳定度

额外空间

备注

冒泡

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大时较好

2.5 空间复杂度

  • 类似于时间复杂度的讨论,一个算法的​​空间复杂度(Space Complexity)定义为该算法所耗费的存储空间​​,它也是问题规模n的函数。
  • 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的比例。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。
  • 在做算法分析时,​​主要讨论的是时间复杂度​​。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。

3.冒泡排序

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

图解冒泡排序算法的过程

将五个无序的数:​​3、9、-1、10、20​​​ 使用​​冒泡排序​​​将其排成一个​​有序数列​


彻底搞懂排序算法,看这篇就够了_插入排序_08

彻底搞懂排序算法,看这篇就够了_算法_09

彻底搞懂排序算法,看这篇就够了_合并排序_10

彻底搞懂排序算法,看这篇就够了_插入排序_11

冒泡排序规则:
1. 一共进行(数组的大小-1)次大的循环
2. 每一趟排序的次数在逐渐的减少
3.

冒泡排序的动态图

彻底搞懂排序算法,看这篇就够了_合并排序_12

3.1 代码实现

/**
* @author xiexu
* @create 2020-11-04 2:07 下午
*/
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 - 0; 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));

}
}

根据以上的规律,进行优化

public class BubbleSort {
public static void main(String[] args) {
int arr[] = {3, 9, -1, 10, -5};
System.out.print("排序前的数组:");
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
}

public static void bubbleSort(int[] arr) {
//冒泡排序的时间复杂度O(n^2)
int temp = 0; //临时变量,用于数据交换
boolean flag = 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]) {
flag = true;
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}

System.out.print("第" + (j + 1) + "趟排序后的数组:");
System.out.println(Arrays.toString(arr));

}
}

}

对代码进行再次优化

public class BubbleSort {
public static void main(String[] args) {
int arr[] = {3, 9, -1, 10, -5};
System.out.print("排序前的数组:");
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
System.out.print("排序前的数组:");
System.out.println(Arrays.toString(arr));
}

public static void bubbleSort(int[] arr) {
//冒泡排序的时间复杂度O(n^2)
int temp = 0; //临时变量,用于数据交换
boolean flag = 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]) {
flag = true;
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
if (!flag) { // 在一趟排序中,一次交换都没有,直接退出
break;
}else {
flag = false; // 重置flag,方便下一次循环判断
}
}
}

}

冒泡排序的速度测试

/**
* @author xiexu
* @create 2020-11-04 2:07 下午
*/
public class BubbleSort {
public static void main(String[] args) {
int arr[] = {3, 9, -1, 10, -5};
System.out.println("排序前的数组:");
System.out.println(Arrays.toString(arr));

//测试一下冒泡排序的速度:O(n^2),给8000个的数据,测试
// int[] arr = new int[80000];
// for(int i = 0;i < 80000;i++){
// arr[i] = (int)(Math.random() * 80000); //自动生成[0,80000)之间的随机数
// }

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

bubbleSort(arr);
System.out.println("排序后的数组:");
System.out.println(Arrays.toString(arr));

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

public static void bubbleSort(int[] arr) {
//冒泡排序的时间复杂度O(n^2)
int temp = 0; //临时变量,用于数据交换
boolean flag = 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]) {
flag = true;
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
if (!flag) { // 在一趟排序中,一次交换都没有,直接退出
break;
}else {
flag = false; // 重置flag,方便下一次循环判断
}

}
}

}

tips:建议理解了冒泡排序算法后,可以自己不看例子手写一遍,有助于更深的理解!

4.选择排序

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

选择排序的思想

选择排序(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)次,得到一个按排序码从小到大排列的有序序列。

选择排序的思路分析图

彻底搞懂排序算法,看这篇就够了_合并排序_13

彻底搞懂排序算法,看这篇就够了_合并排序_14

选择排序的思路图解

彻底搞懂排序算法,看这篇就够了_合并排序_15

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

4.1 代码实现

/**
* @author xiexu
* @create 2020-11-04 5:39 下午
*/
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);
}

//选择排序的时间复杂度O(n^2)
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; //重置最小值索引
}
}

//将最小值放在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; //重置最小值索引
}
}

//将最小值放在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 = 2 + 1; j < arr.length; j++) {
if (min > arr[j]) {
min = arr[j]; //重置最小值
minIndex = j; //重置最小值索引
}
}

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

System.out.print("第三轮后:");
System.out.println(Arrays.toString(arr)); //第三轮后:[1, 34, 101, 119]

}

}

根据以上的规律,进行优化

/**
* @author xiexu
* @create 2020-11-04 5:39 下午
*/
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));
}

//选择排序的时间复杂度O(n^2)
public static void selectSort(int[] arr) {
//使用逐步推导的方式来讲解选择排序
// 第1轮
// 原始数组: 101, 34, 119, 1
// 第一轮排序: 1, 34, 119, 101

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; //重置最小值索引
}
}

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

}

选择排序的速度测试

/**
* @author xiexu
* @create 2020-11-04 5:39 下午
*/
public class SelectSort {

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) {
//使用逐步推导的方式来讲解选择排序
// 第1轮
// 原始数组: 101, 34, 119, 1
// 第一轮排序: 1, 34, 119, 101

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; //重置最小值索引
}
}

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

}

5.插入排序

  • 插入式排序属于​​内部排序法​​,是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

插入排序的思想

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

插入排序的思路分析图

彻底搞懂排序算法,看这篇就够了_算法_16

彻底搞懂排序算法,看这篇就够了_插入排序_17

5.1 代码实现

/**
* @author xiexu
* @create 2020-11-04 8:32 下午
*/
public class InsertSort {

public static void main(String[] args) {
int []arr = {101,34,119,1};
insertSort(arr);
}

//插入排序
public static void insertSort(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];
insertIndex--;
}

//当退出while循环时,说明找到要插入的位置, insertIndex + 1
arr[insertIndex + 1] = insertVal;

System.out.print("第1轮插入:");
System.out.println(Arrays.toString(arr));

//第二轮
//定义待插入的数
insertVal = arr[2];
insertIndex = 2 - 1; //即arr[2]的前面这个数的下标

while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}

//当退出while循环时,说明找到要插入的位置, insertIndex + 1
arr[insertIndex + 1] = insertVal;

System.out.print("第2轮插入:");
System.out.println(Arrays.toString(arr));

//第三轮
//定义待插入的数
insertVal = arr[3];
insertIndex = 3 - 1; //即arr[3]的前面这个数的下标

while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}

//当退出while循环时,说明找到要插入的位置, insertIndex + 1
arr[insertIndex + 1] = insertVal;

System.out.print("第3轮插入:");
System.out.println(Arrays.toString(arr));

}
}

根据以上规律,进行优化

/**
* @author xiexu
* @create 2020-11-04 8:32 下午
*/
public class InsertSort {

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

//插入排序
public static void insertSort(int[] arr) {
//使用逐步推导的方式来讲解,偏于理解

//第1轮{101, 34, 119, 1}; => {34, 101, 119, 1};

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]) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}

//当退出while循环时,说明找到要插入的位置, insertIndex + 1
arr[insertIndex + 1] = insertVal;

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

}
}

插入排序的速度测试

/**
* @author xiexu
* @create 2020-11-04 8:32 下午
*/
public class InsertSort {

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

insertSort(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 insertSort(int[] arr) {
//使用逐步推导的方式来讲解,偏于理解

//第1轮{101, 34, 119, 1}; => {34, 101, 119, 1};

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]) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}

//当退出while循环时,说明找到要插入的位置, insertIndex + 1
arr[insertIndex + 1] = insertVal;
}

}
}

可以对代码进行小小的优化,但​​貌似没什么区别​

/**
* @author xiexu
* @create 2020-11-04 8:32 下午
*/
public class InsertSort {

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

insertSort(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 insertSort(int[] arr) {
//使用逐步推导的方式来讲解,偏于理解

//第1轮{101, 34, 119, 1}; => {34, 101, 119, 1};

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]) {
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}
//当退出while循环时,说明找到要插入的位置, insertIndex + 1
//这里我们判断是否需要赋值
if (insertIndex + 1 == i) {
arr[insertIndex + 1] = insertVal;
}
}

}
}

6.希尔排序

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

我们看简单的插入排序可能存在的问题:
当需要插入的数是较小的数时,后移的次数明显增多,对效率会有影响。
数组 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时,整个文件恰被分成一组,算法便终止。

希尔排序的思路分析图

彻底搞懂排序算法,看这篇就够了_快速排序_18

彻底搞懂排序算法,看这篇就够了_快速排序_19

希尔排序动态图

彻底搞懂排序算法,看这篇就够了_排序算法_20

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

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

6.1 代码实现

/**
* @author xiexu
* @create 2020-11-05 11:17 上午
*/
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组,每组有2个元素),步长为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++) {
//遍历各组中的所有元素(共5组,每组有2个元素),步长为5
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 组
for (int i = 1; i < arr.length; i++) {
//遍历各组中的所有元素(共5组,每组有2个元素),步长为5
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));

}
}

对于以上的规律,进行优化

/**
* @author xiexu
* @create 2020-11-05 11:17 上午
*/
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 gap = arr.length / 2; gap > 0; gap /= 2) {

for (int i = gap; i < arr.length; i++) {
//遍历各组中的所有元素(共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;
}
}
}
System.out.println("第"+(++count)+"轮排序:" + Arrays.toString(arr));

}
}
}

希尔排序-交换法的速度测试

/**
* @author xiexu
* @create 2020-11-05 11:17 上午
*/
public class ShellSort {

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);

shellSort(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 gap = arr.length / 2; gap > 0; gap /= 2) {

for (int i = gap; i < arr.length; i++) {
//遍历各组中的所有元素(共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;
}
}
}

}
}
}

前面的希尔排序代码都是采用​​交换式的​​​,接下来使用​​移位式​​排序

/**
* @author xiexu
* @create 2020-11-05 11:17 上午
*/
public class ShellSort {

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);

//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 gap = arr.length / 2; gap > 0; gap /= 2) {

for (int i = gap; i < arr.length; i++) {
//遍历各组中的所有元素(共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;
}
}
}

}
}

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

7.快速排序

  • 同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。
  • 不同的是,冒泡排序在每一轮中只把一个元素冒泡到数列的一端,而快速排序则在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列的一边,比它小的元素则移动到数列的另一边,从而把数列拆解成两个部分。

7.1 代码实现

/**
* @author xiexu
* @create 2020-11-05 10:52 下午
*/
public class QuickSort {

public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -567, 70};
quackSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}

public static void quackSort(int[] arr, int startIndex, int endIndex) {
//递归结束的条件:startIndex >= endIndex时
if (startIndex >= endIndex) {
return;
}
//得到基准元素位置
int pivotIndex = partition(arr,startIndex,endIndex);
//根据基准元素,分成两部分进行递归排序
quackSort(arr, startIndex, pivotIndex - 1); //往左边递归
quackSort(arr, pivotIndex + 1, endIndex); //往右边递归
}

/**
* 分治(双边循环法)
*
* @param arr
* @param startIndex
* @param endIndex
* @return
*/
private static int partition(int[] arr, int startIndex, int endIndex) {
//取第一个位置(也可以选择随机位置)的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;

while (left != right) {
//控制right指针比较并左移
while (left < right && arr[right] > pivot) {
right--;
}
//控制left指针比较并右移
while (left < right && arr[left] <= pivot) {
left++;
}
//交换left和right指针所指向的元素
if (left<right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}

//pivot和指针重合点交换
arr[startIndex] = arr[left];
arr[left] = pivot;

return left;
}

}

单边循环法

/**
* 分治(单边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
* @return
*/
private static int partition2(int[] arr, int startIndex, int endIndex) {
int pivot = arr[startIndex];
int mark = startIndex;

for (int i = startIndex+1; i <= endIndex; i++) {
if(arr[i] < pivot) {
mark++;
int temp = arr[mark];
arr[mark] = arr[i];
arr[i] = temp;
}
}

arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;

}

对快速排序进行速度测试

/**
* @author xiexu
* @create 2020-11-05 10:52 下午
*/
public class QuickSort {

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);

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 startIndex, int endIndex) {
//递归结束的条件:startIndex >= endIndex时
if (startIndex >= endIndex) {
return;
}
//得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex); //双边循环
//int pivotIndex = partition2(arr, startIndex, endIndex); //单边循环

//根据基准元素,分成两部分进行递归排序
quackSort(arr, startIndex, pivotIndex - 1);
quackSort(arr, pivotIndex + 1, endIndex);
}

/**
* 分治(双边循环法)
* @param arr
* @param startIndex
* @param endIndex
* @return
*/
private static int partition(int[] arr, int startIndex, int endIndex) {
//取第一个位置(也可以选择随机位置)的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;

while (left != right) {
//控制right指针比价并左移
while (left < right && arr[right] > pivot) {
right--;
}
while (left < right && arr[left] <= pivot) {
left++;
}
//交换left和right指针所指向的元素
if (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}

//pivot和指针重合点交换
arr[startIndex] = arr[left];
arr[left] = pivot;

return left;
}

/**
* 分治(单边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
* @return
*/
private static int partition2(int[] arr, int startIndex, int endIndex) {
int pivot = arr[startIndex];
int mark = startIndex;

for (int i = startIndex+1; i <= endIndex; i++) {
if(arr[i] < pivot) {
mark++;
int temp = arr[mark];
arr[mark] = arr[i];
arr[i] = temp;
}
}

arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}

}

8.归并排序

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

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

彻底搞懂排序算法,看这篇就够了_快速排序_21

说明

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

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

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

彻底搞懂排序算法,看这篇就够了_排序算法_22

彻底搞懂排序算法,看这篇就够了_排序算法_23

归并排序的动态图

彻底搞懂排序算法,看这篇就够了_排序算法_24

8.1 代码实现

/**
* @author xiexu
* @create 2020-11-07 4:26 下午
*/
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);
}
}

//合并的方法
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;
}

}
}

对归并排序进行速度测试

/**
* @author xiexu
* @create 2020-11-07 4:26 下午
*/
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);
}
}

//合并的方法
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;
}

}
}

9.基数排序

  • 基数排序(radix sort)属于“分配式排序”(distribution sort),又称​​桶子法​​(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
  • 基数排序法是属于​​稳定性​​​的排序,基数排序法的是​​效率高​​​的​​稳定性排序法​
  • 基数排序(Radix Sort)是​​桶排序​​的扩展
  • 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序的基本思想

  • 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  • 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤

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

彻底搞懂排序算法,看这篇就够了_快速排序_25

彻底搞懂排序算法,看这篇就够了_排序算法_26

彻底搞懂排序算法,看这篇就够了_合并排序_27

基数排序动态图

彻底搞懂排序算法,看这篇就够了_快速排序_28

9.1 代码实现

/**
* @author xiexu
* @create 2020-11-09 10:26 上午
*/
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 dig = arr[j] / 1 % 10; //例如:748 % 10 = 8
//放入到对应的桶中
bucket[dig][bucketNums[dig]] = arr[j];
bucketNums[dig]++;
}
//按照这个桶的顺序(从一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一个桶,并将桶中的数据,放入原数组中
for (int k = 0; k < bucketNums.length; k++) {
//如果桶中有数据,才放入原数组
if (bucketNums[k] != 0) {
//循环该桶,即第k个桶(即第k个一维数组),放入数据
for (int l = 0; l < bucketNums[k]; l++) {
//取出数据放入到arr中
arr[index++] = bucket[k][l];
}
}
//第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 dig = arr[j] / 10 % 10; //例如:748 / 10 = 74 ; 74 % 10 = 4
//放入到对应的桶中
bucket[dig][bucketNums[dig]] = arr[j];
bucketNums[dig]++;
}
//按照这个桶的顺序(从一维数组的下标依次取出数据,放入原来数组)
index = 0;
//遍历每一个桶,并将桶中的数据,放入原数组中
for (int k = 0; k < bucketNums.length; k++) {
//如果桶中有数据,才放入原数组
if (bucketNums[k] != 0) {
//循环该桶,即第k个桶(即第k个一维数组),放入数据
for (int l = 0; l < bucketNums[k]; l++) {
//取出数据放入到arr中
arr[index++] = bucket[k][l];
}
}
//第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 dig = arr[j] / 100 % 10; //例如:748 / 100 = 7
//放入到对应的桶中
bucket[dig][bucketNums[dig]] = arr[j];
bucketNums[dig]++;
}
//按照这个桶的顺序(从一维数组的下标依次取出数据,放入原来数组)
index = 0;
//遍历每一个桶,并将桶中的数据,放入原数组中
for (int k = 0; k < bucketNums.length; k++) {
//如果桶中有数据,才放入原数组
if (bucketNums[k] != 0) {
//循环该桶,即第k个桶(即第k个一维数组),放入数据
for (int l = 0; l < bucketNums[k]; l++) {
//取出数据放入到arr中
arr[index++] = bucket[k][l];
}
}
//第3轮处理后,需要将每个 bucketNums[k] = 0
bucketNums[k] = 0; //重置为0
}
System.out.println("第3轮排序:" + Arrays.toString(arr));
}

}

对于以上的规律,进行优化

/**
* @author xiexu
* @create 2020-11-09 10:26 上午
*/
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) {

//根据前面的推导过程,我们可以得到最终的基数排序代码

//得到数组中的最大数
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];

for (int i = 0, n = 1; i < maxLen; i++, n *= 10) {

//为了记录每个桶中,实际存放了多少个数据,需要定义一个一维数组来记录各个桶 每次放入的数据个数
//可以这里理解
//比如:bucketNums[0],记录的就是:bucket[0]桶 放入数据的个数
int[] bucketNums = new int[10];

//第1轮排序(对每个元素的个位进行排序)
for (int j = 0; j < arr.length; j++) {
//取出每个元素的个位的值
int dig = arr[j] / n % 10; //例如:748 % 10 = 8
//放入到对应的桶中
bucket[dig][bucketNums[dig]] = arr[j];
bucketNums[dig]++;
}
//按照这个桶的顺序(从一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一个桶,并将桶中的数据,放入原数组中
for (int k = 0; k < bucketNums.length; k++) {
//如果桶中有数据,才放入原数组
if (bucketNums[k] != 0) {
//循环该桶,即第k个桶(即第k个一维数组),放入数据
for (int l = 0; l < bucketNums[k]; l++) {
//取出数据放入到arr中
arr[index++] = bucket[k][l];
}
}
//第1轮处理后,需要将每个 bucketNums[k] = 0
bucketNums[k] = 0; //重置为0
}
System.out.println("第" + (i + 1) + "轮排序:" + Arrays.toString(arr));
}
}

}

对基数排序进行速度测试

/**
* @author xiexu
* @create 2020-11-09 10:26 上午
*/
public class RadixSort {
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) {

//根据前面的推导过程,我们可以得到最终的基数排序代码

//得到数组中的最大数
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];

for (int i = 0, n = 1; i < maxLen; i++, n *= 10) {

//为了记录每个桶中,实际存放了多少个数据,需要定义一个一维数组来记录各个桶 每次放入的数据个数
//可以这里理解
//比如:bucketNums[0],记录的就是:bucket[0]桶 放入数据的个数
int[] bucketNums = new int[10];

//第1轮排序(对每个元素的个位进行排序)
for (int j = 0; j < arr.length; j++) {
//取出每个元素的个位的值
int dig = arr[j] / n % 10; //例如:748 % 10 = 8
//放入到对应的桶中
bucket[dig][bucketNums[dig]] = arr[j];
bucketNums[dig]++;
}
//按照这个桶的顺序(从一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一个桶,并将桶中的数据,放入原数组中
for (int k = 0; k < bucketNums.length; k++) {
//如果桶中有数据,才放入原数组
if (bucketNums[k] != 0) {
//循环该桶,即第k个桶(即第k个一维数组),放入数据
for (int l = 0; l < bucketNums[k]; l++) {
//取出数据放入到arr中
arr[index++] = bucket[k][l];
}
}
//第1轮处理后,需要将每个 bucketNums[k] = 0
bucketNums[k] = 0; //重置为0
}

//System.out.println("第" + (i + 1) + "轮排序:" + Arrays.toString(arr));
}
}
}

9.2 基数排序算法的注意事项

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

10.堆排序(在二叉树部分)

11.常用排序算法的总结和对比

彻底搞懂排序算法,看这篇就够了_排序算法_29

相关术语解释:

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