重生之我在Java学斩神。

汇总

关于稳定性和占用内存的说明:

  • 稳定性:如果A = B,排序前A在B前面,排序后A也在B前面,则为稳定;如果出现了排序后A在B后面的情况,则为不稳定。
  • 占用内存:in代表占用常数内存,不消耗额外内存;out代表消耗常数内存。
    十大排序算法汇总

开始

笔记中使用了自定义的类,分别是SortEnum枚举类和SortUtil工具类。
SortEnum枚举类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum SortEnum {  
BUBBLE("冒泡排序"),
SELECTION("选择排序"),
INSERTION("插入排序"),
SHELL("希尔排序"),
MERGE("归并排序"),
QUICK("快速排序"),
HEAP("堆排序"),
COUNTING("计数排序"),
BUCKET("桶排序"),
RADIX("基数排序");

// 排序算法名称
private final String name;

SortEnum(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

SortUtil工具类如下:

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
// 排序工具类  
public class SortUtil {

// 测试数组
private static final int[] TEST_ARRAY = {67, 21, 38, 10, 90, 30, 70, 77, 44, 58, 71, 31, 12, 68, 13};

// 打印数组
private static void print(SortEnum sort, int[] arr) {
System.out.print(sort.getName());
System.out.print(": [");
for (int i : arr) {
System.out.print(i + ", ");
}
System.out.println("\b\b]");
}

/**
* 交换数组中的两个元素
*
* @param arr 数组
* @param i 被交换的元素索引
* @param j 被交换的元素索引
*/
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

public static void sort(SortEnum sort) {
int[] arr = TEST_ARRAY.clone();
switch (sort) {
case BUBBLE -> BubbleSort.sort(arr);
case SELECTION -> SelectionSort.sort(arr);
case INSERTION -> InsertionSort.sort(arr);
case SHELL -> ShellSort.sort(arr);
case MERGE -> MergeSort.sort(arr);
case QUICK -> QuickSort.sort(arr);
case HEAP -> HeapSort.sort(arr);
default -> {
System.out.println("未知排序算法");
return;
}
}
print(sort, arr);
}
}

在使用时直接通过 SortUtil 调用 SortEnum 即可:

1
2
3
public static void main(String[] args) {  
SortUtil.sort(SortEnum.SELECT);
}

⭐冒泡排序

从头开始比较相邻元素,如果第一个元素比第二个元素大,就交换它们两个,直到最后一个元素,这样最后一个就是整个数组中最大的数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BubbleSort {  
public static void sort(int[] arr) {
int len = arr.length;
// 遍历次数
for (int i = 0; i < len - 1; i++) {
// 最大的数在arr[len - i - 1]位置
for (int j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
SortUtil.swap(arr, j, j + 1);
}
}
}
}
}

⭐选择排序

在序列中找到最小/最大的元素,存放到序列的起始位置,然后再继续找到最小/最大的元素,放到已排序好的序列结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SelectionSort {  
public static void sort(int[] arr) {
int len = arr.length;
for (int i = 0; i < len; i++) {
// 初始化最小元素下标为i
int minIndex = i;
for (int j = i + 1; j < len; j++) {
// 找到比arr[i]更小的元素
if(arr[j] < arr[minIndex]){
minIndex = j;
}
}
// 如果不是i本身的话,交换他们
if(minIndex!= i) {
SortUtil.swap(arr, i, minIndex);
}
}
}
}

⭐插入排序

将第一个元素视为已排序好的序列。选择下一个元素,并在已排序好的序列中从后往前扫描,直到找到比该元素小的元素,将该元素插入到其之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InsertionSort {  
public static void sort(int[] arr) {
int len = arr.length;
for (int i = 1; i < len; i++) {
int temp = arr[i];
int j;
// 从后往前遍历已排序的数组
for (j = i - 1; j >= 0 && arr[j] > temp; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
}
}

🔋希尔排序

设置一个增量gap(一般是数组长度/2),将整个序列分为gap个子序列,分别对子序列进行插入排序。然后将增量设置为gap=gap/2,再对排序了一次的序列分割成gap个子序列,继续进行插入排序。直到gap=1时再进行一次插入排序即可完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ShellSort {  
public static void sort(int[] arr) {
int len = arr.length;
// 增量
for(int gap = len / 2; gap > 0; gap /= 2){
for (int i = gap; i < len; i++) {
int temp = arr[i];
int j;
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
}
}

🔋归并排序

对序列进行递归再合并,即为归并。将长度为n序列持续递归分割为两个长度为n/2的子序列,直到子序列中只有一个元素。然后在递归的返回过程中,将子序列两两合并,并在合并的过程中完成子序列的排序。

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
public class MergeSort {  
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}

private static void mergeSort(int[] arr, int left, int right) {
// 只有一个元素,不需要排序
if (left == right) {
return;
}
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid); // 左递归排序
mergeSort(arr, mid + 1, right); // 右递归排序
merge(arr, left, mid, right); // 合并
}

private static void merge(int[] arr, int left, int mid, int right) {
// 临时数组
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;

// 比较两个子数组中的元素
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 如果左子数组还有剩余元素,全部放入临时数组
while (i <= mid) {
temp[k++] = arr[i++];
}

// 如果右子数组还有剩余元素,全部放入临时数组
while (j <= right) {
temp[k++] = arr[j++];
}

System.arraycopy(temp, 0, arr, left, temp.length);
}
}

⭐快速排序

选择一个基准pivot。将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面。该操作结束之后,该基准就处于数列的中间位置(分区partition)。然后递归的进行上述操作即可。

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
public class QuickSort {  
public static void sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int pivot = partition(arr, left, right);
// 对基准值左边排序
quickSort(arr, 0, pivot - 1);
// 对基准值右边排序
quickSort(arr, pivot + 1, right);
}
}
private static int partition(int[] arr, int left, int right) {
// 基准
int pivot = arr[left];
int i = left + 1;
// 当前元素小于基准,交换他们
for (int j = i; j <= right && arr[j] < pivot; j++) {
SortUtil.swap(arr, i++, j);
}
// 交换基准值元素和小于基准值元素数组的最后一个元素
SortUtil.swap(arr, left, i - 1);
return i - 1;
}
}

🔋堆排序

首先构建一个大根堆或小根堆,然后将堆顶元素和堆的最后一个元素进行交换。交换完成后,需要再次调整堆,使其满足大根堆或小根堆的特性。

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
public class HeapSort {  
private static int len;

public static void sort(int[] arr) {
len = arr.length;
// 构建大顶堆
for (int i = len / 2 - 1; i >= 0; i--) {
modifyHeap(arr, i);
}
// 堆排序
for (int i = len - 1; i > 0; i--) {
// 交换堆顶元素和最后一个元素
SortUtil.swap(arr, 0, len - 1);
len--;
// 调整堆
modifyHeap(arr, 0);
}
}
private static void modifyHeap(int[] arr, int index) {
// index节点的左孩子和右孩子
int left = index * 2 + 1;
int right = index * 2 + 2;
// 较大的节点
int largest = index;
// 找出左右孩子中较大的节点
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
// 如果largest不是index节点,则交换
if (largest!= index) {
SortUtil.swap(arr, index, largest);
// 继续调整子树
modifyHeap(arr, largest);
}
}
}

计数排序

首先,我们需要找出数组中的最大值 max 或者最小值 min,然后创建一个计数数组 countArray,数组长度为 max - min + 1,元素默认值为0。通过遍历原数组 arr,以 arr[i] - min作为 countArray 的索引,arr[i]在 arr 出现的次数作为对应的值。最后遍历 countArray,只要索引不为0,将对应的 value 加上min输出返回到原数组即可。

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
public class CountingSort {  
public static void sort(int[] arr) {
if (arr.length < 2) return;
int min = arr[0], max = arr[0];
// 计算 min 和 max for (int value : arr) {
if (value < min) {
min = value;
} else if (value > max) {
max = value;
}
}
// 计算计数数组
int[] countArray = new int[max - min + 1];
// 计算索引和出现次数
for (int i : arr) {
countArray[i - min]++;
}
// 将计数数组输出返回到原数组
for (int arrIndex = 0, countIndex = 0; countIndex < countArray.length; countIndex++) {
// 毫无可读性(╯°□°)╯︵ ┻━┻
while (countArray[countIndex]-- > 0) {
arr[arrIndex++] = countIndex + min;
}
}
}
}

桶排序

首先需要设置一个 bucketSize,决定每个 bucket 能放置多少数值。然后遍历数组,将值放到对应的 bucket 里面。接着对每个非空的 bucket 进行排序,最后将 bucket 拼接即可完成排序。
bucketSize 的大小至关重要,性能最好时每个桶都能均匀放置所有数值。

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
// bucket 的大小  
private static final int BUCKET_SIZE = 20;

// 为了统一调用才使用 int[] arr,建议换成 List
public static void sort(int[] arr) {
if (arr.length < 2) return;
List<Integer> list = new ArrayList<>();
for (int value : arr) {
list.add(value);
}
list = sort(list, BUCKET_SIZE);
for (int i = 0; i < arr.length; i++) {
arr[i] = list.get(i);
}
}

private static List<Integer> sort(List<Integer> list, int bucketSize) {
if (list.size() < 2 || bucketSize == 0) {
return list;
}
int min = list.get(0), max = list.get(0);
for (Integer integer : list) {
if (integer < min) {
min = integer;
} else if (integer > max) {
max = integer;
}
} // 计算 bucket 的数量
int bucketCount = (max - min) / bucketSize + 1;
List<List<Integer>> buckets = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 将元素放入 bucket 中
for (Integer integer : list) {
int index = (integer - min) / bucketSize;
buckets.get(index).add(integer);
}
// 对每个 bucket 进行排序
for (int i = 0; i < bucketCount; i++) {
if (!buckets.get(i).isEmpty()) {
// 此处排序算法可以换成其他排序算法,本项目大部分都使用 int[] 作为参数, 不好更换
buckets.set(i, sort(buckets.get(i), bucketSize / 2));
}
} // 将 bucket 中的元素放回原数组
List<Integer> result = new ArrayList<>();
for (List<Integer> bucket : buckets) {
result.addAll(bucket);
}
return result;
}

基数排序

基数排序需要先获取数组中的最大值 max,然后计算出max的位数,即为迭代次数 n。然后新建一个二维数组 radix,其一维长度为 10,然后遍历数组,根据数组每一位的值放入 radix。最后将 radix遍历并赋值给原数组,重复 n 次即可。

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
public static void sort(int[] arr) {  
if (arr.length < 2) return;
int max = 0;
// 获取最大值
for (int value : arr) {
if (value > max) {
max = value;
}
} // 计算位数
int n = 0;
while (max != 0) {
max /= 10;
n++;
}
// 基数排序
for (int i = 0; i < n; i++) {
List<List<Integer>> radix = new ArrayList<>();
// 初始化
for (int j = 0; j < 10; j++) {
radix.add(new ArrayList<>());
}
// 按位分组添加到radix
for (int value : arr) {
int digit = (value / (int) Math.pow(10, i)) % 10;
radix.get(digit).add(value);
}
// 按位排序,输出返回到原数组
int index = 0;
for (List<Integer> list : radix) {
for (int value : list) {
arr[index++] = value;
}
}
}
}

总结

以下是在不同数据量的情况下合适的排序算法:

  • 低数据量:插入排序或选择排序。插入排序具有稳定性,选择排序的效率高于插入排序。
  • 中数据量:希尔排序。
  • 高数据量:快速排序、归并排序或堆排序。快速排序适合元素分布随机,归并排序具有稳定性,堆排序适合元素分布接近排序好的情况。