基础排序算法

排序算法:让数据有序

排序算法中蕴含着重要的算法设计思想

  • 两个基础的排序算法
    • 选择排序法
    • 插入排序法

选择排序法

思路:先把最小的拿出来,剩下的再把最小的拿出来,剩下的再把最小的拿出来…每次选择还没处理(排序)的元素里最小的元素

过程:

  • 目前有一组元素:6 4 2 3 1 5

  • 开辟一个新的空间进行操作

    • 拿出最小的元素1,放入新的空间内,剩余元素6 4 2 3 5
    • 剩余的元素拿出最小的元素2,放入新的空间内1的后面,剩余元素6 4 3 5
    • 剩余的元素拿出最小的元素3,放入新的空间内2的后面,剩余元素6 4 5
    • 剩余的元素拿出最小的元素4,放入新的空间内3的后面,剩余元素6 5
    • 剩余的元素拿出最小的元素5,放入新的空间内4的后面,剩余元素6
      • 剩下一个元素,拿出放入新的空间内5的后面,则新数组为:1 2 3 4 5 6

这样的排序过程占用了额外的空间

思考:可否进行原地(原空间)排序完成?

  • arr[i…n)未排序
  • arr[0…i)已排序 – 循环不变量
  • arr[i…n)中的最小值要放到arr[i]的位置

已排好序的部分是最终排序的结果

开辟新空间的选择排序法

1
2
3
4
5
6
7
8
9
10
11
12
public static <E extends Comparable<E>> void sortSpace(E[] arr) {
// 开辟一个新的数组,将每轮选择的最小数字,加入到该数组中,注意:泛型数组的需要强转
E[] newArr = (E[]) new Object[arr.length];
// 循环数组,进行比较,将每轮最最小的值放入到newArr新的数组中
for (E value : arr) {
for (int j = 1; j < arr.length; j++) {
if (arr[j].compareTo(value) < 0) {
newArr[j] = arr[j];
}
}
}
}

原地排序的选择排序法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static <E extends Comparable<E>> void sort(E[] arr) {
// 循环不变量:arr[0...i)是有序的;arr[i...n)是无序的
for (int i = 0; i < arr.length; i++) {
// 选择arr[i...n)中的最小值的索引,并赋值给minIndex
int minIndex = i;
for (int j = i; j < arr.length; j++) {
if (arr[j].compareTo(arr[minIndex]) < 0) {
minIndex = j;
}
}
// 交换元素
SortingHelper.swap(arr, i, minIndex);
}
}

选择排序法的复杂度分析

image-20210310152215074

通过测试来验证复杂度及排序的性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
int n1 = 1000;
int n2 = 10000;
int n3 = 100000;
Integer[] arr1 = ArrayGenerator.generateRandomArray(n1, n1);
Integer[] arr2 = ArrayGenerator.generateRandomArray(n2, n2);
Integer[] arr3 = ArrayGenerator.generateRandomArray(n3, n3);
// 选择排序,数组为:10000(一千)
SortingHelper.sortTest("SelectionSort", arr1);
// 选择排序,数组为:100000(一万)
SortingHelper.sortTest("SelectionSort", arr2);
// 选择排序,数组为:1000000(十万)
SortingHelper.sortTest("SelectionSort", arr3);
}

上面的代码,根据数组长度分别测试一千、一万、十万,生成的随机数组进行排序,我们看下执行结果

image-20210310153619616

随着数组的长度增长,时间也是线性直线增加。

插入排序法

思路:每次处理一个元素,把这个元素插入到前面已经排好序的数组中

过程:

  • arr[0, 1)已经排好序
  • arr[i…n)未排序
  • 把arr[i]放到合适的位置
  • 第一个元素(元素1),第二元素(元素2)与元素1比较,如果元素2比元素1小,则进行交换位置,否则不动,进行下一步
  • 第三个元素(元素3),元素3比元素2小,则交换位置,继续元素3比元素1小,则交换位置,否则不动,进行下一步
  • 第四个元素(元素4),元素4比元素3小,则交换位置,继续元素4比元素2小,则交换位置,下来元素4比元素1大,则不进行交换,元素4已找到对应位置…
  • 第五个元素…依次类推…

已排好序的部分是目前已经遍历的元素,还有未遍历的元素,不是最终结果

插入排序算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 插入排序算法
*
* @param arr 数组
* @param <E> 数组元素的类型
*/
public static <E extends Comparable<E>> void sort(E[] arr) {
for (int i = 0; i < arr.length; i++) {
// 将arr[i]插入到合适的位置
for (int j = i; j - 1 >= 0; j--) {
if (arr[j].compareTo(arr[j - 1]) < 0) {
SortingHelper.swap(arr, j, j - 1);
} else {
break;
}
}
}
}

将if语句中的条件合并到for循环中的条件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 插入排序算法优化
*
* @param arr 数组
* @param <E> 数组元素的类型
*/
public static <E extends Comparable<E>> void sortMerge(E[] arr) {
for (int i = 0; i < arr.length; i++) {
// 将if语句合并到一起
for (int j = i; j - 1 >= 0 && arr[j].compareTo(arr[j - 1]) < 0; j--) {
SortingHelper.swap(arr, j, j - 1);
}
}
}

插入排序算法优化

注意:上述的步骤都是每次交换是三次操作,是否可优化呢?

  • 暂存需要比较的元素,该元素比较前一个元素,如果前一个元素比该元素大,则将前一个元素赋值给该元素的位置
  • 根据上一步,继续该方式,将大的值赋值给后一位元素(则是后移一位)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 插入排序算法优化
*
* @param arr 数组
* @param <E> 数组元素的类型
*/
public static <E extends Comparable<E>> void sortPlus(E[] arr) {
for (int i = 0; i < arr.length; i++) {
// 暂存元素
E t = arr[i];
int j;
for (j = i; j - 1 >= 0 && t.compareTo(arr[j - 1]) < 0; j--) {
// 前面的元素,赋值给当前元素(相当于平移)
arr[j] = arr[j - 1];
}
arr[j] = t;
}
}

插入排序复杂度分析及两种方式性能的对比

时间复杂度:O(n^2)

依旧是根据数组长度分别测试一千、一万、十万,生成的随机数组进行排序,我们看下测试代码及执行结果

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
int[] dataSize = {1000, 10000, 100000};
for (int n : dataSize) {
Integer[] arr = ArrayGenerator.generateRandomArray(n, n);
Integer[] arr2 = Arrays.copyOf(arr, arr.length);
SortingHelper.sortTest("InsertionSort", arr);
SortingHelper.sortTest("InsertionSortPlus", arr2);
}
}

image-20210312105924376

插入排序法的重要特性

  • 对于有序数组,插入排序的复杂度是O(n)的
  • 对于无序数组,插入排序的复杂度依然是O(n^2)的
  • 对比,不论是有序还是无序数组,选择排序的复杂度永远是O(n^2)的

换个角度实现插入排序法

基本逻辑是:从后向前扫描,对于每一个arr[i],寻找arr[i…n)区间中需要插入的位置。

在具体找这个位置的时候,我们依然是暂存arr[i]到t这个变量,之后,只要t比当前的arr[j+1]还要大,说明t应该插入的位置还靠后,我们只需要让arr[j+1]平行移动到arr[j]的位置,然后j++,继续看下一个j。

1
2
3
4
5
6
7
8
9
10
11
public static <E extends Comparable<E>> void sort2(E[] arr) {
for (int i = arr.length - 1; i >= 0; i--) {
// 暂存元素
E t = arr[i];
int j;
for (j = i; j + 1 < arr.length && arr[j].compareTo(arr[j + 1]) > 0; j--) {
arr[j] = arr[j + 1];
}
arr[j] = t;
}
}

Comparable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package demo.helper.model;

/**
* 对象排序,必须要重写父类的equals方法
* 实现Comparable接口,必须重写compareTo方法
*
* @author jingLv
* @date 2020/11/10
*/
public class Student implements Comparable<Student> {
private final String name;
private final int score;

public Student(String name, int score) {
this.name = name;
this.score = score;
}

/**
* 自定义比较方法,重写Object父类equals方法
* 学生类的对象比较,转换为字符串的比较
*
* @param student 学生对象
* @return boolean
*/
@Override
public boolean equals(Object student) {
// 判断当期对象是否是student对象,如果是的话则不需要进行转换
if (this == student) {
return true;
}
// 判断student是否是空对象,如果是空则返回false
if (student == null) {
return false;
}
// 判断当前的类对象是否与student的类对象相等,如果不等则返回false
if (this.getClass() != student.getClass()) {
return false;
}
Student another = (Student) student;
return this.name.equals(another.name);
}

@Override
public int compareTo(Student another) {
return this.score - another.score;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}