每一秒钟的时间都值得铭记

0%

排序算法之聊聊你所不知道的冒泡排序

什么是冒泡排序

冒泡排序是一种基础的交换排序,其思想是,把相邻的元素两两比较,当一个元素大于右侧相邻的元素时,交换它们的位置:当一个元素小于或等于右侧的元素时,位置不变。

冒泡排序是一种稳定排序,值相等的元素并不会打乱原有的顺序,由于该排序算法的每一轮都要遍历元素,总共遍历(元素数量 - 1)轮,所以平均时间复杂度是O(n^2)。

工欲善其事,必先利其器

在讲述冒泡排序之前,我们先来搭建一个简单的排序算法编写的环境,这个环境可以帮助我们更加方便地编写和测试排序算法。

SortUtils

一个排序算法的工具类,其中包含了排序算法编写和测试的一些基本操作。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public class SortUtils {

/**
* 随机生成一个整形数组
*
* @param size 数组大小
* @param rangeL 数组左边范围(包含)
* @param rangeR 数组右边范围(包含)
* @return 一个整形数组
*/
public static int[] generateRandomArray(int size, int rangeL, int rangeR) {
int[] arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = (int) (Math.random() * (rangeR - rangeL + 1)) + rangeL;
}
return arr;
}

/**
* 生成一个近乎有序的整形数组
*
* @param size 数组大小
* @param swapTimes 将有序数组打乱次数
* @return 一个近乎有序的整形数组
*/
public static int[] generateAlmostOrderlyArray(int size, int swapTimes) {
int[] arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i;
}
for (int i = 0; i < swapTimes; i++) {
int index1 = (int) (Math.random() * size);
int index2 = (int) (Math.random() * size);
swap(arr, index1, index2);
}
return arr;
}

/**
* 判断一个数组是否有序,有序返回true,无序返回false
*
* @param arr 数组
* @return 是否有序
*/
public static boolean isOrderly(int[] arr) {
return isOrderAsc(arr) || isOrderDesc(arr);
}

/**
* 判断一个数组是否顺序有序,有序返回true,无序返回false
*
* @param arr 数组
* @return 是否有序
*/
private static boolean isOrderAsc(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
return false;
}
}
return true;
}

/**
* 判断一个数组是否倒序有序,有序返回true,无序返回false
*
* @param arr 数组
* @return 是否有序
*/
private static boolean isOrderDesc(int[] arr) {
for (int i = arr.length - 1; i > 0; i--) {
if (arr[i] > arr[i - 1]) {
return false;
}
}
return true;
}

/**
* 拷贝一个整形数组
*
* @param arr 整形数组
* @return 新的整形数组
*/
public static int[] copyIntArray(int[] arr) {
int[] copyArr = new int[arr.length];
System.arraycopy(arr, 0, copyArr, 0, arr.length);
return copyArr;
}

/**
* 交换数组中的两个索引的位置
*
* @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;
}

/**
* 打印数组
*
* @param arr 整形数组
*/
public static void printArray(int[] arr) {
for (int i : arr) {
System.out.print(i + "\t");
}
System.out.println();
}
}

Sort函数式接口

一个基于 JDK8 Lambda 特性的函数式接口,所以该排序算法环境的搭建基于 JDK8+ 的版本。

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Sort {

/**
* 排序方法
*
* @param arr 数组
*/
void sort(int[] arr);
}

SortRunner

SortRunner 中有用于运行 Sort 函数式接口的方法,可以在该类的方法下编写一些特定的操作,用于辅助排序算法编写和测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SortRunner {

/**
* 运行排序算法的方法
*
* @param arr 一个数组
* @param sort Sort函数式接口
*/
public static void run(int[] arr, Sort sort) {
System.out.print("数组排序前:");
SortUtils.printArray(arr);
long startTime = System.currentTimeMillis();
sort.sort(arr);
long endTime = System.currentTimeMillis();
if (SortUtils.isOrderly(arr)) {
System.out.print("数组排序后:");
SortUtils.printArray(arr);
System.out.println("数组排序所需时间为:" + (endTime - startTime) + "毫秒");
} else {
System.out.println("数组排序失败");
}
System.out.println();
}
}

冒泡排序

普通冒泡排序

普通的冒泡排序是把相邻的元素两两比较,直到所有元素都比较结束为止

1
2
3
4
5
6
7
8
9
10
11
System.out.println("普通冒泡排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
(arr) -> {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
SortUtils.swap(arr, j, j + 1);
}
}
}
});

这是最为常见的冒泡排序,编写简单,易于理解,但是在一些特殊情况下,可能会浪费一些性能。

比如排序到 n - i 轮的时候,这是遍历数组,发现数组已经有序了,剩下的 i -1 轮其实已经可以不进行遍历了。

所以我们可以对冒泡排序进行一些优化。

布尔类型变量判断有序

这种冒泡排序的思想是,在每一轮遍历数组前,设置一个变量用于标识数组有序,如果在该轮中,数组相邻的元素之间发生了交换,则说明此时数组仍处于无序状态,反之则有序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 布尔类型变量判断有序优化冒泡排序
System.out.println("布尔类型变量判断有序优化冒泡排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
(arr) -> {
for (int i = 0; i < arr.length - 1; i++) {
boolean isSorted = true;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
SortUtils.swap(arr, j, j + 1);
isSorted = false;
}
}
if (isSorted) {
break;
}
}
});

无序边界值

布尔类型变量判断有序优化冒泡排序,本质上是减少了冒泡排序的轮数,可以让冒泡排序轮询 <= n - 1 次,但是对于每一轮交换的元素个数,依然是固定的。

但是有的时候,每一轮交换元素的时候,可能交换 n - i 次,数组后段的元素其实就已经有序了,这时继续向后遍历比较是无用的。

所以为了解决这种情况,可以使用无序边界来优化冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 无序边界值优化冒泡排序
System.out.println("无序边界值优化冒泡排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
(arr) -> {
int lastExchangeIndex = 0;
int sortBorder = arr.length - 1;
for (int i = 0; i < arr.length - 1; i++) {
boolean isSorted = true;
for (int j = 0; j < sortBorder; j++) {
if (arr[j] > arr[j + 1]) {
SortUtils.swap(arr, j, j + 1);
isSorted = false;
lastExchangeIndex = j;
}
}
sortBorder = lastExchangeIndex;
if (isSorted) {
break;
}
}
});

鸡尾酒排序

在一些特殊情况下,比如对一些近似有序的数组进行排序的时候,可以考虑用鸡尾酒排序来对数据进行排序。

鸡尾酒排序的思想是:在冒泡排序比较元素和交换的过程中,使用双向遍历比较和交换元素。

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
// 鸡尾酒排序
System.out.println("鸡尾酒排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
(arr) -> {
for (int i = 0; i < arr.length / 2; i++) {
// 奇数轮,从左往右排序
boolean isSorted = true;
for (int j = i; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
SortUtils.swap(arr, j, j + 1);
isSorted = false;
}
}
if (isSorted) {
break;
}

// 偶数轮,从右往左毕竟
isSorted = true;
for (int j = arr.length - i - 1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
SortUtils.swap(arr, j, j - 1);
isSorted = false;
}
}
if (isSorted) {
break;
}
}
});

左右无序边界值

鸡尾酒排序同样可以使用无序边界值的方法对算法进行优化,不过因为鸡尾酒排序是双向的,所以无序值也是有左右两个。

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
// 左右无序边界值优化鸡尾酒排序
System.out.println("左右无序边界值优化鸡尾酒排序");
SortRunner.run(SortUtils.generateRandomArray(10000, 100, 100000),
(arr) -> {
// 左边最后进行元素交换的索引位置
int leftExchangeIndex = 0;
// 左边界
int leftSortBorder = leftExchangeIndex;
// 右边最后进行元素交换的索引位置
int rightExchangeIndex = arr.length - 1;
// 右边界
int rightSortBorder = rightExchangeIndex;
for (int i = 0; i < arr.length / 2; i++) {
// 奇数轮,从左往右排序
boolean isSorted = true;
for (int j = i; j < rightSortBorder; j++) {
if (arr[j] > arr[j + 1]) {
SortUtils.swap(arr, j, j + 1);
isSorted = false;
rightExchangeIndex = j;
}
}
rightSortBorder = rightExchangeIndex;
if (isSorted) {
break;
}

// 偶数轮,从右往左毕竟
isSorted = true;
for (int j = arr.length - i - 1; j > leftSortBorder; j--) {
if (arr[j] < arr[j - 1]) {
SortUtils.swap(arr, j, j - 1);
isSorted = false;
leftExchangeIndex = j;
}
}
leftSortBorder = leftExchangeIndex;
if (isSorted) {
break;
}
}
});

鸡尾酒排序的效率

数组长度:100000

冒泡排序 鸡尾酒排序
无序数组 27412毫秒 12868毫秒
近似有序 13962毫秒 43毫秒

以上的测试数据仅仅为简单测试,具体情况请自行测试,另外,如果排序数组长度过长,建议修改 SortRunner.run() 方法,去掉一些无用的操作步骤,减少排序测试的总体时长

最后

本文Github https://github.com/herenpeng/code-learn 已收录,欢迎Star

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------这是我的底线^_^-------------